Python · API · LLM-Bridge

One reasoning_effort, Translated for Three Backends

An LLM-Bridge postmortem: OpenAI gives one unified reasoning_effort field, but the three backends accept depth in completely different ways — an SDK option, a config override, encoded in the model name.

OpenAI’s request format has a standard field, reasoning_effort, taking values like low / medium / high / xhigh to control how deep the model thinks. LLM-Bridge accepts this field outward — but inward, not one of the three backends accepts depth the same way. This post is about translating one field into three completely different landings.

claude: first, press the SDK’s default down

claude goes through the Agent SDK, where depth is the SDK’s effort option, taking low / medium / high / xhigh / max. The translation itself is easy; the hard part is the default.

The SDK’s own default is high. That default is set for “coding agents” — they want to think deeply, reason across turns. But LLM-Bridge is a chat gateway; most requests are a casual one-liner, and high is both slow and credit-hungry (headless claude burns the monthly Agent SDK allotment). So the gateway deliberately presses the default down to medium:

DEFAULT_EFFORT = "medium"  # SDK default is high — too deep, too costly for chat

EFFORT_MAP = {
    "minimal": "low", "low": "low", "medium": "medium",
    "high": "high", "xhigh": "xhigh", "max": "max",
}
effort = EFFORT_MAP.get(request.reasoning_effort or "", DEFAULT_EFFORT)

If the request explicitly sets reasoning_effort, follow it; if not, use medium rather than the SDK’s high. When you wrap a third-party SDK, its default is tuned for its scenario, not necessarily yours — override it when you should, don’t silently inherit.

codex: from request field to a -c config override

codex is a CLI subprocess, where depth isn’t a command-line flag — you pass a config override through -c:

mapped_effort = EFFORT_MAP.get(effort or "")
if mapped_effort:
    args += ["-c", f'model_reasoning_effort="{mapped_effort}"']

-c model_reasoning_effort="high" amounts to temporarily rewriting one of codex’s config entries. Request field → intermediate vocabulary → CLI config override, two hops in between. Without an effort, the parameter isn’t added, letting the CLI use its own default.

agy: depth isn’t in a field, it’s in the model name

At agy (the Antigravity CLI), the reasoning_effort field is ignored outright — not unimplemented, but because its models encode depth into their names:

Gemini 3.5 Flash (Low)
Gemini 3.5 Flash (Medium)
Gemini 3.5 Flash (High)

(Low) / (Medium) / (High) are three different model slugs, not one parameter of one model. Want shallower, pick gemini-3.5-flash-low; deeper, pick -high. The depth knob grows out of model selection, not a separate field. So this channel honestly doesn’t handle reasoning_effort, and the docs say so: control agy’s depth by choosing the model.

The same concept sits in a different place in each vendor’s abstraction: an SDK option for claude, a config entry for codex, part of the model identifier for agy. The adapter layer’s job is recognizing where it lands in each, then wiring the request field over.

Two mapping tables, because the vocabularies genuinely differ

You may have noticed claude and codex each have an EFFORT_MAP, and they don’t match. That’s not a copy-paste someone forgot to merge — the two vendors’ vocabularies are simply different:

  • OpenAI’s minimal: neither claude nor codex has a matching tier, so both map it to low;
  • The top tier: claude has max, so max maps to max; codex only goes up to xhigh, so codex’s table has max → xhigh, capped.
# claude:  "max": "max"      -- SDK supports max
# codex:   "max": "xhigh"    -- codex has no max, cap at xhigh

Landing one unified field on N backends means maintaining, per backend, a “my vocabulary → its vocabulary” map, and honestly handling the tiers that don’t line up. This dull vocabulary alignment is the real workload hiding under the phrase “unified API.”

Aside: don’t let the user’s environment silently rewrite your request

In the same family as effort, there’s another hidden variable: codex reads the user’s ~/.codex/config.toml and drags its skills, plugins, and even reasoning settings into every request. For a chat gateway, that’s two problems — those skills add tens of thousands of input tokens for nothing, and if the user has xhigh reasoning set globally, every request of yours slows down, and the effort you set in code gets silently rewritten by the user’s environment.

So codex adds --ignore-user-config by default:

if self.ignore_user_config:
    args.append("--ignore-user-config")

It only isolates the “user’s personalization,” and auth is unaffected (login is still read from ~/.codex). Now the gateway request is clean: depth is entirely determined by the request’s reasoning_effort, not swayed in secret by a global config the user tweaked some day. It’s configurable — turn it off if you want the user config carried.

Takeaways

One reasoning_effort field, translated for three backends:

  • claude: the SDK’s effort option, the key being to press the SDK’s high default down to medium — a third-party default is tuned for its scenario, not yours;
  • codex: turned into a -c model_reasoning_effort="…" config override, the request field taking two hops to the CLI;
  • agy: depth encoded in the model name ((Low/Medium/High)), the field ignored, controlled by picking the model;
  • Two mapping tables: neither has minimal, both map it to low; max is max for claude, capped to xhigh for codex — vocabulary alignment is the real work of a unified API;
  • --ignore-user-config: isolate the user’s global config, so it can’t sneak 20k tokens onto a request or rewrite the effort you set.

In one line: “unified API” sounds like smoothing away differences; doing it means writing, for every single difference, one honest mapping.

Comments

  • Loading…

Comments are reviewed before publishing; email is visible only to me.