diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index d9f889e2..8eded6fa 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -327,6 +327,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } template, _ = sjson.SetBytes(template, "reasoning.effort", reasoningEffort) template, _ = sjson.SetBytes(template, "reasoning.summary", "auto") + if serviceTier := normalizeCodexServiceTier(rootResult.Get("service_tier")); serviceTier != "" { + template, _ = sjson.SetBytes(template, "service_tier", serviceTier) + } template, _ = sjson.SetBytes(template, "stream", true) template, _ = sjson.SetBytes(template, "store", false) template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"}) @@ -334,6 +337,19 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) return template } +func normalizeCodexServiceTier(result gjson.Result) string { + if !result.Exists() || result.Type != gjson.String { + return "" + } + + switch strings.ToLower(strings.TrimSpace(result.String())) { + case "fast", "priority": + return "priority" + default: + return "" + } +} + // shortenCodexCallIDIfNeeded keeps Claude tool IDs within the OpenAI Responses // API call_id limit while preserving a stable, low-collision mapping. func shortenCodexCallIDIfNeeded(id string) string { diff --git a/internal/translator/codex/claude/codex_claude_request_test.go b/internal/translator/codex/claude/codex_claude_request_test.go index eab12e47..abf893e4 100644 --- a/internal/translator/codex/claude/codex_claude_request_test.go +++ b/internal/translator/codex/claude/codex_claude_request_test.go @@ -148,6 +148,58 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) { } } +func TestConvertClaudeRequestToCodex_ServiceTier(t *testing.T) { + tests := []struct { + name string + serviceTierJSON string + want string + wantExists bool + }{ + { + name: "Priority passes through", + serviceTierJSON: `"priority"`, + want: "priority", + wantExists: true, + }, + { + name: "Fast normalizes to priority", + serviceTierJSON: `"fast"`, + want: "priority", + wantExists: true, + }, + { + name: "Unsupported tier is omitted", + serviceTierJSON: `"default"`, + }, + { + name: "Non-string tier is omitted", + serviceTierJSON: `true`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inputJSON := `{ + "model": "gpt-5.4", + "service_tier": ` + tt.serviceTierJSON + `, + "messages": [{"role": "user", "content": "Reply with OK"}] + }` + + result := ConvertClaudeRequestToCodex("gpt-5.4", []byte(inputJSON), false) + serviceTierResult := gjson.GetBytes(result, "service_tier") + if serviceTierResult.Exists() != tt.wantExists { + t.Fatalf("service_tier exists = %v, want %v. Output: %s", serviceTierResult.Exists(), tt.wantExists, string(result)) + } + if !tt.wantExists { + return + } + if got := serviceTierResult.String(); got != tt.want { + t.Fatalf("service_tier = %q, want %q. Output: %s", got, tt.want, string(result)) + } + }) + } +} + func TestConvertClaudeRequestToCodex_ShortenLongToolUseIDs(t *testing.T) { longID := "toolu_" + strings.Repeat("a", 62) if len(longID) <= 64 {