Repository navigation
Codex: service_tier is still dropped on /v1/chat/completions (Fast mode unusable for chat clients) #6138
Description
Goal
Keep a string service_tier from OpenAI Chat Completions requests when they are translated to Codex, so /v1/chat/completions clients can use Codex Fast (priority) mode the same way /v1/responses, Claude, Gemini and Interactions clients already can.
Problem
ConvertOpenAIRequestToCodex (internal/translator/codex/openai/chat-completions/codex_openai_request.go) builds a new Codex body and never copies service_tier, so the field is silently dropped. This was reported in #2286, which was closed as completed, but git log -S service_tier on this file shows the field was never added and current main still drops it.
This matters more now that #6090 is merged. applyCodexRoutingHint builds X-Codex-Routing-Hint: model=<slug>;tier=<tier> from the final upstream body, so Chat Completions requests never get the tier in the body or in the hint.
Many OpenAI-compatible clients only talk to /v1/chat/completions. For example, opencode's @ai-sdk/openai-compatible provider sends a service_tier: "priority" variant through that endpoint.
About the concern raised in #2285
In #2285 the concern was that /v1/chat/completions is not Codex-only and forwarding the field could break other models. This change is limited to the chat-completions → codex translator, which only runs for requests routed to a Codex executor. Other providers' translators are unchanged. Only string values are copied, and the Codex executor path downstream already handles service_tier for other source formats.
Proposed implementation
// internal/translator/codex/openai/chat-completions/codex_openai_request.go
// in ConvertOpenAIRequestToCodex, right after reasoning.effort is set:
if tier := root.Get("service_tier"); tier.Type == gjson.String {
out, _ = sjson.SetBytes(out, "service_tier", tier.String())
}Test:
func TestConvertOpenAIRequestToCodexPreservesPriorityServiceTier(t *testing.T) {
tests := []struct {
name string
body string
want string
}{
{name: "fast variant", body: `{"model":"gpt-6-sol","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high","service_tier":"priority"}`, want: "priority"},
{name: "normal variant", body: `{"model":"gpt-6-sol","messages":[{"role":"user","content":"hi"}]}`},
{name: "non-string tier", body: `{"model":"gpt-6-sol","messages":[{"role":"user","content":"hi"}],"service_tier":1}`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := ConvertOpenAIRequestToCodex("gpt-6-sol", []byte(tt.body), true)
if got := gjson.GetBytes(out, "service_tier").String(); got != tt.want {
t.Fatalf("service_tier = %q, want %q; payload=%s", got, tt.want, out)
}
if tt.want != "" && gjson.GetBytes(out, "reasoning.effort").String() != "high" {
t.Fatalf("reasoning.effort lost when selecting Fast: %s", out)
}
})
}
}Evidence
Tested on current main (including #6090) plus the change above, with a ChatGPT OAuth Codex credential and the HTTP/SSE upstream transport.
Upstream request (request log) for a /v1/chat/completions call with "service_tier":"priority":
Upstream URL: https://chatgpt.com/backend-api/codex/responses
X-Codex-Routing-Hint: model=gpt-6-sol;tier=priority
{"instructions":"","stream":true,"reasoning":{"effort":"low",...},"service_tier":"priority",...,"model":"gpt-6-sol",...}
Without the change, neither the body field nor ;tier=priority in the hint is sent.
Throughput. gpt-6-sol, reasoning_effort: low, same prompt (~2,400 output tokens), 8 streamed runs through /v1/chat/completions, alternating normal and priority. Tokens/s is measured from the first content token to the end of the stream.
| Mode | Tokens/s (median) | Range | TTFT (median) | Total time |
|---|---|---|---|---|
| Normal | 38.6 | 36.0 to 39.6 | 4.15 s | 65 to 74 s |
service_tier: "priority" |
88.2 | 86.6 to 89.0 | 2.79 s | 30 to 32 s |
Priority was about 2.3× faster, and the ranges do not overlap.
Note: the upstream response.completed event reported service_tier: "default" in every run, both normal and priority, and also on /v1/responses. The echoed field does not reflect whether priority was applied, so throughput is the reliable signal. This may explain the confusion in #4587 and #4586.
Activity
feat(codex): support service tier in OpenAI request translation
feat(codex): support service tier in OpenAI request translation
feat(codex): support service tier in OpenAI request translation