When integrating modern agentic applications like the Codex macOS app with managed cloud models, you often run into strict API schema validations that are invisible until they blow up at runtime. Recently I encountered a stubborn issue while trying to route Codex requests to Vertex AI’s managed open-source models (like glm-5-maas) using a local LiteLLM proxy.
Here is the story of what went wrong, what didn’t work, and how we finally fixed it by patching the local LiteLLM source code inside our virtual environment.
The Setup
The architecture is straightforward:
Codex macOS App ──▶ LiteLLM Proxy (localhost:4000) ──▶ Vertex AI (glm-5-maas)
LiteLLM acts as a gateway, accepting OpenAI-compatible requests from Codex and translating them for Vertex AI. The LiteLLM config load-balances requests across multiple GCP projects for higher rate limits:
model_list:
- model_name: glm-5
litellm_params:
model: vertex_ai/zai-org/glm-5-maas
vertex_project: project-1
vertex_location: global
- model_name: glm-5
litellm_params:
model: vertex_ai/zai-org/glm-5-maas
vertex_project: project-2
vertex_location: global
router_settings:
model_group_alias:
gemini-2.5-pro: glm-5
litellm_settings:
drop_params: true
The Problem: “Expected type ‘function’”
Codex is a powerful agentic tool. To achieve its capabilities it injects advanced tool types into every chat completion request. Rather than just standard function tools, it sends tools with types like image_generation, custom, computer_use, namespace, and code_interpreter.
When LiteLLM proxies this request to Vertex AI, the API strictly enforces the standard OpenAI schema. Vertex AI expects the type field of every item in the tools array to be "function". When it sees "image_generation" or "namespace", it rejects the request with a 400 Bad Request:
{
"error": {
"code": 400,
"message": "Expected the 'type' field of a(n) 'tools' array element to be 'function'; found 'image_generation'.",
"status": "INVALID_ARGUMENT"
}
}
The frustrating part is that this error appeared for every single request, making Codex completely unusable with our Vertex AI backend.
Attempt 1: LiteLLM drop_params (Failed)
LiteLLM has a feature called drop_params: true in its litellm_settings. The idea is that if a provider doesn’t support a parameter, LiteLLM silently strips it out to prevent errors.
We already had this enabled in our config. Surely it would handle this?
Why it didn’t work: drop_params operates at the parameter name level, not the value level. It checks whether parameter names like tools, temperature, or parallel_tool_calls are in the provider’s supported params list. Because Vertex AI Gemini does support the tools parameter, LiteLLM passes the entire tools array through without inspecting the individual items inside it. The unsupported tool types leaked straight to Vertex AI.
In code terms, drop_params would only strip tools entirely if "tools" were not in the supported params list. Since Vertex AI supports tools, the array passes through completely unfiltered.
Attempt 2: Disabling Codex Plugins and Features (Failed)
My next thought was to stop Codex from sending these tools in the first place. I went into ~/.codex/config.toml and explicitly disabled every feature and plugin that might inject non-standard tools:
[features]
image_generation = false
plugins = false
browser_use = false
computer_use = false
[plugins."computer-use@openai-bundled"]
enabled = false
[plugins."browser@openai-bundled"]
enabled = false
Why it didn’t work: Even with every plugin disabled and image_generation = false set in the features config, the Codex background server continued to inject image_generation and namespace tools into the payloads. The Codex macOS app has its own internal logic for which tools to advertise, and the config flags do not fully suppress all of them.
Attempt 3: Blocklist Approach (Partially Failed)
After understanding that the fix had to be server-side in LiteLLM, I patched the tool transformation code to skip specific tool types:
elif tool.get("type") in ["image_generation", "custom", "computer_use",
"code_interpreter", "file_search", "local_shell"]:
continue
Why it partially failed: It fixed the image_generation error, but immediately a new tool type namespace appeared. Codex injects many different non-standard tool types, and maintaining a blocklist is a game of whack-a-mole.
The Solution: Patching the Local LiteLLM Source Code
The real fix required modifying LiteLLM’s source code directly. Since LiteLLM is installed as a Python package in our virtual environment, we edited the installed source files in .venv/.
Where the bug lives
The issue is in LiteLLM’s Responses API to Chat Completion adapter layer. When Codex sends a request to the /responses endpoint, LiteLLM converts it into a standard /chat/completions request before forwarding to Vertex AI.
The transformation function transform_responses_api_tools_to_chat_completion_tools() processes each tool in the array:
type == "mcp"→ Passes through as an MCP tool.type == "web_search_preview"or"web_search"→ Converts to web search options.type == "function"→ Converts to standard chat completion function tool format.else→ Passes the tool through unchanged ← This is the problem.
That else branch is a catch-all that blindly forwards any unknown tool type to the downstream provider. When the downstream provider is Vertex AI, it chokes on these non-standard types.
The patch
We edited the file directly in our virtual environment:
File: .venv/lib/python3.11/site-packages/litellm/responses/litellm_completion_transformation/transformation.py
Before (the problematic catch-all):
else:
chat_completion_tools.append(
cast(Union[ChatCompletionToolParam, OpenAIMcpServerTool], tool)
)
After (allowlist approach):
else:
# Skip any tool type not natively supported by the chat
# completion API (e.g. image_generation, custom, namespace,
# computer_use, code_interpreter, file_search, local_shell).
# Only function, mcp, and web_search* tools are forwarded.
continue
By replacing the catch-all pass-through with a continue, our local LiteLLM installation now silently drops any non-standard tool type. Only function, mcp, and web_search tools are forwarded to Vertex AI. No more whack-a-mole with new tool types.
Important caveat
This is a modification to the installed package source code inside .venv/. It has two implications:
- It will be overwritten if you run
pip install --upgrade litellmor recreate the virtual environment. You would need to re-apply the patch after upgrading. - It is not upstreamed. This points to a feature that LiteLLM should adopt natively: when
drop_paramsis enabled, unsupported tool types within thetoolsarray should be stripped, not just unsupported top-level parameter names.
Bonus Fix: Model Name Aliases
Codex also sends model names like gpt-5.5 or gpt-5.4-mini depending on what the user selects in the UI. These don’t exist in our LiteLLM config, causing a second class of errors:
Invalid model name passed in model=gpt-5.4-mini.
Call /v1/models to view available models for your key.
The fix is simple. Add router aliases in config.yaml:
router_settings:
model_group_alias:
gemini-2.5-pro: glm-5
gemini-2.5-flash: glm-5
gpt-5.4-mini: glm-5
gpt-5.5: glm-5
Now any model name Codex requests gets mapped to our glm-5 deployment on Vertex AI.
The Full Request Pipeline
After all fixes, the request pipeline looks like this:
Codex (sends gpt-5.5 + image_generation/namespace/custom tools)
│
▼
LiteLLM Proxy (localhost:4000)
├─ Router alias: gpt-5.5 → glm-5
├─ Tool filter: drops image_generation, namespace, custom, etc.
└─ Forwards only function/mcp/web_search tools
│
▼
Vertex AI (glm-5-maas, load-balanced across 4 GCP projects)
└─ Returns response successfully
Key Takeaways
drop_paramshas limits. It drops unsupported parameter names, not unsupported values within supported parameters. Don’t assume it will handle everything.- Client-side config doesn’t always work. Disabling features in Codex’s config didn’t stop it from injecting non-standard tool types. When the client won’t cooperate, fix it at the proxy layer.
- Allowlists beat blocklists. Our first server-side fix used a blocklist of specific tool types. A new type (
namespace) immediately broke it. Inverting to an allowlist (only forward known-good types) is the resilient approach. - Patching installed packages is a valid escape hatch. Sometimes the cleanest fix is editing the source code in
.venv/. Just document it, automate re-application if possible, and consider upstreaming the change.