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
11 changes: 8 additions & 3 deletions src/cli/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { randomUUID } from "node:crypto";
import { createInterface } from "node:readline/promises";
import { syncModelsToCodex } from "../codex/sync";
import { configuredContextWindow } from "../codex/catalog/provider-fetch";
import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
import {
canonicalizeReasoningEfforts,
Expand Down Expand Up @@ -86,6 +87,11 @@ interface ModelEntry {
reasoningEfforts: string[] | null;
}

/**
* Collect static configured models for all providers or one selected provider.
* Keep each provider's default model first and resolve metadata through shared helpers.
* Live-discovered models are not fetched by this listing.
*/
function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[] {
const entries: ModelEntry[] = [];
const providers = providerFilter
Expand All @@ -95,10 +101,9 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
for (const [provName, prov] of Object.entries(providers)) {
if (!prov) continue;
const seen = new Set<string>();
const contextWindows = prov.modelContextWindows ?? {};
const inputModalities = prov.modelInputModalities ?? {};
const globalContext = prov.contextWindow ?? null;

/** Append one model with resolved metadata, ignoring duplicates within this provider. */
const addModel = (model: string, isDefault: boolean) => {
if (seen.has(model)) return;
seen.add(model);
Expand All @@ -124,7 +129,7 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
provider: provName,
model,
isDefault,
contextWindow: modelRecordValue(contextWindows, model) ?? globalContext,
contextWindow: configuredContextWindow(prov, model) ?? null,
inputModalities: modalities,
reasoningEfforts: efforts,
});
Expand Down
30 changes: 29 additions & 1 deletion src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,36 @@ export function clearGatherRoutedModelsInflight(): void {
gatherInflight.clear();
}

const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/;

/**
* Resolve an unknown Claude point release or date pin from the nearest configured
* family row. Only numeric tail segments are removed so unrelated model families
* cannot inherit one another's limits.
*/
function anthropicFamilyContextWindow(
record: Record<string, number> | undefined,
id: string,
): number | undefined {
if (!record || !id.toLowerCase().startsWith("claude-")) return undefined;
let candidate = id;
while (true) {
const cut = candidate.lastIndexOf("-");
if (cut <= 0 || !NUMERIC_MODEL_ID_SEGMENT.test(candidate.slice(cut + 1))) return undefined;
candidate = candidate.slice(0, cut);
const value = modelRecordValue(record, candidate);
if (typeof value === "number" && value > 0) return value;
}
}

/**
* Resolve the configured context window in exact-model, Anthropic numeric-family,
* then provider-wide order. Return undefined when the selected value is not positive.
Comment on lines +652 to +653

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document Anthropic family-key inheritance

This changes the public meaning of modelContextWindows, but docs-site/src/content/docs/reference/configuration/providers.md:129 still describes it only as a per-model map and gives operators no indication that an Anthropic key such as claude-fable-5 also applies to numeric descendants. Document the matching order and Anthropic-only numeric-tail behavior so configuration edits do not unexpectedly affect additional models.

AGENTS.md reference: src/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

*/
export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
const configured = modelRecordValue(prov.modelContextWindows, id)
?? (prov.adapter === "anthropic" ? anthropicFamilyContextWindow(prov.modelContextWindows, id) : undefined)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Share family resolution with request admission

For an Anthropic numeric variant with a smaller provider-wide fallback—for example the tested claude-fable-5-2 with contextWindow: 128000 and a 1M family entry—the catalog and ocx models now report 1M, but resolveInputCeiling in src/server/responses/input-admission.ts:149-150 still performs only an exact lookup and therefore rejects requests above the 128k fallback. candidateCapabilityEvidence and the compatibility report have the same divergent lookup. Move this resolver to a shared leaf module and use it in every runtime consumer, with an input-admission regression test for the inherited variant.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

?? prov.contextWindow;
return typeof configured === "number" && configured > 0 ? configured : undefined;
}

Expand Down
17 changes: 17 additions & 0 deletions tests/cli/cli-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ describe("ocx models richer metadata", () => {
noVisionModels: ["model-b"],
reasoningEfforts: ["low", "medium", "high"],
},
anthropic: {
adapter: "anthropic",
baseUrl: "https://api.anthropic.com",
models: ["claude-fable-5-2", "Claude-fable-5-3", "claude-fable-5-1", "unknown-model"],
contextWindow: 128000,
modelContextWindows: { "claude-fable-5": 1000000, "claude-fable-5-1": 800000 },
},
},
defaultProvider: "test",
};
Expand All @@ -164,6 +171,16 @@ describe("ocx models richer metadata", () => {
const modelB = parsed.models.find((m: { model: string }) => m.model === "model-b");
expect(modelB.contextWindow).toBe(32000);
expect(modelB.inputModalities).toEqual(["text"]);

const anthropicWindows = Object.fromEntries(parsed.models
.filter((m: { provider: string }) => m.provider === "anthropic")
.map((m: { model: string; contextWindow: number }) => [m.model, m.contextWindow]));
expect(anthropicWindows).toMatchObject({
"claude-fable-5-2": 1000000,
"Claude-fable-5-3": 1000000,
"claude-fable-5-1": 800000,
"unknown-model": 128000,
});
} finally {
removeTreeWithRetry(dir);
}
Expand Down
30 changes: 30 additions & 0 deletions tests/providers/provider-registry-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,36 @@ describe("provider registry parity", () => {
}
});

test("unknown Claude numeric variants inherit the nearest configured family context window", () => {
const anthropic = PROVIDER_REGISTRY.find(entry => entry.id === "anthropic");
const seed = providerConfigSeed(anthropic!);
const contextWindow = (id: string) => applyProviderConfigHints("anthropic", seed, {
id,
provider: "anthropic",
}).contextWindow;

expect(contextWindow("claude-fable-5-2")).toBe(1_000_000);
expect(contextWindow("Claude-fable-5-2")).toBe(1_000_000);
expect(contextWindow("claude-haiku-4-5-20251001")).toBe(200_000);
expect(contextWindow("CLAUDE-HAIKU-4-5-20251001")).toBe(200_000);
expect(contextWindow("claude-opus-4-1-20250805")).toBeUndefined();
expect(contextWindow("claude-3-7-sonnet-20250219")).toBeUndefined();
});

test("context-window family inheritance stays scoped to the Anthropic adapter", () => {
const minimax = PROVIDER_REGISTRY.find(entry => entry.id === "minimax");
const seed = {
...providerConfigSeed(minimax!),
modelContextWindows: { "claude-fable-5": 1_000_000 },
};
const model = applyProviderConfigHints("minimax", seed, {
id: "claude-fable-5-2",
provider: "minimax",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

expect(model.contextWindow).toBeUndefined();
});

test("GUI preset projection preserves current featured set plus key catalog and custom", () => {
const featured = deriveFeaturedProviderIds();
expect(featured).toEqual([
Expand Down
Loading