| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "unsafe"
- "github.com/gin-gonic/gin"
- "github.com/tidwall/gjson"
- "github.com/tidwall/sjson"
- )
- func validateOpenAIWSBearerToken(account *Account, token string) error {
- if account == nil {
- return errors.New("account is nil")
- }
- if strings.TrimSpace(token) == "" && !account.IsOpenAIAgentIdentity() {
- return errors.New("token is empty")
- }
- return nil
- }
- func (s *OpenAIGatewayService) buildOpenAIResponsesWSURL(account *Account) (string, error) {
- if account == nil {
- return "", errors.New("account is nil")
- }
- var targetURL string
- switch account.Type {
- case AccountTypeOAuth:
- targetURL = chatgptCodexURL
- case AccountTypeSetupToken:
- if account.IsOpenAIOAuthLike() {
- targetURL = chatgptCodexURL
- } else {
- targetURL = openaiPlatformAPIURL
- }
- case AccountTypeAPIKey:
- baseURL := account.GetOpenAIBaseURL()
- if account.UsesNativeCNResponses() && account.IsAdaptiveAPIProtocol() {
- baseURL = account.GetCNProtocolBaseURL(APIProtocolResponses)
- }
- if baseURL == "" {
- targetURL = openaiPlatformAPIURL
- } else {
- validatedURL, err := s.validateUpstreamBaseURL(baseURL)
- if err != nil {
- return "", err
- }
- targetURL = buildOpenAIResponsesURLForPlatform(account.Platform, validatedURL)
- }
- default:
- targetURL = openaiPlatformAPIURL
- }
- parsed, err := url.Parse(strings.TrimSpace(targetURL))
- if err != nil {
- return "", fmt.Errorf("invalid target url: %w", err)
- }
- switch strings.ToLower(parsed.Scheme) {
- case "https":
- parsed.Scheme = "wss"
- case "http":
- parsed.Scheme = "ws"
- case "wss", "ws":
- // 保持不变
- default:
- return "", fmt.Errorf("unsupported scheme for ws: %s", parsed.Scheme)
- }
- return parsed.String(), nil
- }
- func (s *OpenAIGatewayService) buildOpenAIWSHeaders(
- ctx context.Context,
- c *gin.Context,
- account *Account,
- token string,
- decision OpenAIWSProtocolDecision,
- isCodexCLI bool,
- turnState string,
- turnMetadata string,
- promptCacheKey string,
- routingModel string,
- routingServiceTier string,
- ) (http.Header, openAIWSSessionHeaderResolution, error) {
- headers := make(http.Header)
- if account == nil || !account.IsOpenAIAgentIdentity() {
- headers.Set("authorization", "Bearer "+token)
- }
- sessionResolution := resolveOpenAIWSSessionHeaders(c, promptCacheKey)
- if c != nil && c.Request != nil {
- if v := strings.TrimSpace(c.Request.Header.Get("accept-language")); v != "" {
- headers.Set("accept-language", v)
- }
- for _, value := range c.Request.Header.Values("x-codex-beta-features") {
- if value = strings.TrimSpace(value); value != "" {
- headers.Add("x-codex-beta-features", value)
- }
- }
- for _, name := range [...]string{
- "x-codex-window-id",
- "x-codex-installation-id",
- "session-id",
- "thread-id",
- "x-client-request-id",
- } {
- if value := c.Request.Header.Get(name); strings.TrimSpace(value) != "" {
- headers.Set(name, value)
- }
- }
- }
- // 真实 Codex 的 WS 握手同样携带会话级 x-codex-beta-features
- // (client.rs build_websocket_headers 复用 build_responses_headers),
- // 客户端未声明时补成默认形态,与 HTTP 出站保持一致。放在客户端头拷贝
- // 之外:该头是账号/会话级属性,不依赖入站请求是否存在,也避免预热与
- // 实际请求因头差异落进不同的连接池兼容分桶。
- applyOpenAICodexBetaFeatures(c, account, headers)
- // OAuth 账号:将 apiKeyID 混入 session 标识符,防止跨用户会话碰撞。
- if account != nil && account.UsesOpenAICodexProtocol() {
- apiKeyID := getAPIKeyIDFromContext(c)
- if sessionResolution.SessionID != "" {
- headers.Set("session_id", isolateOpenAIUpstreamSessionID(apiKeyID, codexAccountIdentitySource(c, account), sessionResolution.SessionID))
- }
- if sessionResolution.ConversationID != "" {
- headers.Set("conversation_id", isolateOpenAIUpstreamSessionID(apiKeyID, codexAccountIdentitySource(c, account), sessionResolution.ConversationID))
- }
- } else {
- if sessionResolution.SessionID != "" {
- headers.Set("session_id", sessionResolution.SessionID)
- }
- if sessionResolution.ConversationID != "" {
- headers.Set("conversation_id", sessionResolution.ConversationID)
- }
- }
- if state := strings.TrimSpace(turnState); state != "" {
- headers.Set(openAIWSTurnStateHeader, state)
- }
- if metadata := strings.TrimSpace(turnMetadata); metadata != "" {
- headers.Set(openAIWSTurnMetadataHeader, metadata)
- }
- applyCodexAccountIdentityHeaders(headers, codexAccountIdentitySource(c, account), getAPIKeyIDFromContext(c))
- applyStagedCodexFingerprintHeaders(c, account, headers)
- if account != nil && account.UsesOpenAICodexProtocol() {
- if err := resolveAndSetOpenAIChatGPTAccountHeaders(ctx, s.accountRepo, headers, account); err != nil {
- return nil, sessionResolution, fmt.Errorf("resolve chatgpt account headers: %w", err)
- }
- headers.Set("originator", resolveOpenAIUpstreamOriginator(c, isCodexCLI))
- }
- betaValue := openAIWSBetaV2Value
- if decision.Transport == OpenAIUpstreamTransportResponsesWebsocket {
- betaValue = openAIWSBetaV1Value
- }
- headers.Set("OpenAI-Beta", betaValue)
- customUA := ""
- if account != nil {
- customUA = account.GetOpenAIUserAgent()
- }
- if strings.TrimSpace(customUA) != "" {
- headers.Set("user-agent", customUA)
- } else if c != nil {
- if ua := strings.TrimSpace(c.GetHeader("User-Agent")); ua != "" {
- headers.Set("user-agent", ua)
- }
- }
- if s != nil && s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
- headers.Set("user-agent", CodexCanonicalUserAgent())
- }
- // 终态收口:WS 握手与 HTTP 出站共用同一套身份语义,账号级自定义 UA 同样作为
- // 管理员显式配置传入(上面写进 headers 的值只在强制统一被关闭时才参与配对)。
- if account != nil && account.UsesOpenAICodexProtocol() {
- enforceCodexIdentityHeadersWithUA(headers, s.codexIdentityOverrideUA(account))
- }
- // 账号级请求头覆写(仅 openai api_key 账号启用时生效;OAuth 路径 no-op)。
- // 覆盖所有 WS 模式(ctx_pool/dedicated/passthrough)的握手头。
- account.ApplyHeaderOverrides(headers)
- setOpenAICodexRoutingHint(headers, account, routingModel, routingServiceTier)
- logOpenAIRoutingDiagnostics(
- ctx,
- account,
- string(decision.Transport),
- routingModel,
- routingServiceTier,
- strings.TrimSpace(headers.Get(openAICodexRoutingHintHeader)) != "",
- "soft_routing_hint",
- )
- return headers, sessionResolution, nil
- }
- func (s *OpenAIGatewayService) buildOpenAIWSCreatePayload(reqBody map[string]any, account *Account) map[string]any {
- // OpenAI WS Mode 协议:response.create 字段与 HTTP /responses 基本一致。
- // 保留 stream 字段(与 Codex CLI 一致),仅移除 background。
- payload := make(map[string]any, len(reqBody)+1)
- for k, v := range reqBody {
- payload[k] = v
- }
- delete(payload, "background")
- if _, exists := payload["stream"]; !exists {
- payload["stream"] = true
- }
- payload["type"] = "response.create"
- // OAuth 默认保持 store=false,避免误依赖服务端历史。
- if account != nil && account.UsesOpenAICodexProtocol() && !s.isOpenAIWSStoreRecoveryAllowed(account) {
- payload["store"] = false
- }
- return payload
- }
- func setOpenAIWSTurnMetadata(payload map[string]any, turnMetadata string) {
- if len(payload) == 0 {
- return
- }
- metadata := strings.TrimSpace(turnMetadata)
- if metadata == "" {
- return
- }
- switch existing := payload["client_metadata"].(type) {
- case map[string]any:
- existing[openAIWSTurnMetadataHeader] = metadata
- payload["client_metadata"] = existing
- case map[string]string:
- next := make(map[string]any, len(existing)+1)
- for k, v := range existing {
- next[k] = v
- }
- next[openAIWSTurnMetadataHeader] = metadata
- payload["client_metadata"] = next
- default:
- payload["client_metadata"] = map[string]any{
- openAIWSTurnMetadataHeader: metadata,
- }
- }
- }
- func (s *OpenAIGatewayService) isOpenAIWSStoreRecoveryAllowed(account *Account) bool {
- if account != nil && account.IsOpenAIWSAllowStoreRecoveryEnabled() {
- return true
- }
- if s != nil && s.cfg != nil && s.cfg.Gateway.OpenAIWS.AllowStoreRecovery {
- return true
- }
- return false
- }
- func (s *OpenAIGatewayService) isOpenAIWSStoreDisabledInRequest(reqBody map[string]any, account *Account) bool {
- if account != nil && account.UsesOpenAICodexProtocol() && !s.isOpenAIWSStoreRecoveryAllowed(account) {
- return true
- }
- if len(reqBody) == 0 {
- return false
- }
- rawStore, ok := reqBody["store"]
- if !ok {
- return false
- }
- storeEnabled, ok := rawStore.(bool)
- if !ok {
- return false
- }
- return !storeEnabled
- }
- func (s *OpenAIGatewayService) isOpenAIWSStoreDisabledInRequestRaw(reqBody []byte, account *Account) bool {
- if account != nil && account.UsesOpenAICodexProtocol() && !s.isOpenAIWSStoreRecoveryAllowed(account) {
- return true
- }
- if len(reqBody) == 0 {
- return false
- }
- storeValue := gjson.GetBytes(reqBody, "store")
- if !storeValue.Exists() {
- return false
- }
- if storeValue.Type != gjson.True && storeValue.Type != gjson.False {
- return false
- }
- return !storeValue.Bool()
- }
- func (s *OpenAIGatewayService) openAIWSStoreDisabledConnMode() string {
- if s == nil || s.cfg == nil {
- return openAIWSStoreDisabledConnModeStrict
- }
- mode := strings.ToLower(strings.TrimSpace(s.cfg.Gateway.OpenAIWS.StoreDisabledConnMode))
- switch mode {
- case openAIWSStoreDisabledConnModeStrict, openAIWSStoreDisabledConnModeAdaptive, openAIWSStoreDisabledConnModeOff:
- return mode
- case "":
- // 兼容旧配置:仅配置了布尔开关时按旧语义推导。
- if s.cfg.Gateway.OpenAIWS.StoreDisabledForceNewConn {
- return openAIWSStoreDisabledConnModeStrict
- }
- return openAIWSStoreDisabledConnModeOff
- default:
- return openAIWSStoreDisabledConnModeStrict
- }
- }
- func shouldForceNewConnOnStoreDisabled(mode, lastFailureReason string) bool {
- switch mode {
- case openAIWSStoreDisabledConnModeOff:
- return false
- case openAIWSStoreDisabledConnModeAdaptive:
- reason := strings.TrimPrefix(strings.TrimSpace(lastFailureReason), "prewarm_")
- switch reason {
- case "policy_violation", "message_too_big", "auth_failed", "write_request", "write":
- return true
- default:
- return false
- }
- default:
- return true
- }
- }
- func dropPreviousResponseIDFromRawPayload(payload []byte) ([]byte, bool, error) {
- return dropPreviousResponseIDFromRawPayloadWithDeleteFn(payload, sjson.DeleteBytes)
- }
- func dropPreviousResponseIDFromRawPayloadWithDeleteFn(
- payload []byte,
- deleteFn func([]byte, string) ([]byte, error),
- ) ([]byte, bool, error) {
- if len(payload) == 0 {
- return payload, false, nil
- }
- if !gjson.GetBytes(payload, "previous_response_id").Exists() {
- return payload, false, nil
- }
- if deleteFn == nil {
- deleteFn = sjson.DeleteBytes
- }
- updated := payload
- for i := 0; i < openAIWSMaxPrevResponseIDDeletePasses &&
- gjson.GetBytes(updated, "previous_response_id").Exists(); i++ {
- next, err := deleteFn(updated, "previous_response_id")
- if err != nil {
- return payload, false, err
- }
- updated = next
- }
- return updated, !gjson.GetBytes(updated, "previous_response_id").Exists(), nil
- }
- func setPreviousResponseIDToRawPayload(payload []byte, previousResponseID string) ([]byte, error) {
- normalizedPrevID := strings.TrimSpace(previousResponseID)
- if len(payload) == 0 || normalizedPrevID == "" {
- return payload, nil
- }
- updated, err := sjson.SetBytes(payload, "previous_response_id", normalizedPrevID)
- if err == nil {
- return updated, nil
- }
- var reqBody map[string]any
- if unmarshalErr := decodeOpenAIJSONUseNumber(payload, &reqBody); unmarshalErr != nil {
- return nil, err
- }
- reqBody["previous_response_id"] = normalizedPrevID
- rebuilt, marshalErr := json.Marshal(reqBody)
- if marshalErr != nil {
- return nil, marshalErr
- }
- return rebuilt, nil
- }
- func shouldInferIngressFunctionCallOutputPreviousResponseID(
- storeDisabled bool,
- turn int,
- signals ToolContinuationSignals,
- currentPreviousResponseID string,
- expectedPreviousResponseID string,
- ) bool {
- if !storeDisabled || turn <= 1 || !signals.HasFunctionCallOutput {
- return false
- }
- if strings.TrimSpace(currentPreviousResponseID) != "" {
- return false
- }
- if signals.HasFunctionCallOutputMissingCallID {
- return false
- }
- // If the client already sent the actual tool-call context, treat this as
- // a full replay / self-contained continuation payload rather than
- // downgrading it into an inferred delta continuation. item_reference alone
- // is not enough on the store=false WS path: it still needs a valid prior
- // response anchor so upstream can resolve the referenced function_call.
- if signals.HasToolCallContext {
- return false
- }
- return strings.TrimSpace(expectedPreviousResponseID) != ""
- }
- func alignStoreDisabledPreviousResponseID(
- payload []byte,
- expectedPreviousResponseID string,
- ) ([]byte, bool, error) {
- if len(payload) == 0 {
- return payload, false, nil
- }
- expected := strings.TrimSpace(expectedPreviousResponseID)
- if expected == "" {
- return payload, false, nil
- }
- current := openAIWSPayloadStringFromRaw(payload, "previous_response_id")
- if current == "" || current == expected {
- return payload, false, nil
- }
- withoutPrev, removed, dropErr := dropPreviousResponseIDFromRawPayload(payload)
- if dropErr != nil {
- return payload, false, dropErr
- }
- if !removed {
- return payload, false, nil
- }
- updated, setErr := setPreviousResponseIDToRawPayload(withoutPrev, expected)
- if setErr != nil {
- return payload, false, setErr
- }
- return updated, true, nil
- }
- // Replay 状态所有权不变式:replay 序列中的 json.RawMessage 正文一经放入即视为
- // 不可变,所有持有者共享同一份字节,任何修改都必须整体替换元素或重建 payload。
- // 序列头数组在跨持有者保存时必须新建(combineOpenAIWSReplayItems),禁止通过
- // 共享头 append,否则会写入其他持有者可见的底层数组。
- // combineOpenAIWSReplayItems 合并历史与增量为新头数组,正文共享不复制。
- func combineOpenAIWSReplayItems(history, delta []json.RawMessage) []json.RawMessage {
- if len(delta) == 0 {
- return history
- }
- combined := make([]json.RawMessage, 0, len(history)+len(delta))
- combined = append(combined, history...)
- return append(combined, delta...)
- }
- // openAIWSPayloadStringView 返回与 payload 共享底层数组的零拷贝 string 视图,
- // 供 gjson.Get 使用(gjson.GetBytes 会整段复制结果 Raw,对 input 这类占
- // payload 主体的字段是每次 O(payload) 分配)。调用方必须保证 payload 在结果
- // 存活期间不可变(replay 所有权不变式)。
- func openAIWSPayloadStringView(payload []byte) string {
- return unsafe.String(unsafe.SliceData(payload), len(payload))
- }
- // openAIWSRawMessageFromResult 优先返回 parent 的子切片(gjson 值零拷贝共享),
- // Index 不可用时回退为复制。共享要求 parent 遵守上面的不可变约定。
- func openAIWSRawMessageFromResult(parent []byte, value gjson.Result) json.RawMessage {
- idx := value.Index
- if idx > 0 && idx+len(value.Raw) <= len(parent) && string(parent[idx:idx+len(value.Raw)]) == value.Raw {
- return json.RawMessage(parent[idx : idx+len(value.Raw)])
- }
- return json.RawMessage(value.Raw)
- }
- func normalizeOpenAIWSJSONForCompare(raw []byte) ([]byte, error) {
- trimmed := bytes.TrimSpace(raw)
- if len(trimmed) == 0 {
- return nil, errors.New("json is empty")
- }
- var decoded any
- if err := decodeOpenAIJSONUseNumber(trimmed, &decoded); err != nil {
- return nil, err
- }
- return json.Marshal(decoded)
- }
- func normalizeOpenAIWSJSONForCompareOrRaw(raw []byte) []byte {
- normalized, err := normalizeOpenAIWSJSONForCompare(raw)
- if err != nil {
- return bytes.TrimSpace(raw)
- }
- return normalized
- }
- func normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(payload []byte) ([]byte, error) {
- if len(payload) == 0 {
- return nil, errors.New("payload is empty")
- }
- var decoded map[string]any
- if err := decodeOpenAIJSONUseNumber(payload, &decoded); err != nil {
- return nil, err
- }
- delete(decoded, "input")
- delete(decoded, "previous_response_id")
- // Codex changes transport-only metadata for every response.create. These fields
- // do not alter the context referenced by previous_response_id and are excluded
- // from Codex's own websocket reuse comparison.
- delete(decoded, "client_metadata")
- delete(decoded, "stream_options")
- // Official Codex prewarms a connection with generate=false, then omits the
- // field on the business request that continues from the prewarm response.
- // Only normalize false so a meaningful generate=true change remains visible.
- if generate, ok := decoded["generate"].(bool); ok && !generate {
- delete(decoded, "generate")
- }
- return json.Marshal(decoded)
- }
- // openAIWSExtractNormalizedInputSequence 拆出 input 序列。返回的正文尽可能与
- // payload 共享底层数组(零拷贝),受 replay 所有权不变式保护。
- func openAIWSExtractNormalizedInputSequence(payload []byte) ([]json.RawMessage, bool, error) {
- if len(payload) == 0 {
- return nil, false, nil
- }
- inputValue := gjson.Get(openAIWSPayloadStringView(payload), "input")
- if !inputValue.Exists() {
- return nil, false, nil
- }
- if inputValue.Type == gjson.JSON {
- if inputValue.IsArray() {
- // gjson 宽容解析;数组整体先做零分配合法性校验,避免把断裂
- // JSON 塞进 replay 历史。
- arrayRaw := openAIWSRawMessageFromResult(payload, inputValue)
- if !json.Valid(arrayRaw) {
- return nil, true, errors.New("input array json is invalid")
- }
- elems := inputValue.Array()
- items := make([]json.RawMessage, 0, len(elems))
- for _, elem := range elems {
- items = append(items, openAIWSRawMessageFromResult(payload, elem))
- }
- return items, true, nil
- }
- return []json.RawMessage{openAIWSRawMessageFromResult(payload, inputValue)}, true, nil
- }
- if inputValue.Type == gjson.String {
- encoded, _ := json.Marshal(inputValue.String())
- return []json.RawMessage{encoded}, true, nil
- }
- return []json.RawMessage{openAIWSRawMessageFromResult(payload, inputValue)}, true, nil
- }
- func openAIWSInputIsPrefixExtended(previousPayload, currentPayload []byte) (bool, error) {
- previousItems, previousExists, prevErr := openAIWSExtractNormalizedInputSequence(previousPayload)
- if prevErr != nil {
- return false, prevErr
- }
- currentItems, currentExists, currentErr := openAIWSExtractNormalizedInputSequence(currentPayload)
- if currentErr != nil {
- return false, currentErr
- }
- if !previousExists && !currentExists {
- return true, nil
- }
- if !previousExists {
- return len(currentItems) == 0, nil
- }
- if !currentExists {
- return len(previousItems) == 0, nil
- }
- if len(currentItems) < len(previousItems) {
- return false, nil
- }
- for idx := range previousItems {
- previousNormalized := normalizeOpenAIWSJSONForCompareOrRaw(previousItems[idx])
- currentNormalized := normalizeOpenAIWSJSONForCompareOrRaw(currentItems[idx])
- if !bytes.Equal(previousNormalized, currentNormalized) {
- return false, nil
- }
- }
- return true, nil
- }
- func openAIWSRawItemsHasPrefix(items []json.RawMessage, prefix []json.RawMessage) bool {
- if len(prefix) == 0 {
- return true
- }
- if len(items) < len(prefix) {
- return false
- }
- for idx := range prefix {
- // 快路径:客户端逐字节重发历史时直接比较,避免整轮历史的解码/再编码。
- if bytes.Equal(bytes.TrimSpace(prefix[idx]), bytes.TrimSpace(items[idx])) {
- continue
- }
- previousNormalized := normalizeOpenAIWSJSONForCompareOrRaw(prefix[idx])
- currentNormalized := normalizeOpenAIWSJSONForCompareOrRaw(items[idx])
- if !bytes.Equal(previousNormalized, currentNormalized) {
- return false
- }
- }
- return true
- }
- func openAIWSRawItemsHasFunctionCallOutput(items []json.RawMessage) bool {
- for _, item := range items {
- if isCodexToolCallOutputItemType(gjson.GetBytes(item, "type").String()) {
- return true
- }
- }
- return false
- }
- func openAIWSRawItemsHaveToolCallContextForOutputs(items []json.RawMessage) bool {
- if len(items) == 0 {
- return false
- }
- contextCallIDs := make(map[string]struct{})
- outputCallIDs := make(map[string]struct{})
- for _, item := range items {
- itemType := gjson.GetBytes(item, "type").String()
- callID := strings.TrimSpace(gjson.GetBytes(item, "call_id").String())
- switch {
- case isCodexToolCallContextItemType(itemType):
- if callID != "" {
- contextCallIDs[callID] = struct{}{}
- }
- case isCodexToolCallOutputItemType(itemType):
- if callID == "" {
- return false
- }
- outputCallIDs[callID] = struct{}{}
- }
- }
- if len(outputCallIDs) == 0 || len(contextCallIDs) == 0 {
- return false
- }
- for callID := range outputCallIDs {
- if _, ok := contextCallIDs[callID]; !ok {
- return false
- }
- }
- return true
- }
- // sanitizeOpenAIWSHistoricalReplayToolCalls 返回的新头数组与 previousItems 共享正文。
- func sanitizeOpenAIWSHistoricalReplayToolCalls(
- previousItems []json.RawMessage,
- currentItems []json.RawMessage,
- ) []json.RawMessage {
- if len(previousItems) == 0 {
- return previousItems
- }
- outputCallIDs := make(map[string]struct{})
- collectOutputCallIDs := func(items []json.RawMessage) {
- for _, item := range items {
- if !isCodexToolCallOutputItemType(gjson.GetBytes(item, "type").String()) {
- continue
- }
- if callID := strings.TrimSpace(gjson.GetBytes(item, "call_id").String()); callID != "" {
- outputCallIDs[callID] = struct{}{}
- }
- }
- }
- collectOutputCallIDs(previousItems)
- collectOutputCallIDs(currentItems)
- sanitized := make([]json.RawMessage, 0, len(previousItems))
- for _, item := range previousItems {
- if isCodexToolCallContextItemType(gjson.GetBytes(item, "type").String()) {
- callID := strings.TrimSpace(gjson.GetBytes(item, "call_id").String())
- if _, paired := outputCallIDs[callID]; !paired {
- continue
- }
- }
- sanitized = append(sanitized, item)
- }
- return sanitized
- }
- func openAIWSRawPayloadHasToolCallOutput(payload []byte) bool {
- if len(payload) == 0 {
- return false
- }
- input := gjson.Get(openAIWSPayloadStringView(payload), "input")
- if !input.Exists() {
- return false
- }
- if input.IsArray() {
- for _, item := range input.Array() {
- if isCodexToolCallOutputItemType(item.Get("type").String()) {
- return true
- }
- }
- return false
- }
- if input.Type == gjson.JSON {
- return isCodexToolCallOutputItemType(input.Get("type").String())
- }
- return false
- }
- // buildOpenAIWSReplayInputSequenceFromItems 基于已解析的当前 turn input 构建
- // replay 序列。返回序列的正文与 previousFullInput/currentItems 共享所有权
- // (见 combineOpenAIWSReplayItems 上方的所有权不变式),头数组可能直接转移自
- // currentItems。
- func buildOpenAIWSReplayInputSequenceFromItems(
- previousFullInput []json.RawMessage,
- previousFullInputExists bool,
- currentItems []json.RawMessage,
- currentExists bool,
- hasPreviousResponseID bool,
- ) ([]json.RawMessage, bool) {
- if !hasPreviousResponseID || !previousFullInputExists {
- return currentItems, currentExists
- }
- previousFullInput = sanitizeOpenAIWSHistoricalReplayToolCalls(previousFullInput, currentItems)
- if !currentExists || len(currentItems) == 0 {
- return previousFullInput, true
- }
- if openAIWSRawItemsHasPrefix(currentItems, previousFullInput) {
- return currentItems, true
- }
- merged := make([]json.RawMessage, 0, len(previousFullInput)+len(currentItems))
- merged = append(merged, previousFullInput...)
- merged = append(merged, currentItems...)
- return merged, true
- }
- func buildOpenAIWSReplayInputSequence(
- previousFullInput []json.RawMessage,
- previousFullInputExists bool,
- currentPayload []byte,
- hasPreviousResponseID bool,
- ) ([]json.RawMessage, bool, error) {
- currentItems, currentExists, currentErr := openAIWSExtractNormalizedInputSequence(currentPayload)
- if currentErr != nil {
- return nil, false, currentErr
- }
- items, exists := buildOpenAIWSReplayInputSequenceFromItems(
- previousFullInput,
- previousFullInputExists,
- currentItems,
- currentExists,
- hasPreviousResponseID,
- )
- return items, exists, nil
- }
- func setOpenAIWSPayloadInputSequence(
- payload []byte,
- fullInput []json.RawMessage,
- fullInputExists bool,
- ) ([]byte, error) {
- if !fullInputExists {
- return payload, nil
- }
- // Preserve [] vs null semantics when input exists but is empty.
- inputForMarshal := fullInput
- if inputForMarshal == nil {
- inputForMarshal = []json.RawMessage{}
- }
- inputRaw, marshalErr := json.Marshal(inputForMarshal)
- if marshalErr != nil {
- return nil, marshalErr
- }
- return sjson.SetRawBytes(payload, "input", inputRaw)
- }
- func buildOpenAIWSCurrentTurnRetryPayload(
- payload []byte,
- fullInput []json.RawMessage,
- fullInputExists bool,
- originalModel string,
- ) ([]byte, bool, error) {
- if !fullInputExists {
- return nil, false, nil
- }
- retryPayload, err := setOpenAIWSPayloadInputSequence(payload, fullInput, true)
- if err != nil {
- return nil, false, err
- }
- retryPayload = RemovePreviousResponseIDFromBody(retryPayload)
- if model := strings.TrimSpace(originalModel); model != "" {
- retryPayload, err = sjson.SetBytes(retryPayload, "model", model)
- if err != nil {
- return nil, false, err
- }
- }
- coverage := AnalyzeToolCallOutputContextCoverageBytes(retryPayload)
- if coverage.HasFunctionCallOutput && !coverage.ContextCoversAllCallIDs {
- return nil, false, nil
- }
- return retryPayload, true, nil
- }
- func shouldKeepIngressPreviousResponseID(
- previousPayload []byte,
- currentPayload []byte,
- lastTurnResponseID string,
- hasFunctionCallOutput bool,
- ) (bool, string, error) {
- if hasFunctionCallOutput {
- return true, "has_function_call_output", nil
- }
- currentPreviousResponseID := strings.TrimSpace(openAIWSPayloadStringFromRaw(currentPayload, "previous_response_id"))
- if currentPreviousResponseID == "" {
- return false, "missing_previous_response_id", nil
- }
- expectedPreviousResponseID := strings.TrimSpace(lastTurnResponseID)
- if expectedPreviousResponseID == "" {
- return false, "missing_last_turn_response_id", nil
- }
- if currentPreviousResponseID != expectedPreviousResponseID {
- return false, "previous_response_id_mismatch", nil
- }
- if len(previousPayload) == 0 {
- return false, "missing_previous_turn_payload", nil
- }
- previousComparable, previousComparableErr := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(previousPayload)
- if previousComparableErr != nil {
- return false, "non_input_compare_error", previousComparableErr
- }
- currentComparable, currentComparableErr := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(currentPayload)
- if currentComparableErr != nil {
- return false, "non_input_compare_error", currentComparableErr
- }
- if !bytes.Equal(previousComparable, currentComparable) {
- return false, "non_input_changed", nil
- }
- return true, "strict_incremental_ok", nil
- }
- type openAIWSIngressPreviousTurnStrictState struct {
- nonInputComparable []byte
- }
- func buildOpenAIWSIngressPreviousTurnStrictState(payload []byte) (*openAIWSIngressPreviousTurnStrictState, error) {
- if len(payload) == 0 {
- return nil, nil
- }
- nonInputComparable, nonInputErr := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(payload)
- if nonInputErr != nil {
- return nil, nonInputErr
- }
- return &openAIWSIngressPreviousTurnStrictState{
- nonInputComparable: nonInputComparable,
- }, nil
- }
- func shouldKeepIngressPreviousResponseIDWithStrictState(
- previousState *openAIWSIngressPreviousTurnStrictState,
- currentPayload []byte,
- lastTurnResponseID string,
- hasFunctionCallOutput bool,
- ) (bool, string, error) {
- if hasFunctionCallOutput {
- return true, "has_function_call_output", nil
- }
- currentPreviousResponseID := strings.TrimSpace(openAIWSPayloadStringFromRaw(currentPayload, "previous_response_id"))
- if currentPreviousResponseID == "" {
- return false, "missing_previous_response_id", nil
- }
- expectedPreviousResponseID := strings.TrimSpace(lastTurnResponseID)
- if expectedPreviousResponseID == "" {
- return false, "missing_last_turn_response_id", nil
- }
- if currentPreviousResponseID != expectedPreviousResponseID {
- return false, "previous_response_id_mismatch", nil
- }
- if previousState == nil {
- return false, "missing_previous_turn_payload", nil
- }
- currentComparable, currentComparableErr := normalizeOpenAIWSPayloadWithoutInputAndPreviousResponseID(currentPayload)
- if currentComparableErr != nil {
- return false, "non_input_compare_error", currentComparableErr
- }
- if !bytes.Equal(previousState.nonInputComparable, currentComparable) {
- return false, "non_input_changed", nil
- }
- return true, "strict_incremental_ok", nil
- }
|