diff --git a/dto/openai_request.go b/dto/openai_request.go index fd0bed0e..114f0481 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -874,6 +874,9 @@ type OpenAIResponsesRequest struct { User json.RawMessage `json:"user,omitempty"` MaxToolCalls *uint `json:"max_tool_calls,omitempty"` Prompt json.RawMessage `json:"prompt,omitempty"` + // Codex Responses metadata/client_metadata: + // https://github.com/openai/codex/commit/14df0e8833aad0d6d78287954b61ffac67af936c + ClientMetadata json.RawMessage `json:"client_metadata,omitempty"` // qwen EnableThinking json.RawMessage `json:"enable_thinking,omitempty"` // perplexity @@ -958,8 +961,10 @@ func (r *OpenAIResponsesRequest) GetToolsMap() []map[string]any { } type Reasoning struct { - Effort string `json:"effort,omitempty"` - Summary string `json:"summary,omitempty"` + Effort string `json:"effort,omitempty"` + Summary string `json:"summary,omitempty"` + Mode json.RawMessage `json:"mode,omitempty"` + Context json.RawMessage `json:"context,omitempty"` } type Input struct { diff --git a/dto/openai_responses_compaction_request.go b/dto/openai_responses_compaction_request.go index 7ea584ca..0988908f 100644 --- a/dto/openai_responses_compaction_request.go +++ b/dto/openai_responses_compaction_request.go @@ -14,6 +14,15 @@ type OpenAIResponsesCompactionRequest struct { Input json.RawMessage `json:"input,omitempty"` Instructions json.RawMessage `json:"instructions,omitempty"` PreviousResponseID string `json:"previous_response_id,omitempty"` + // Codex compact request parity: + // https://github.com/openai/codex/commit/53d59722268dde82fb93c1f37964ce196c2a86d7 + // https://github.com/openai/codex/commit/5d6f23a27bf9c90709af527a7108c1c2eadf5123 + Tools json.RawMessage `json:"tools,omitempty"` + ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"` + Reasoning *Reasoning `json:"reasoning,omitempty"` + ServiceTier string `json:"service_tier,omitempty"` + PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` + Text json.RawMessage `json:"text,omitempty"` } func (r *OpenAIResponsesCompactionRequest) GetTokenCountMeta() *types.TokenCountMeta { diff --git a/relay/channel/codex/constants.go b/relay/channel/codex/constants.go index 5233393e..61a37310 100644 --- a/relay/channel/codex/constants.go +++ b/relay/channel/codex/constants.go @@ -9,7 +9,7 @@ var baseModelList = []string{ "gpt-5", "gpt-5-codex", "gpt-5-codex-mini", "gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", - "gpt-5.4", + "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", } var ModelList = withCompactModelSuffix(baseModelList) diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index 4d200ddd..f88bef52 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -14,6 +14,7 @@ import ( "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/bytedance/gopkg/util/gopool" @@ -45,6 +46,24 @@ func NewStreamScanner(reader io.Reader) *bufio.Scanner { return scanner } +func copyCodexSSEHeaders(c *gin.Context, resp *http.Response) { + if c == nil || c.Writer == nil || resp == nil { + return + } + // codex + for _, name := range []string{"X-Reasoning-Included", "X-Codex-Turn-State"} { + values := resp.Header.Values(name) + if !service.ShouldCopyUpstreamHeader(c, name, values) { + continue + } + for _, value := range values { + if value != "" { + c.Writer.Header().Add(name, value) + } + } + } +} + // ExtendWriteDeadline pushes the connection write deadline forward before each // stream write. Best-effort: writers that don't support deadlines (e.g. // httptest recorders) are silently ignored. @@ -122,6 +141,7 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon defer cleanup() scanner.Split(bufio.ScanLines) + copyCodexSSEHeaders(c, resp) SetEventStreamHeaders(c) ctx = context.WithValue(ctx, "stop_chan", stopChan) diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 010c38bb..7679a70d 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -45,6 +45,8 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * Input: req.Input, Instructions: req.Instructions, PreviousResponseID: req.PreviousResponseID, + ParallelToolCalls: req.ParallelToolCalls, + ServiceTier: req.ServiceTier, } default: return types.NewErrorWithStatusCode( diff --git a/setting/operation_setting/channel_affinity_setting.go b/setting/operation_setting/channel_affinity_setting.go index a61d2546..a925a847 100644 --- a/setting/operation_setting/channel_affinity_setting.go +++ b/setting/operation_setting/channel_affinity_setting.go @@ -36,12 +36,33 @@ type ChannelAffinitySetting struct { Rules []ChannelAffinityRule `json:"rules"` } +// Keep Codex CLI passthrough aligned with upstream. Codex uses lower-case +// header names, while HTTP matching here is case-insensitive. +// Request session/thread headers: +// https://github.com/openai/codex/commit/7c7b4861d88960f7e3bd5b7f30f8351be666dd84 +// Responses metadata headers/client_metadata: +// https://github.com/openai/codex/commit/14df0e8833aad0d6d78287954b61ffac67af936c +// x-codex-turn-state response/request round trip: +// https://github.com/openai/codex/commit/ebdd8795e924a8149b616e46ca2ed7848c207a4b var codexCliPassThroughHeaders = []string{ "Originator", "Session_id", + "Thread_id", + "Session-Id", + "Thread-Id", + "X-Client-Request-Id", "User-Agent", "X-Codex-Beta-Features", + "X-Codex-Turn-State", "X-Codex-Turn-Metadata", + "X-Codex-Window-Id", + "X-Codex-Parent-Thread-Id", + //"X-Codex-Installation-Id", + "X-OpenAI-Subagent", + "X-OpenAI-Memgen-Request", + //"X-OAI-Attestation", + "X-ResponsesAPI-Include-Timing-Metrics", + "X-OpenAI-Internal-Codex-Responses-Lite", } var claudeCliPassThroughHeaders = []string{ @@ -74,6 +95,20 @@ func buildPassHeaderTemplate(headers []string) map[string]interface{} { } } +func buildCodexPassHeaderTemplate() map[string]interface{} { + requestHeaders := make([]string, 0, len(codexCliPassThroughHeaders)) + requestHeaders = append(requestHeaders, codexCliPassThroughHeaders...) + return map[string]interface{}{ + "operations": []map[string]interface{}{ + { + "mode": "pass_headers", + "value": requestHeaders, + "keep_origin": true, + }, + }, + } +} + var channelAffinitySetting = ChannelAffinitySetting{ Enabled: true, SwitchOnSuccess: true, @@ -90,7 +125,7 @@ var channelAffinitySetting = ChannelAffinitySetting{ }, ValueRegex: "", TTLSeconds: 0, - ParamOverrideTemplate: buildPassHeaderTemplate(codexCliPassThroughHeaders), + ParamOverrideTemplate: buildCodexPassHeaderTemplate(), SkipRetryOnFailure: true, IncludeUsingGroup: true, IncludeRuleName: true, diff --git a/web/classic/src/constants/channel-affinity-template.constants.js b/web/classic/src/constants/channel-affinity-template.constants.js index f3e88c26..39c4e330 100644 --- a/web/classic/src/constants/channel-affinity-template.constants.js +++ b/web/classic/src/constants/channel-affinity-template.constants.js @@ -27,12 +27,39 @@ const buildPassHeadersTemplate = (headers) => ({ ], }); +const buildCodexPassHeadersTemplate = () => ({ + operations: [ + { + mode: 'pass_headers', + value: [...CODEX_CLI_HEADER_PASSTHROUGH_HEADERS], + keep_origin: true, + }, + ], +}); + +// Keep in sync with upstream Codex request headers: +// https://github.com/openai/codex/commit/7c7b4861d88960f7e3bd5b7f30f8351be666dd84 +// https://github.com/openai/codex/commit/14df0e8833aad0d6d78287954b61ffac67af936c +// https://github.com/openai/codex/commit/ebdd8795e924a8149b616e46ca2ed7848c207a4b export const CODEX_CLI_HEADER_PASSTHROUGH_HEADERS = [ 'Originator', 'Session_id', + 'Thread_id', + 'Session-Id', + 'Thread-Id', + 'X-Client-Request-Id', 'User-Agent', 'X-Codex-Beta-Features', + 'X-Codex-Turn-State', 'X-Codex-Turn-Metadata', + 'X-Codex-Window-Id', + 'X-Codex-Parent-Thread-Id', + // 'X-Codex-Installation-Id', + 'X-OpenAI-Subagent', + 'X-OpenAI-Memgen-Request', + // 'X-OAI-Attestation', + 'X-ResponsesAPI-Include-Timing-Metrics', + 'X-OpenAI-Internal-Codex-Responses-Lite', ]; export const CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS = [ @@ -51,9 +78,8 @@ export const CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS = [ 'Anthropic-Version', ]; -export const CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate( - CODEX_CLI_HEADER_PASSTHROUGH_HEADERS, -); +export const CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE = + buildCodexPassHeadersTemplate(); export const CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate( CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS, @@ -65,7 +91,7 @@ export const CHANNEL_AFFINITY_RULE_TEMPLATES = { model_regex: ['^gpt-.*$'], path_regex: ['/v1/responses'], key_sources: [{ type: 'gjson', path: 'prompt_cache_key' }], - param_override_template: CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE, + param_override_template: buildCodexPassHeadersTemplate(), value_regex: '', ttl_seconds: 0, skip_retry_on_failure: true, diff --git a/web/default/src/features/channels/components/dialogs/param-override-editor-dialog.tsx b/web/default/src/features/channels/components/dialogs/param-override-editor-dialog.tsx index 7f85e260..bdcfe08f 100644 --- a/web/default/src/features/channels/components/dialogs/param-override-editor-dialog.tsx +++ b/web/default/src/features/channels/components/dialogs/param-override-editor-dialog.tsx @@ -291,12 +291,29 @@ const GEMINI_IMAGE_4K_TEMPLATE = { ], } +// Keep in sync with upstream Codex request headers: +// https://github.com/openai/codex/commit/7c7b4861d88960f7e3bd5b7f30f8351be666dd84 +// https://github.com/openai/codex/commit/14df0e8833aad0d6d78287954b61ffac67af936c +// https://github.com/openai/codex/commit/ebdd8795e924a8149b616e46ca2ed7848c207a4b const CODEX_CLI_HEADER_PASSTHROUGH_HEADERS = [ 'Originator', 'Session_id', + 'Thread_id', + 'Session-Id', + 'Thread-Id', + 'X-Client-Request-Id', 'User-Agent', 'X-Codex-Beta-Features', + 'X-Codex-Turn-State', 'X-Codex-Turn-Metadata', + 'X-Codex-Window-Id', + 'X-Codex-Parent-Thread-Id', + // 'X-Codex-Installation-Id', + 'X-OpenAI-Subagent', + 'X-OpenAI-Memgen-Request', + // 'X-OAI-Attestation', + 'X-ResponsesAPI-Include-Timing-Metrics', + 'X-OpenAI-Internal-Codex-Responses-Lite', ] const CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS = [ @@ -321,9 +338,15 @@ const buildPassHeadersTemplate = (headers: string[]) => ({ ], }) -const CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate( - CODEX_CLI_HEADER_PASSTHROUGH_HEADERS -) +const CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE = { + operations: [ + { + mode: 'pass_headers', + value: [...CODEX_CLI_HEADER_PASSTHROUGH_HEADERS], + keep_origin: true, + }, + ], +} const CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE = buildPassHeadersTemplate( CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS ) @@ -941,6 +964,7 @@ const validateOperations = ( if (headers.length === 0) return t('Rule {{line}} pass_headers format is invalid', { line }) } + } return '' } diff --git a/web/default/src/features/system-settings/general/channel-affinity/constants.ts b/web/default/src/features/system-settings/general/channel-affinity/constants.ts index 041ab7eb..abe3e77b 100644 --- a/web/default/src/features/system-settings/general/channel-affinity/constants.ts +++ b/web/default/src/features/system-settings/general/channel-affinity/constants.ts @@ -18,12 +18,29 @@ For commercial licensing, please contact support@quantumnous.com */ import type { AffinityRule } from './types' +// Keep in sync with upstream Codex request headers: +// https://github.com/openai/codex/commit/7c7b4861d88960f7e3bd5b7f30f8351be666dd84 +// https://github.com/openai/codex/commit/14df0e8833aad0d6d78287954b61ffac67af936c +// https://github.com/openai/codex/commit/ebdd8795e924a8149b616e46ca2ed7848c207a4b const CODEX_CLI_HEADER_PASSTHROUGH_HEADERS = [ 'Originator', 'Session_id', + 'Thread_id', + 'Session-Id', + 'Thread-Id', + 'X-Client-Request-Id', 'User-Agent', 'X-Codex-Beta-Features', + 'X-Codex-Turn-State', 'X-Codex-Turn-Metadata', + 'X-Codex-Window-Id', + 'X-Codex-Parent-Thread-Id', + // 'X-Codex-Installation-Id', + 'X-OpenAI-Subagent', + 'X-OpenAI-Memgen-Request', + // 'X-OAI-Attestation', + 'X-ResponsesAPI-Include-Timing-Metrics', + 'X-OpenAI-Internal-Codex-Responses-Lite', ] const CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS = [ @@ -54,6 +71,18 @@ function buildPassHeadersTemplate(headers: string[]) { } } +function buildCodexPassHeadersTemplate() { + return { + operations: [ + { + mode: 'pass_headers', + value: [...CODEX_CLI_HEADER_PASSTHROUGH_HEADERS], + keep_origin: true, + }, + ], + } +} + export type RuleTemplate = Omit export const RULE_TEMPLATES: Record = { @@ -62,9 +91,7 @@ export const RULE_TEMPLATES: Record = { model_regex: ['^gpt-.*$'], path_regex: ['/v1/responses'], key_sources: [{ type: 'gjson', path: 'prompt_cache_key' }], - param_override_template: buildPassHeadersTemplate( - CODEX_CLI_HEADER_PASSTHROUGH_HEADERS - ), + param_override_template: buildCodexPassHeadersTemplate(), value_regex: '', ttl_seconds: 0, skip_retry_on_failure: true,