diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 945f18084..95170b7a4 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -16,6 +16,9 @@ ### Fixed - Fixed dynamic forced tool choices from queue hooks being filtered against the active per-turn tool set before provider dispatch. ([#1701](https://github.com/can1357/oh-my-pi/issues/1701)) +### Fixed +- Honor `context_length` reported by OpenAI-compatible proxy `/v1/models` discovery (`discovery: { type: "proxy" }` or `discovery: { type: "openai-models-list" }`), so aggregator-reported windows override the bundled reference when they differ. + ## [15.12.4] - 2026-06-13 ### Fixed diff --git a/packages/coding-agent/src/config/model-discovery.ts b/packages/coding-agent/src/config/model-discovery.ts index 0d8308dc8..bb489bdfa 100644 --- a/packages/coding-agent/src/config/model-discovery.ts +++ b/packages/coding-agent/src/config/model-discovery.ts @@ -393,7 +393,7 @@ export async function discoverOpenAIModelsList( const response = apiKey ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` })) : await attempt(baseHeaders); - const payload = (await response.json()) as { data?: Array<{ id: string }> }; + const payload = (await response.json()) as { data?: Array<{ id: string; context_length?: number }> }; const models = payload.data ?? []; const discovered: Model[] = []; for (const item of models) { @@ -409,7 +409,7 @@ export async function discoverOpenAIModelsList( reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, + contextWindow: toPositiveNumberOrUndefined(item.context_length) ?? 128000, maxTokens: discoveryDefaultMaxTokens(providerConfig.api), headers, compat: { @@ -463,7 +463,7 @@ export async function discoverProxyModels( ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` })) : await attempt(baseHeaders); const payload = (await response.json()) as { - data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[] }>; + data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>; }; const items = payload.data ?? []; const discovered: Model[] = []; @@ -499,7 +499,12 @@ export async function discoverProxyModels( // upstream bundled catalogs, so keep costs local-unknown even when // we successfully recover the upstream model identity. cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: reference?.contextWindow ?? 128000, + // Prefer the context_length the API reports for this model; fall + // back to the bundled reference, then a sane default. + contextWindow: + toPositiveNumberOrUndefined(item.context_length) ?? + reference?.contextWindow ?? + 128000, maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api), headers, // OpenAI-compat fields are no-ops on anthropic models; the diff --git a/packages/coding-agent/test/model-discovery.test.ts b/packages/coding-agent/test/model-discovery.test.ts index adf0fbe9a..67a25b4b6 100644 --- a/packages/coding-agent/test/model-discovery.test.ts +++ b/packages/coding-agent/test/model-discovery.test.ts @@ -607,4 +607,68 @@ describe("ModelRegistry runtime discovery", () => { expect(llama?.maxTokens).toBe(32_768); expect(llama?.input).toEqual(["text", "image"]); }); + test("openai-models-list discovery honors API-reported context_length over fallback", async () => { + writeRawModelsJson({ + "openai-test": { + baseUrl: "http://127.0.0.1:9999", + api: "openai-completions", + auth: "none", + discovery: { type: "openai-models-list" }, + }, + }); + const fetchMock: FetchImpl = async input => { + const url = String(input); + if (url === "http://127.0.0.1:9999/v1/models") { + return new Response( + JSON.stringify({ + data: [ + { id: "openai-test/contextual-model", context_length: 16385 }, + { id: "openai-test/no-context-model" }, + ], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + throw new Error(`Unexpected URL: ${url}`); + }; + const registry = new ModelRegistry(authStorage, modelsJsonPath, { fetch: fetchMock }); + await registry.refresh(); + const contextual = registry.getAll().find(m => m.provider === "openai-test" && m.id === "openai-test/contextual-model"); + expect(contextual?.contextWindow).toBe(16385); + const fallback = registry.getAll().find(m => m.provider === "openai-test" && m.id === "openai-test/no-context-model"); + expect(fallback?.contextWindow).toBe(128000); + }); + + test("proxy discovery honors API-reported context_length and endpoint routing", async () => { + writeRawModelsJson({ + "proxy-test": { + baseUrl: "http://127.0.0.1:9998", + auth: "none", + discovery: { type: "proxy" }, + }, + }); + const fetchMock: FetchImpl = async input => { + const url = String(input); + if (url === "http://127.0.0.1:9998/v1/models") { + return new Response( + JSON.stringify({ + data: [ + { id: "anthropic-model", supported_endpoint_types: ["anthropic"], context_length: 200000 }, + { id: "openai-model", supported_endpoint_types: ["openai"], context_length: 65536 }, + ], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + throw new Error(`Unexpected URL: ${url}`); + }; + const registry = new ModelRegistry(authStorage, modelsJsonPath, { fetch: fetchMock }); + await registry.refresh(); + const anthropic = registry.getAll().find(m => m.provider === "proxy-test" && m.id === "anthropic-model"); + expect(anthropic?.api).toBe("anthropic-messages"); + expect(anthropic?.contextWindow).toBe(200000); + const openai = registry.getAll().find(m => m.provider === "proxy-test" && m.id === "openai-model"); + expect(openai?.api).toBe("openai-completions"); + expect(openai?.contextWindow).toBe(65536); + }); });