fix(config): prefer context_length from API in model discovery

Updated model discovery to use `context_length` reported by the API when available, falling back to bundled reference data and default. Added `context_length` field to parsed model response and modified context window assignment logic.
This commit is contained in:
Nicolas Lorin
2026-06-13 20:40:07 +02:00
committed by can1357
parent 9a5e44df68
commit 87c9e77dbe
3 changed files with 76 additions and 4 deletions
+3
View File
@@ -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
@@ -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<Api>[] = [];
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<Api>[] = [];
@@ -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
@@ -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);
});
});