Skip to content

Commit c550fff

Browse files
samxu01claude
andcommitted
fix(litellm): route codex CLI (cloud-codex) through the /responses path
The codex CLI speaks wire_api="responses" and hits LiteLLM's /v1/responses endpoint, not /chat/completions. On that path the chatgpt provider's get_complete_url already targets /responses, so the `responses/` prefix the chat-path aliases carry is redundant and gets sent literally to OpenAI as `responses/gpt-5.4` -> 400 "model not supported". Cody (cloud-codex) was pointed at the prefixed `gpt-5.4` alias and silently fell back to Nemotron. - Add `codex-cli/gpt-5.4` -> `chatgpt/gpt-5.4` (no prefix) for the /responses path - Point Cody's config.toml at the new alias - Re-path the responses-input patch from the nonexistent /usr/lib/... to litellm.__file__ (/app/litellm) so it actually applies — OpenAI's /responses rejects a bare-string input ("Input must be a list"); the patch wraps it Verified end-to-end via litellm.aresponses: real codex (served=gpt-5.4 / gpt-5.4-mini-2026-03-17). Single LiteLLM auth surface for the CLI, as intended. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 20647ab commit c550fff

3 files changed

Lines changed: 37 additions & 7 deletions

File tree

k8s/helm/commonly/templates/agents/cloud-codex-deployment.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,10 @@ spec:
193193
# Runtime stays codex: codex CLI still spawns, still sandboxes,
194194
# still owns tool use and sessions. Only the HTTPS layer is proxied.
195195
cat > /state/.codex/config.toml <<EOF
196-
model = "gpt-5.4"
196+
# wire_api=responses below -> LiteLLM /v1/responses. Must use the unprefixed
197+
# codex-cli/* alias (maps to chatgpt/gpt-5.4); the prefixed chat-path aliases
198+
# 400 with "model not supported" on /responses. See litellm-config.yaml.
199+
model = "codex-cli/gpt-5.4"
197200
model_provider = "litellm"
198201
199202
# Codex CLI's default sandbox uses bubblewrap (bwrap), which

k8s/helm/commonly/templates/agents/litellm-deployment.yaml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,25 @@ spec:
5252
command: ["/bin/sh", "-c"]
5353
args:
5454
- |
55+
# Patch litellm's OpenAI responses-input transform to wrap a bare string
56+
# `input` into a [{"role":"user","content":...}] list. OpenAI's Codex /responses
57+
# backend rejects a raw string ("Input must be a list"); the chatgpt provider
58+
# inherits this openai transform. Path resolved via litellm.__file__ (the server
59+
# imports from /app/litellm — an earlier hardcoded /usr/lib path made this a
60+
# silent no-op). Idempotent; no-ops cleanly if the upstream signature changes.
5561
python3 -c "
56-
f='/usr/lib/python3.13/site-packages/litellm/llms/openai/responses/transformation.py'
62+
import litellm, os
63+
f=os.path.join(os.path.dirname(litellm.__file__),'llms','openai','responses','transformation.py')
5764
s=open(f).read()
5865
old=' # Input is expected to be either str or List, no single BaseModel expected\n return input'
59-
new=' # Input is expected to be either str or List, no single BaseModel expected\n if isinstance(input, str):\n return [{\"role\": \"user\", \"content\": input}]\n return input'
60-
if old in s:
61-
open(f,'w').write(s.replace(old,new))
62-
print('LiteLLM patch applied: string input -> list for chatgpt responses API')
66+
new=' # Input is expected to be either str or List, no single BaseModel expected\n if isinstance(input, str): # commonly: wrap bare str for chatgpt /responses\n return [{\"role\": \"user\", \"content\": input}]\n return input'
67+
if 'commonly: wrap bare str' in s:
68+
print('LiteLLM responses-input patch: already applied')
69+
elif old in s:
70+
open(f,'w').write(s.replace(old,new,1))
71+
print('LiteLLM responses-input patch applied: bare-str input -> list for chatgpt /responses')
6372
else:
64-
print('LiteLLM patch: pattern not found, may already be fixed upstream')
73+
print('LiteLLM responses-input patch: pattern not found (version changed?) - review needed')
6574
"
6675
# Disable LiteLLM_SpendLogToolIndex population. LiteLLM (1.88.0) inserts one
6776
# row per tool-call into this table but provides NO retention, cleanup, or

k8s/helm/commonly/templates/configmaps/litellm-config.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ data:
150150
input_cost_per_token: 0.00000075
151151
output_cost_per_token: 0.0000045
152152
153+
# --- Codex CLI native /responses path (cloud-codex / Cody, wire_api="responses") ---
154+
# Unlike the chat-path aliases above, the codex CLI hits LiteLLM's /v1/responses
155+
# endpoint directly. There the chatgpt provider's get_complete_url already targets
156+
# /responses, so the `responses/` prefix is redundant AND breaks: it gets sent
157+
# literally to OpenAI as `responses/gpt-5.4` -> 400 "model not supported". These
158+
# aliases map to plain `chatgpt/gpt-5.4*` (NO prefix) so /v1/responses sends the
159+
# bare model name. Verified end-to-end via litellm.aresponses: real codex (served=
160+
# gpt-5.4 / gpt-5.4-mini-2026-03-17). Do NOT point chat-path (/chat/completions)
161+
# callers here — plain chatgpt/gpt-5.4* on the chat path hits the Cloudflare-
162+
# challenged /backend-api/codex/chat/completions (see the chat-path note above).
163+
- model_name: codex-cli/gpt-5.4
164+
litellm_params:
165+
model: chatgpt/gpt-5.4
166+
timeout: 120
167+
input_cost_per_token: 0.0000025
168+
output_cost_per_token: 0.000015
169+
153170
# --- OpenAI (optional, for embeddings) ---
154171
- model_name: text-embedding-3-large
155172
litellm_params:
@@ -253,6 +270,7 @@ data:
253270
- {"openai-codex/gpt-5.4-mini": ["openrouter/nvidia/nemotron-3-super-120b-a12b:free"]}
254271
- {"gpt-5.4-nano": ["openrouter/nvidia/nemotron-3-super-120b-a12b:free"]}
255272
- {"openai-codex/gpt-5.4-nano": ["openrouter/nvidia/nemotron-3-super-120b-a12b:free"]}
273+
- {"codex-cli/gpt-5.4": ["openrouter/nvidia/nemotron-3-super-120b-a12b:free"]}
256274
# No fallback for nemotron itself — fail fast rather than chain through
257275
# dead endpoints. The rotator + next-request path will pick a fresh
258276
# Codex account on the subsequent call.

0 commit comments

Comments
 (0)