Skip to content

Codex: service_tier is still dropped on /v1/chat/completions (Fast mode unusable for chat clients) #6138

Closed

Description

@heliowap

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.

Related: #2285, #2286, #6090, #4587.

Activity

  1. added a commit that references this issue on Sep 26, 2026
    be4b28f
  2. added a commit that references this issue on Sep 28, 2026
    5354eb7
  3. added a commit that references this issue on Oct 17, 2026
    11a852a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions