From b54cc9ee8c8ab94cb3cba1a8a68cb3dbbf860c3b Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Sat, 8 Aug 2026 11:37:56 -0700 Subject: [PATCH] Share one recursive tree-walker across the models.dev pricing collectors collectModelPricing, collectModelReasoning, and collectModelContextWindows each recursed the same untyped JSON tree independently and re-derived the model id the same way, so a payload-shape change needed the same fix applied three times. They had also drifted: the pricing collector was the only one that didn't handle a top-level array payload, silently dropping pricing for any tree shaped that way while reasoning and context-window extraction worked fine. walkModelNodes now does the traversal and id resolution once, yielding (id, node) pairs that each parser extracts its own field from. --- src/cost/pricing-fetcher.ts | 74 +++++++++++------------------- tests/unit/pricing-fetcher.test.ts | 10 ++++ 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/cost/pricing-fetcher.ts b/src/cost/pricing-fetcher.ts index 1b0858a8a..3f6e41870 100644 --- a/src/cost/pricing-fetcher.ts +++ b/src/cost/pricing-fetcher.ts @@ -75,72 +75,54 @@ function parseModelPricing(value: unknown): ModelPricing | null { }; } -function collectModelPricing(value: unknown, models: Record): void { +/** + * Recurses through an untyped models.dev JSON tree and yields every node that + * resolves to a model id, alongside that node. Each `parseModelsDev*` walker + * below shares this single traversal and only differs in which field it + * extracts from the yielded node. + */ +function* walkModelNodes(value: unknown): Generator<[id: string, node: Record]> { + if (Array.isArray(value)) { + for (const item of value) yield* walkModelNodes(item); + return; + } if (!isRecord(value)) return; const id = typeof value.id === "string" ? value.id : typeof value.model === "string" ? value.model : null; - const pricing = parseModelPricing(value); - if (id !== null && pricing !== null) { - models[id] = pricing; - } + if (id !== null) yield [id, value]; for (const child of Object.values(value)) { - if (isRecord(child)) { - collectModelPricing(child, models); - continue; - } - if (Array.isArray(child)) { - for (const item of child) collectModelPricing(item, models); - } + yield* walkModelNodes(child); } } export function parseModelsDevPricing(payload: unknown): Record { const models: Record = {}; - collectModelPricing(payload, models); - return models; -} - -function collectModelReasoning(value: unknown, reasoning: Record): void { - if (!isRecord(value)) { - if (Array.isArray(value)) for (const item of value) collectModelReasoning(item, reasoning); - return; - } - const id = typeof value.id === "string" ? value.id : typeof value.model === "string" ? value.model : null; - if (id !== null && typeof value.reasoning === "boolean") { - reasoning[id] = value.reasoning; - } - for (const child of Object.values(value)) { - if (isRecord(child) || Array.isArray(child)) collectModelReasoning(child, reasoning); + for (const [id, node] of walkModelNodes(payload)) { + const pricing = parseModelPricing(node); + if (pricing !== null) models[id] = pricing; } + return models; } export function parseModelsDevReasoning(payload: unknown): Record { const reasoning: Record = {}; - collectModelReasoning(payload, reasoning); - return reasoning; -} - -function collectModelContextWindows(value: unknown, windows: Record): void { - if (!isRecord(value)) { - if (Array.isArray(value)) for (const item of value) collectModelContextWindows(item, windows); - return; - } - const id = typeof value.id === "string" ? value.id : typeof value.model === "string" ? value.model : null; - // models.dev nests the window under `limit.context`. - const limit = isRecord(value.limit) ? value.limit : undefined; - const context = limit !== undefined && typeof limit.context === "number" ? limit.context : undefined; - if (id !== null && context !== undefined && Number.isFinite(context) && context > 0) { - windows[id] = context; - } - for (const child of Object.values(value)) { - if (isRecord(child) || Array.isArray(child)) collectModelContextWindows(child, windows); + for (const [id, node] of walkModelNodes(payload)) { + if (typeof node.reasoning === "boolean") reasoning[id] = node.reasoning; } + return reasoning; } export function parseModelsDevContextWindows(payload: unknown): Record { const windows: Record = {}; - collectModelContextWindows(payload, windows); + for (const [id, node] of walkModelNodes(payload)) { + // models.dev nests the window under `limit.context`. + const limit = isRecord(node.limit) ? node.limit : undefined; + const context = limit !== undefined && typeof limit.context === "number" ? limit.context : undefined; + if (context !== undefined && Number.isFinite(context) && context > 0) { + windows[id] = context; + } + } return windows; } diff --git a/tests/unit/pricing-fetcher.test.ts b/tests/unit/pricing-fetcher.test.ts index 9c7787b49..a73398a9a 100644 --- a/tests/unit/pricing-fetcher.test.ts +++ b/tests/unit/pricing-fetcher.test.ts @@ -25,6 +25,16 @@ test("parseModelsDevContextWindows reads limit.context per model", () => { expect(windows["openai/no-limit"]).toBeUndefined(); }); +test("parseModelsDevPricing walks a top-level array payload the same way the other collectors do", () => { + // A nesting level where the root itself is an array, rather than an object + // whose values are arrays. parseModelsDevReasoning and + // parseModelsDevContextWindows already handle this; pricing must match. + const models = parseModelsDevPricing([ + { id: "m1", input_cost_per_million: 1, output_cost_per_million: 2 }, + ]); + expect(models["m1"]).toBeDefined(); +}); + function response(body: unknown, ok = true, status = 200): Response { return new Response(JSON.stringify(body), { status: ok ? status : status }); }