* feat(settings): per-model context_window and max_output_tokens overrides
Adds a `modelLimits` settings.json map so users can declare context window
and max output tokens for OpenAI-compatible models that are not in the
built-in catalog. Resolution order is env var → settings → catalog, so the
existing CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS / _MAX_OUTPUT_TOKENS env vars
keep priority. Keys are matched exactly, then by prefix, with optional
"<host>:<model>" host-qualified forms.
Example:
"modelLimits": {
"qwen3.6-plus": { "contextWindow": 1048576, "maxOutputTokens": 32768 }
}
Closes #478
* fix(model-limits): apply settings overrides in resolveModelRuntimeLimits
The settings.json `modelLimits` fallback was only wired into the scalar
getOpenAIContextWindow / getOpenAIMaxOutputTokens helpers. The runtime
resolution path (resolveModelRuntimeLimits) instead consumes the *Matches
variants, which only returned env-var matches — so a configured modelLimits
override never reached the model whose limits are actually resolved at
runtime, contrary to the documented env → settings → catalog order.
Fold the settings lookup into getOpenAIContextWindowMatches /
getOpenAIMaxOutputTokenMatches as a dedicated `settings` field, and insert it
between the exact env override and the built-in catalog in
resolveModelRuntimeLimits. Declare `modelLimits` on GlobalConfig so the lookup
is typed. Add an integration test that drives resolveModelRuntimeLimits and
asserts settings resolve for exact, host-qualified and prefix keys, and that an
env override still wins.
* fix(model-limits): resolve modelLimits from settings.json, not global config
readSettingsLimits read getGlobalConfig().modelLimits, which is ~/.openclaude.json
— a different file from the settings.json the feature is documented and schema'd
for (SettingsSchema in settings/types.ts). A user adding modelLimits to
settings.json saw no effect. Read it from getInitialSettings() (the merged
settings snapshot, session-cached) instead, and drop the now-unused modelLimits
field from GlobalConfig so the override lives in one place. Rewire both test
suites to a gated-passthrough getInitialSettings mock.
* test(model-limits): capture real settings module at load, not in beforeEach
The settings modelLimits suites re-imported the real settings module with
a cold dynamic import inside beforeEach. That import sat right at Bun's
default 5s hook timeout and intermittently failed the first test. Capture
the genuine module once via a top-level query-string-busted static import
so the cost moves to module load (no hook timeout) and the gated
passthrough still bypasses other suites' settings.js mocks. Also correct
the integration-test comment to name the helpers the runtime path
actually calls (getOpenAIContextWindowMatches / getOpenAIMaxOutputTokenMatches).
* fix(model-limits): keep env-prefix override above settings in runtime resolver
resolveModelRuntimeLimits ordered the settings `modelLimits` value above
the env-prefix match, so a broad env-prefix override (e.g.
`{"my-custom":N}`) was silently overtaken by a more specific settings
entry (`my-custom-deployment`). The scalar getOpenAIContextWindow treats
env (exact ?? prefix) as strictly higher priority than settings; mirror
that in the runtime resolver: env.exact, then env.prefix, then settings,
then catalog/cache/descriptor. Regression covers both contextWindow and
maxOutputTokens.
* docs(model-limits): document the settings.json modelLimits override
Add a user-facing section near the env-var overrides covering the
modelLimits map, JSON shape, exact/prefix/host-qualified key matching, and
the env > settings > catalog > descriptor precedence.
* fix(model-limits): keep catalog above env-prefix in runtime precedence
The previous reorder put the env-prefix match above the catalog value,
which broke the existing invariant that a `:cloud` catalog variant takes
its known catalog limit rather than inheriting a broad base-model env
prefix (deepseek-v4-pro:cloud regressed to 262144). Correct order:
exact env -> catalog/cache -> env prefix -> settings -> descriptor. This
still keeps settings strictly below env-prefix (the original drift fix)
while preserving catalog precedence over prefix. Docs precedence updated
to match.
* docs(advanced-setup): document CLAUDE_CODE_OPENAI_MAX_OUTPUT_TOKENS env var
The modelLimits section referenced the max-output env var but the
Environment Variables table only listed the context-window one, leaving
output-only configuration undocumented. Add the matching table row.
Refs #478
* docs(model): align openaiContextWindows precedence comments with the resolver
The module header and the OpenALimitOverrideMatches.settings comment claimed a
resolution order of "env → settings → catalog", which contradicts
resolveModelRuntimeLimits (exact env → catalog/discovery cache → prefix env →
settings modelLimits → descriptor default). Since this module only produces the
override candidates and does not own the precedence, narrow the comments to say
so and point at runtimeMetadata.ts as the authoritative chain, so this
precedence-sensitive code isn't misread when touched again.
* fix(model): rank host-qualified modelLimits keys above bare model keys
lookupByModel grouped all exact matches (host-qualified and bare) ahead of all
prefix matches, so a bare exact key like `qwen3.6-plus` beat a host-qualified
prefix like `openrouter.ai:qwen3`. That defeated the advertised per-endpoint
disambiguation for versioned model families. A host-qualified key is strictly
more specific than a bare one, so rank both host-qualified forms (exact and
prefix) in the high-priority tier ahead of the bare exact match, leaving only
the bare prefix in the low-priority tier.
* fix(model): keep an exact modelLimits key ahead of any host-qualified prefix
The previous commit ranked host-qualified PREFIX matches above bare exact
matches, which broke the deliberate precedence in context.test.ts: an exact
`gpt-4o` limit was overridden by an unrelated `api.foo.com:gpt-4` prefix that
only matches a shorter, different model name.
Restore the tiering so an exact match (host-qualified or bare) always beats a
prefix, and a host-qualified key beats a bare key WITHIN the same match kind.
The supported way to set a different limit for the same model per endpoint is a
host-qualified EXACT key. Narrow the regression + comment to that behavior.
* docs(model): align modelLimits matching wording with exact-over-prefix rule
Narrow the advanced-setup wording so a host-qualified key only wins over a bare
key within the same match kind (a bare exact key still beats a host-qualified
prefix), matching lookupByModel's exact ?? prefix behavior; per-endpoint limits
for the same model need host-qualified exact keys. Also note modelLimits as part
of the documented user-override layer in the integration add-model and
common-pitfalls guides.
* docs(model): clarify modelLimits host-port key and catalog/cache precedence
Spell out that the host-qualified key uses new URL(baseUrl).host — including the
port when present (localhost:4000:my-model, not localhost:my-model) — and split
the precedence line so the built-in catalog is shown as checked before the
discovery-cache value, matching resolveModelRuntimeLimits.
6.6 KiB
Common Pitfalls
Purpose
This is the short pre-PR checklist for descriptor-era integration work.
Use it after reading the architecture note, the relevant how-to guide, and the reference samples.
Pitfall 1: Using the wrong descriptor type
Common mistake: Modeling a route as a gateway or vendor just because it feels close enough.
Safer rule:
- use
VendorDescriptorwhen the route is the canonical direct vendor API; - use
GatewayDescriptorwhen the route hosts, proxies, or aggregates models behind its own endpoint contract; - use
AnthropicProxyDescriptorwhen the route accepts Anthropic-native traffic through its own Anthropic-style env contract; - use
ModelDescriptorfor shared model metadata, not route availability.
Pitfall 2: Treating category as the routing contract
Common mistake:
Using gateway category to decide runtime behavior.
Safer rule:
transportConfig.kind is the routing contract. category is only optional
grouping/display metadata.
Pitfall 3: Reintroducing removed gateway fields
Common mistake:
Adding fields such as targetVendorId, isOpenAICompatible, or routing-style
gateway classification.
Safer rule:
Keep routing in transportConfig.kind. Use current descriptor fields from
src/integrations/descriptors.ts, not legacy examples from older branches or
notes.
Pitfall 4: Calling registry mutation helpers from descriptor files
Common mistake:
Using registerGateway(...), registerVendor(...), or registerModel(...)
inside contributor-authored descriptor files.
Safer rule:
Use the define* helpers from src/integrations/define.ts and default-export
the descriptor. Loader-owned registration stays in src/integrations/index.ts.
Pitfall 5: Putting route availability into shared model files
Common mistake:
Treating src/integrations/models/*.ts as the main place to say where a model
is available.
Safer rule: Shared model descriptors answer what a model is. Route-owned catalogs answer where it is offered.
Pitfall 6: Duplicating model defaults in catalog entries
Common mistake:
Marking catalog entries with per-model default or recommended flags after
the route already declares defaultModel.
Safer rule:
Declare the route's default once with defaultModel. UI recommendation labels
derive from that route default.
Pitfall 7: Forgetting providerModelMap boundaries
Common mistake:
Assuming providerModelMap enables a route automatically.
Safer rule:
Use providerModelMap only to record route-specific API names for the same
conceptual model. The route catalog still decides whether that route exposes
the model.
Pitfall 8: Omitting openaiShim.maxTokensField on strict routes
Common mistake: Assuming every OpenAI-compatible route accepts the same max-token field.
Safer rule:
- use
max_completion_tokensfor newer hosted OpenAI-style contracts; - use
max_tokensfor local or legacy-shaped routes and other providers that reject the newer field; - keep the choice explicit in
transportConfig.openaiShim.maxTokensFieldwhen the route is strict.
Pitfall 9: Flattening real protocol differences
Common mistake: Treating Bedrock, Vertex, Gemini, GitHub native Claude mode, or Mistral as if they were all just generic OpenAI-compatible routes.
Safer rule: If the external API contract is genuinely different, keep that difference explicit. Descriptor-first does not mean protocol differences should be hidden.
Pitfall 10: Overstating /usage support
Common mistake: Declaring usage support because the descriptor schema allows it, without checking the active runtime/UI path.
Safer rule:
- use
usage.supported: trueonly when the route has real current support; - delegate from a gateway to a vendor only when the vendor is the true source of usage data;
- keep unsupported routes explicit with
usage: { supported: false }; - remember that the current resolver in
src/commands/usage/index.tsis still vendor/gateway-focused, with the current settings UI still concrete for Anthropic, MiniMax, and Codex.
Pitfall 11: Hiding discovery complexity in the descriptor file
Common mistake:
Packing a large hybrid catalog or complex discovery rules inline in
gateways/<id>.ts.
Safer rule:
Move large catalog or discovery-specific logic into a companion
gateways/<id>.models.ts file and keep the descriptor file small.
Pitfall 12: Rebuilding the old OpenAI context table
Common mistake:
Adding built-in context or output limits to
src/utils/model/openaiContextWindows.ts.
Safer rule:
Put built-in model metadata in src/integrations/models/. Keep
openaiContextWindows.ts focused on documented user overrides — the
CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS / CLAUDE_CODE_OPENAI_MAX_OUTPUT_TOKENS
env vars and the settings.json modelLimits map — not a built-in table.
Pitfall 13: Forgetting the compatibility layer
Common mistake: Changing descriptor metadata and assuming every public surface is already descriptor-native.
Safer rule:
If the route should be user-facing in preset flows, add descriptor preset
metadata and regenerate the artifacts:
bun run integrations:generatesrc/integrations/generated/integrationArtifacts.generated.ts- env-facing flows that still preserve legacy names
Do not hand-edit:
src/integrations/compatibility.tssrc/integrations/profileResolver.tssrc/integrations/providerUiMetadata.ts- preset typing or preset ordering tables
Only touch remaining env-facing compatibility surfaces when the route truly needs them.
Pitfall 14: Using stale repo paths in docs
Common mistake: Pointing contributors at outdated files or command entrypoints.
Safer rule: Use the current repo surfaces:
src/commands/usage/index.tsfor descriptor-backed/usageroutingsrc/components/Settings/Usage.tsxfor the current usage UI boundarysrc/integrations/routeMetadata.tsfor route/default/label helperssrc/integrations/runtimeMetadata.tsfor request-shaping metadatasrc/integrations/discoveryCache.tsandsrc/integrations/discoveryService.tsfor discovery caching and loading
Final check
Before opening or landing integration docs or descriptor changes:
- confirm the descriptor type is correct;
- confirm
transportConfig.kindis doing the routing work; - confirm examples use
define*helpers plus default exports; - confirm route catalogs own availability;
- confirm route defaults are declared once through
defaultModel; - confirm built-in model limits live in
src/integrations/models/; - confirm strict OpenAI-compatible routes specify the correct max-token field;
- confirm
/usagedocs match the actual current resolver/UI behavior; - confirm any illustrative sample is clearly marked as illustrative.