Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs-site/src/content/docs/guides/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ Two exceptions are worth knowing because you can hit them:
| OrcaRouter | `ocx login orcarouter-oauth` — consent mints a user-owned, long-lived `sk-orca-…` key, and the request then carries a key | `orcarouter` — the same key pasted by hand |
| Meta Muse | `ocx login meta-muse` can import a local Muse Code CLI key or start device login. Meta scopes that credential to its own CLI, so this is an unsupported use: how the calls settle is not observable from the API, and you should treat every call as billable against your account | `meta-model` is the supported path — every call is metered per token, and a Muse Code subscription does not work there |

When the Grok subscription catalog publishes selectable context windows, opencodex uses the
widest published rung: `[256000, 500000]` exposes 500k for Grok 4.5–4.7, including the 4.7 Fast
lane. Your configured model window and enabled Context cap still lower that value. If your plan
serves a shorter window, configure that limit; the catalog does not verify account entitlement.

Cursor, Kiro, Zed Hosted AI and Nous Portal are login-only and have no API-key equivalent. Google Antigravity is
login-only too: `ocx login google-antigravity` signs in with your Google account over the Cloud Code
Assist wire, and the `google` preset beside it is the AI Studio Gemini API — a different product
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. |
| `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. |
| `modelDisplayNames?` | `Record<string, string>` | Durable labels used only for display, keyed by this provider's exact upstream model id. Labels win over provider catalog metadata, survive discovery refreshes and provider edits, and never change authentication, adapter behavior, routing, billing, upstream request construction, the routed `provider/model` selector, or the upstream wire model. Keys are exact and case sensitive. Unknown model ids are kept so a temporarily missing model receives its label when it returns. The map accepts at most 2,000 entries, matching the discovery limit. |
| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. The Models dashboard exposes this separately from `providerContextCaps`. |
| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. Registry defaults cannot override a lower configured limit; explicit `modelContextWindows` entries take priority. The Models dashboard exposes this separately from `providerContextCaps`. |
| `modelContextWindows?` | `Record<string, number>` | Per-model context fallbacks/caps. These override `contextWindow`: an unknown window uses the configured value, while smaller live metadata remains authoritative. |
| `inferContextWindow?` | `boolean` | Custom providers only, default off. `true` fills a missing window from bundled model-ID metadata ([details](#context-windows-on-custom-providers)); the value is an estimate, not verified relay capacity. |
| `modelContextTiers?` | `Record<string, "default" \| "long_context">` | Per-model upstream tier for `github-copilot`. `long_context` sends `contextTier: "long_context"`. Catalog metadata grows only with an exact per-model `modelContextWindows` value that documents the supported window; that value wins before the provider cap. An unknown model keeps its observed window. Unconfigured passthrough requests retain a caller-supplied `contextTier` field on any provider. |
Expand Down
2 changes: 1 addition & 1 deletion scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@
"bun-stream-caps.test.ts": "lib",
"cache-diagnostic.test.ts": "usage",
"cancel-body-on-abort.test.ts": "server",
"catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration",
"catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration", "catalog-context-window-ladder.test.ts": "codex-integration",
"catalog-cursor-search.test.ts": "codex-integration",
"catalog-custom-ordering.test.ts": "codex-integration",
"catalog-duplicate-slug-dedup.test.ts": "codex-integration",
Expand Down
3 changes: 3 additions & 0 deletions src/codex/catalog/gather-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { applyRegistryCapabilitySeedFill } from "./model-hints";
import {
captureFastPolicyAuthority,
fastPolicyForModel,
Expand Down Expand Up @@ -360,6 +361,8 @@ export function captureProviderGather(
): CapturedProviderGather {
const metadataConfigDir = getConfigDir();
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
// Bound seed windows while the clone still distinguishes operator settings from defaults.
applyRegistryCapabilitySeedFill(name, enriched);
enrichProviderFromRegistry(name, enriched);
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
const registryEntry = registryTransportMatch ? getProviderRegistryEntry(name) : undefined;
Expand Down
40 changes: 36 additions & 4 deletions src/codex/catalog/model-hints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
import type { ComboCatalogOmission } from "./aggregation";
import { clampObservedModelLimits, resolveModelPolicy } from "../../providers/resolved-model-policy";
import { mapFill } from "../../providers/resolved-model-policy-merge";
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
import type {
CatalogAdmissionSnapshot,
Expand All @@ -110,8 +111,8 @@ import type {
/**
* Fill the registry seed's per-model numeric capability maps beneath the provider's own
* values, mutating `prov` in place. The merge is per key — an operator's entry always
* wins; a model the persisted map never mentions picks up its seed value — matching
* `mergeRecordFill` in src/router.ts exactly.
* wins; a model the persisted map never mentions picks up its seed value, bounded
* by the operator's provider-wide context limit before enrichment can hide its origin.
*
* Routing already performs this fill at resolve time (routedProviderConfig in
* src/router.ts) and the catalog did not, and that divergence is #4570:
Expand All @@ -134,10 +135,14 @@ export function applyRegistryCapabilitySeedFill(name: string, prov: OcxProviderC
const entry = getProviderRegistryEntry(name);
if (!entry) return;
if (entry.modelContextWindows || prov.modelContextWindows) {
prov.modelContextWindows = { ...(entry.modelContextWindows ?? {}), ...(prov.modelContextWindows ?? {}) };
const providerWindow = typeof prov.contextWindow === "number" && prov.contextWindow > 0
? prov.contextWindow : undefined;
const seeds = Object.fromEntries(Object.entries(entry.modelContextWindows ?? {}).map(([id, window]) =>
[id, providerWindow !== undefined ? Math.min(window, providerWindow) : window]));
[prov.modelContextWindows] = mapFill(seeds, prov.modelContextWindows);
}
if (entry.modelMaxOutputTokens || prov.modelMaxOutputTokens) {
prov.modelMaxOutputTokens = { ...(entry.modelMaxOutputTokens ?? {}), ...(prov.modelMaxOutputTokens ?? {}) };
[prov.modelMaxOutputTokens] = mapFill(entry.modelMaxOutputTokens, prov.modelMaxOutputTokens);
}
}
const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/;
Expand Down Expand Up @@ -562,6 +567,25 @@ export function positiveSafeInteger(...values: unknown[]): number | undefined {
return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined;
}

/** Bound inspection of untrusted upstream context ladders. */
const MAX_CONTEXT_WINDOW_RUNGS = 8;

/**
* The widest rung of a selectable context ladder an upstream published, if it published one.
*
* A lane that offers a long-context rung announces the whole ladder and reports its default
* rung as the effective window. The capacity is the widest rung, not the default one.
*/
function widestContextWindowRung(value: unknown): number | undefined {
if (!Array.isArray(value)) return undefined;
let widest: number | undefined;
for (const rung of value.slice(0, MAX_CONTEXT_WINDOW_RUNGS)) {
const window = positiveSafeInteger(rung);
if (window !== undefined && (widest === undefined || window > widest)) widest = window;
}
return widest;
}

function normalizedMetadataString(raw: string, maxLength: number): string | undefined {
if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined;
const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength);
Expand Down Expand Up @@ -771,6 +795,14 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
// real values. Appended after the recognized fields for the same reason as the
// llama.cpp entries above: no provider that already resolves changes behavior.
capabilityRecord?.context_length,
// The Grok CLI subscription lane publishes its default rung in `context_window` and the
// selectable ladder in `context_windows` (`[256000, 500000]` for grok-4.5/4.6/4.7 on
// 2026-10-24). Reading only the default rung published half of what the lane offers, and
// because a configured or registered window may only LOWER the observed one, the 500k the
// registry already carries could not raise it back. The widest rung is the model's capacity,
// so it outranks the default rung beside it; every other recognized field keeps the
// precedence it already had, and a lane that publishes no ladder is unaffected.
widestContextWindowRung(item.context_windows),
// OpenGateway publishes a top-level context_window. Keep this last so catalogs
// already resolving through any recognized field retain their existing window.
item.context_window,
Expand Down
34 changes: 34 additions & 0 deletions structure/catalog-vendor-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ then Azure metadata, then this estimate, then an enabled context cap as the wind
retain their existing policies; OpenAI API metadata used for a custom relay is not Codex subscription
capacity. The lookup changes no configuration, credentials, pricing, or request model identity.

`src/codex/catalog/gather-capture.ts` fills registry numeric capability maps on the detached
provider before enrichment. `applyRegistryCapabilitySeedFill` bounds registry context seeds by
an explicitly configured positive provider-wide window, then overlays explicit per-model windows
using the static resolver's case-aware map merge for both context and output limits.
Thus enrichment cannot promote a registry default above the operator's limit; explicit per-model
settings still win and smaller observed windows remain authoritative. Capture, hints and Codex
serialization are covered by `tests/codex-integration/catalog-context-window-ladder.test.ts`,
including OAuth xAI's 500k ladder with a 300k provider limit and smaller key-auth limits.

Management reads, strict boolean/null PATCH updates, and same-name overwrite preservation of the
optional flag follow the [provider API contract](gui-and-management-api.md#api-ownership).

Expand Down Expand Up @@ -45,3 +54,28 @@ window, including retained lower soft budgets. Native capacity remains defined b
`tests/codex-integration/catalog-native-max-context.test.ts`.

The first-party DeepSeek catalog retains `deepseek-v4-pro`: its retirement plan was withdrawn, and the official models endpoint still advertises it. `src/codex/catalog/parsing.ts` does not exclude that first-party row; third-party exclusions remain provider-specific. `src/providers/registry/entries-core.ts` restores its text-only capability, context window, reasoning controls and inbound-scoped Responses route while retaining Flash as the default. Evidence: [official service continuation notice](https://api-docs.deepseek.com/updates/), checked 2026-10-23.

## Published context ladders

A lane may publish a selectable window ladder instead of one number. The Grok CLI subscription
lane (`cli-chat-proxy.grok.com`) reports its default rung in `context_window` and the ladder in
`context_windows`; PR #7701 reports a 2026-10-24 observation in which all four rows (grok-4.5, grok-4.6, grok-4.7,
grok-4.7-build-fast) read `context_window: 256000` with `context_windows: [256000, 500000]`.
`catalogHintsFromModelsApiItem` in `src/codex/catalog/model-hints.ts` reads the widest valid
rung, so the 500k the registry already carries survives a lane that reports its default rung as
the effective window. Configuration cannot restore that: `clampObservedModelLimits` only lowers
an observation, so a lane-reported short window was previously sticky.

The read sits immediately above the `context_window` fallback — a ladder outranks the default rung
beside it, while every other recognized field keeps the precedence it already had, and a lane that
publishes no ladder is unaffected. Inspection is bounded to eight rungs. A ladder is the model's
capacity, not evidence that the configured tier serves it; the same rule as above applies, and an
operator who is entitled to only the short rung sets a lower window or an enabled Context cap.
Covered by `tests/codex-integration/catalog-context-window-ladder.test.ts`.

Independent subscription overflow probes recorded a 500000-token rejection ceiling for 4.7 and
build-fast (`devlog/_fin/260930_grok47_build_unify/010_probe-evidence.md`); this is not a successful
near-limit generation test for every model or account. The [official API model table](https://docs.x.ai/developers/models.md)
also lists 500k context tokens for 4.5, 4.6 and 4.7 (checked 2026-10-26), independently of subscription entitlement.
No equivalent 4.5/4.6 boundary probe is recorded here. Effort and Fast/Flex policy projection remain
independent of context parsing; xAI rows retain `supportsFlexTier: false` unless explicitly eligible.
2 changes: 1 addition & 1 deletion structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ validation-pending. Import alone supplies no entitlement evidence for the model
## Shared catalog
`src/server/models-capabilities.ts` preserves the legacy `/v1/models` negative reasoning signal for an explicit empty effort ladder when no reasoning, thinking-toggle or effort capability is declared. Explicit capability axes take precedence; a declared thinking toggle or reasoning support remains available with no effort rungs. This compatibility projection does not change Codex ladders, Claude thinking discovery, client exports or request wire policy.
Vision-sidecar attachment metadata is projected after provider discovery/cache reads in `src/codex/catalog/routed-gather.ts`. The gather captures `visionSidecar.enabled !== false` before awaiting discovery, and `src/codex/catalog/gather-capture.ts` includes that effective flag in the flight key. Provider TTL/stale rows exclude synthetic sidecar image input; custom rows and synthesized combo members use the same captured switch. Explicit/native image modalities remain intact, and enabled advertisement retains the existing config-based consumer predicate without probing credential availability. An effective Vision switch change through `src/server/management/config-routes.ts` invokes the catalog-only management funnel after persistence to publish both catalog and cache; incomplete work is exposed as `catalogRefreshPending` without claiming client reload. Coverage: `tests/codex-integration/catalog-vision-sidecar-disabled.test.ts` and `tests/vision/sidecar-settings-vision-catalog.test.ts`.
Custom-provider missing windows use [source-labelled vendor estimates](catalog-vendor-context.md) only when the provider opts in with `inferContextWindow: true`; these remain distinct from asserted routing capacity.
Custom-provider missing windows use [source-labelled vendor estimates](catalog-vendor-context.md) only when the provider opts in with `inferContextWindow: true`; these remain distinct from asserted routing capacity. Registry context seed capture preserves [configured window precedence](catalog-vendor-context.md): provider-wide limits bound seed defaults, explicit per-model windows win, and smaller observed limits remain authoritative.
Static policy and observed catalog evidence are separate authorities. Antigravity grouping and selection projection follow the [discovered effort-family contract](providers-and-adapters.md#antigravity-effort-families).
`src/providers/resolved-model-policy.ts` resolves and freezes only registry/operator static facts,
hard wire pins, aliases, and explicit false/empty declarations. Discovery responses, generated
Expand Down
Loading
Loading