diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index 11125162ab3..a97116cc818 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -2866,6 +2866,19 @@ The caller supplies an explicit campaign ceiling, deadline, request count, and concurrency. Monetary decisions use integer nanodollars. Model list prices remain estimates with a pinned version; they do not become provider invoices. +A versioned pricecard may declare `responseModels`, a list of one to sixteen +additional exact response model identities with the same reviewed rates. Request +admission still requires the exact `model`; the list only permits those response +identities for usage estimates and reviewed reservation bounds. Entries must be +unique, valid ledger identities and distinct from `model`. No prefix matching or +date removal is performed. Add or change mappings only with a new price version +and verified provider provenance. The reviewed bound digest includes the mapping; +changing it invalidates old bound evidence. An unlisted response remains unpriced +and disqualifies the bound. Historical receipts and ledgers remain unchanged. +The existing supported request cache policy also uses that exact mapping when +the pricecard provider and request model match. It does not repair missing write +counters, malformed TTL details or an unqualified cache policy. + The journal retains setup/agent/judge/tool/infrastructure attribution separately from heartbeat IDs. Multiple sources for the same request are retained but only one cost contributes to the total: reconciled, reported, estimated, then allocated. diff --git a/tests/runner-e2e/eval-gateway.test.ts b/tests/runner-e2e/eval-gateway.test.ts index 150fac62f5b..a5c6a0855ff 100644 --- a/tests/runner-e2e/eval-gateway.test.ts +++ b/tests/runner-e2e/eval-gateway.test.ts @@ -86,12 +86,14 @@ it("stops at the campaign cap before any upstream request", async () => { await expect(forwardEvalRequest({ ...f, transport })).rejects.toThrow("spending cap"); expect(transport).not.toHaveBeenCalled(); }); -it.each(["direct", "braintrust"] as const)("retains request-policy cost provenance without changing the %s cache policy", async mode => { +it.each((["direct", "braintrust"] as const).flatMap(mode => (["alias", "snapshot"] as const).map(model => ({ mode, model }))))( + "retains request-policy cost provenance for $model responses without changing the $mode cache policy", async ({ mode, model }) => { const f = await fixture(); const pricedRoute = { ...route, mode, price: { ...route.price, model: "gpt-5.6-sol", inputUsdPerMillion: "4", cachedReadUsdPerMillion: "0.4", - outputUsdPerMillion: "20", cacheWrite30mUsdPerMillion: "5", cacheWrite12hUsdPerMillion: "10" } }; + outputUsdPerMillion: "20", cacheWrite30mUsdPerMillion: "5", cacheWrite12hUsdPerMillion: "10", responseModels: ["gpt-5.6-sol-reviewed-snapshot"] } }; const payload = { ...f.payload, model: pricedRoute.price.model, prompt_cache_retention: "24h", stream: true }; - const body = { ...usage, model: pricedRoute.price.model, usage: { ...usage.usage, input_tokens_details: { cache_write_tokens: 6, cached_tokens: 2 } } }; + const body = { ...usage, model: model === "alias" ? pricedRoute.price.model : pricedRoute.price.responseModels[0], + usage: { ...usage.usage, input_tokens_details: { cache_write_tokens: 6, cached_tokens: 2 } } }; const transport = vi.fn(async () => new Response(`data: ${JSON.stringify({ type: "response.completed", response: body })}\n\n`, { headers: { "content-type": "text/event-stream", "x-bt-used-endpoint": "OPENAI_API_KEY", "x-bt-cached": "MISS" }, })); @@ -101,6 +103,7 @@ it.each(["direct", "braintrust"] as const)("retains request-policy cost provenan cacheWriteAttribution: { source: "request_policy", version: "openai-prompt-cache-2026-10-20", ttl: "30m" } } }); expect(f.receipts.at(-1)).toEqual(receipt); expect(f.ledger.snapshot()).toMatchObject({ knownUsd: "0.000138800", heldUsd: "0.000000000", inflight: 0 }); + expect((await readEvalCostLedger(f.file)).state.snapshot()).toMatchObject({ knownUsd: "0.000138800", heldUsd: "0.000000000", unknownRequests: 0 }); }); it("retains cancelled and provider-error requests as unknown, including repeated retries", async () => { const f = await fixture(); @@ -451,6 +454,66 @@ it("refuses missing or unreviewed attribution checkpoints before request dispatc const provisionalBound = { version: 1 as const, scope: "full_lifecycle" as const, evidenceSha256: "a".repeat(64), configurationSha256: costBoundConfigurationSha256({ provider: route.provider, mode: route.mode, price: route.price, maximumInputTokens: route.maximumInputTokens, maximumOutputTokens: route.maximumOutputTokens, reservation: evalRequestReservation(route) }) }; +it.each(["json", "sse"] as const)("prices a declared exact response snapshot from forwarded %s bytes and replays the ledger", async format => { + const f = await fixture(); + const selected = { ...route, price: { ...route.price, model: "gpt-5.4-mini", responseModels: ["gpt-5.4-mini-2026-03-17"], version: "snapshot-fixture-2" } }; + const bound = { ...provisionalBound, configurationSha256: costBoundConfigurationSha256({ + provider: selected.provider, mode: selected.mode, price: selected.price, + maximumInputTokens: selected.maximumInputTokens, maximumOutputTokens: selected.maximumOutputTokens, reservation: evalRequestReservation(selected), + }) }; + const returned = { ...usage, model: selected.price.responseModels[0] }; + const wire = Buffer.from(format === "sse" ? `data: ${JSON.stringify({ type: "response.completed", response: returned })}\r\n\r\ndata: [DONE]\r\n\r\n` : JSON.stringify(returned)); + const forwarded: Uint8Array[] = []; + const transport = vi.fn(async (_url, init) => { + expect(JSON.parse(init!.body as string).model).toBe(selected.price.model); + expect((await readEvalCostLedger(f.file)).state.snapshot().inflight).toBe(1); + return new Response(new ReadableStream({ start(controller) { + for (let index = 0; index < wire.length; index += 3) controller.enqueue(wire.subarray(index, index + 3)); + controller.close(); + } }), { headers: { "content-type": format === "sse" ? "text/event-stream" : "application/json", "x-bt-used-endpoint": selected.endpointName! } }); + }); + const receipt = await forwardEvalRequest({ ...f, route: { ...selected, provisionalBound: bound }, + payload: { ...f.payload, model: selected.price.model }, transport, onChunk: async chunk => { forwarded.push(chunk); } }); + expect(Buffer.concat(forwarded)).toEqual(wire); + expect(receipt).toMatchObject({ outcome: "succeeded", costUsd: "0.000068000", usageComplete: true, final: true, usage: { model: returned.model } }); + expect(f.receipts.at(-1)).toEqual(receipt); + const replay = (await readEvalCostLedger(f.file)).state; + expect(replay.snapshot()).toMatchObject({ knownUsd: "0.000068000", heldUsd: "0.000000000", unknownRequests: 0, boundedUnresolvedRequests: 0, inflight: 0 }); + const request = [...replay.requests.values()][0]!; + expect(request.boundInvalidated).not.toBe(true); + expect(request.observations).toEqual(expect.arrayContaining([expect.objectContaining({ + classification: "estimated", model: returned.model, costUsd: "0.000068000", pricingVersion: selected.price.version, + })])); + expect(transport).toHaveBeenCalledOnce(); +}); + +it.each(["listed-partial", "unlisted-snapshot", "unlisted-suffix", "no-mapping"] as const)("preserves unknown usage and the correct bound for %s response bytes", async reason => { + const f = await fixture(); + const selected = { ...route, price: { ...route.price, model: "gpt-5.4-mini", + ...(reason === "no-mapping" ? {} : { responseModels: ["gpt-5.4-mini-2026-03-17"] }) } }; + const bound = { ...provisionalBound, configurationSha256: costBoundConfigurationSha256({ + provider: selected.provider, mode: selected.mode, price: selected.price, + maximumInputTokens: selected.maximumInputTokens, maximumOutputTokens: selected.maximumOutputTokens, reservation: evalRequestReservation(selected), + }) }; + const model = reason === "unlisted-snapshot" ? "gpt-5.4-mini-2026-03-18" + : reason === "unlisted-suffix" ? "gpt-5.4-mini-2026-03-17-extra" : "gpt-5.4-mini-2026-03-17"; + const returned = { ...usage, model, ...(reason === "listed-partial" ? { status: "in_progress", usage: undefined } : {}) }; + const receipt = await forwardEvalRequest({ ...f, route: { ...selected, provisionalBound: bound }, payload: { ...f.payload, model: selected.price.model }, + transport: async () => new Response(JSON.stringify(returned), { headers: { "x-bt-used-endpoint": selected.endpointName! } }) }); + expect(receipt.costUsd).toBeNull(); + const replay = (await readEvalCostLedger(f.file)).state; + expect(replay.snapshot()).toMatchObject({ knownUsd: "0.000000000", heldUsd: "0.003000000", unknownRequests: 1, + boundedUnresolvedRequests: reason === "listed-partial" ? 1 : 0, inflight: 0 }); + expect([...replay.requests.values()][0]!.boundInvalidated === true).toBe(reason !== "listed-partial"); +}); + +it("does not admit a declared response snapshot as the request model", async () => { + const f = await fixture(), transport = vi.fn(), persist = vi.fn(); + const selected = { ...route, price: { ...route.price, responseModels: ["fixture-2026-03-17"] } }; + await expect(forwardEvalRequest({ ...f, route: selected, payload: { ...f.payload, model: selected.price.responseModels[0] }, transport, persist })).rejects.toThrow("unqualified_model"); + expect(transport).not.toHaveBeenCalled(); expect(persist).not.toHaveBeenCalled(); + expect(f.ledger.snapshot().requests).toHaveLength(0); +}); it("retains a reviewed model bound through a partial response and durable reload without releasing the hold", async () => { const f = await fixture(); const receipt = await forwardEvalRequest({ ...f, route: { ...route, provisionalBound }, transport: async () => { @@ -476,9 +539,10 @@ it.each(["no-headers", "route", "failover", "model", "input-limit", "output-limi expect([...replay.requests.values()][0]!.boundInvalidated).toBe(true); }); -it("refuses a changed model limit or rate with a stale reviewed bound before dispatch", async () => { +it("refuses a changed model limit, rate or response mapping with a stale reviewed bound before dispatch", async () => { const f = await fixture(), transport = vi.fn(); - for (const changed of [{ ...route, maximumInputTokens: 500 }, { ...route, price: { ...route.price, inputUsdPerMillion: "20" } }]) { + for (const changed of [{ ...route, maximumInputTokens: 500 }, { ...route, price: { ...route.price, inputUsdPerMillion: "20" } }, + { ...route, price: { ...route.price, responseModels: ["fixture-2026-03-17"] } }]) { await expect(forwardEvalRequest({ ...f, route: { ...changed, provisionalBound }, transport })).rejects.toThrow("configuration changed"); } expect(transport).not.toHaveBeenCalled(); diff --git a/tests/runner-e2e/eval-gateway.ts b/tests/runner-e2e/eval-gateway.ts index a90cd28047f..c35c0134bf6 100644 --- a/tests/runner-e2e/eval-gateway.ts +++ b/tests/runner-e2e/eval-gateway.ts @@ -4,7 +4,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import { once } from "node:events"; import { braintrustGatewayRequestParent } from "./braintrust-gateway-context.js"; import { usdToUnits } from "../../packages/shared/src/money.js"; -import { estimateProviderCost, ProviderUsageAccumulator, type EvalModelPrice, type ProviderUsageReceipt } from "./provider-usage.js"; +import { estimateProviderCost, matchesPriceResponseModel, ProviderUsageAccumulator, validatePriceResponseModels, type EvalModelPrice, type ProviderUsageReceipt } from "./provider-usage.js"; import { EvalAdmissionError, validEvalLedgerId, type EvalChargeIdentity, type openEvalCostLedger } from "./cost-ledger.js"; type Ledger = Awaited>; @@ -34,6 +34,7 @@ const row = (value: unknown): Record => value && typeof value = class EvalRequestError extends Error {} export function evalRequestReservation(route: EvalGatewayRoute): string { + validatePriceResponseModels(route.price); if (route.provisionalBound !== undefined) validateCostBoundEvidence(route.provisionalBound); if (![route.maximumInputTokens, route.maximumOutputTokens, route.timeoutMs].every(n => Number.isSafeInteger(n) && n > 0) || route.timeoutMs > 300_000 || route.provider !== route.price.provider || !validEvalLedgerId(route.price.version) || !validEvalLedgerId(route.price.model)) throw new Error("Invalid eval gateway limits or price identity"); @@ -150,7 +151,7 @@ export async function forwardEvalRequest(input: { receipt.clientDisconnected = true; receipt.outcome = "cancelled"; receipt.errorCode ??= "client_disconnected"; return true; }; - const usage = new ProviderUsageAccumulator(route.provider, body); + const usage = new ProviderUsageAccumulator(route.provider, body, route.price); const bound = modelReservationBound(route, reservation); await ledger.reserve(requestId, input.identity, reservation, bound); const abort = new AbortController(); @@ -222,7 +223,7 @@ export async function forwardEvalRequest(input: { receipt.errorCode ??= "invalid_usage"; } const observedInput = observed.tokenBounds?.input ?? 0; - if (bound && (observed.tokenBounds?.valid === false || receipt.errorCode === "invalid_usage" || observed.model !== undefined && observed.model !== route.price.model + if (bound && (observed.tokenBounds?.valid === false || receipt.errorCode === "invalid_usage" || observed.model !== undefined && !matchesPriceResponseModel(route.price, observed.model) || !Number.isSafeInteger(observedInput) || observedInput > route.maximumInputTokens || (observed.tokenBounds?.output ?? 0) > route.maximumOutputTokens || route.mode === "braintrust" && (!response || receipt.endpoint !== route.endpointName diff --git a/tests/runner-e2e/operations.test.ts b/tests/runner-e2e/operations.test.ts index 7193837a012..452403765fd 100644 --- a/tests/runner-e2e/operations.test.ts +++ b/tests/runner-e2e/operations.test.ts @@ -33,6 +33,26 @@ it("rejects credentials in the manifest and unknown pricing/billing policy field expect(() => validateEvalOperationsManifest(changed, [cell], 0)).toThrow("fields"); expect(() => validateEvalOperationsManifest({ ...manifest(), route: "braintrust" }, [cell], 0)).toThrow("project UUID"); }); +it("admits a bounded exact response mapping without widening the selected request model", () => { + const value = manifest(); + value.models[0]!.price.responseModels = [`${cell.profile.model}-2026-03-17`]; + expect(validateEvalOperationsManifest(value, [cell], 0)).toBe(value); + value.models[0]!.price.responseModels = Array.from({ length: 16 }, (_, index) => `${cell.profile.model}-snapshot-${index}`); + expect(validateEvalOperationsManifest(value, [cell], 0)).toBe(value); + const changed = structuredClone(value); changed.models[0]!.price.model = "other"; + changed.models[0]!.price.responseModels = [cell.profile.model]; + expect(() => validateEvalOperationsManifest(changed, [cell], 0)).toThrow("Missing exact model"); +}); +it.each([null, "fixture", [], [""], [" leading-space"], ["fixture?query"], [4], ["a", "a"], + new Array(1), Array.from({ length: 17 }, (_, index) => `fixture-${index}`), +].map(responseModels => ({ responseModels })))("rejects malformed or excessive response model mappings: $responseModels", ({ responseModels }) => { + const value = manifest(); Object.assign(value.models[0]!.price, { responseModels }); + expect(() => validateEvalOperationsManifest(value, [cell], 0)).toThrow("response model"); +}); +it("rejects a response mapping that repeats the exact request identity", () => { + const value = manifest(); value.models[0]!.price.responseModels = [cell.profile.model]; + expect(() => validateEvalOperationsManifest(value, [cell], 0)).toThrow("response model"); +}); it("rejects an unaffordable selected request before opening a ledger or checking credentials", async () => { const root = await mkdtemp(path.join(tmpdir(), "eval-unaffordable-preflight-")); diff --git a/tests/runner-e2e/operations.ts b/tests/runner-e2e/operations.ts index 62507e05533..ed4e82d1cee 100644 --- a/tests/runner-e2e/operations.ts +++ b/tests/runner-e2e/operations.ts @@ -37,7 +37,7 @@ export function validateEvalOperationsManifest(value: EvalOperationsManifest, ex for (const model of value.models) { fields(model, ["provider", "price", "maximumInputTokens", "maximumOutputTokens", "timeoutMs", "provisionalBound"]); const rateFields = ["inputUsdPerMillion", "cachedReadUsdPerMillion", "outputUsdPerMillion", "cacheWrite5mUsdPerMillion", "cacheWrite1hUsdPerMillion", "cacheWrite30mUsdPerMillion", "cacheWrite12hUsdPerMillion"]; - fields(model.price, ["provider", "model", "version", "sourceUrl", "longContext", ...rateFields]); + fields(model.price, ["provider", "model", "responseModels", "version", "sourceUrl", "longContext", ...rateFields]); if (model.price.longContext) { fields(model.price.longContext, ["aboveInputTokens", "rates"]); fields(model.price.longContext.rates, rateFields); } const url = new URL(model.price.sourceUrl); if (!["openai", "anthropic"].includes(model.provider) || url.protocol !== "https:" || url.username || url.password || url.search || url.hash) throw new Error("Invalid model price provenance"); diff --git a/tests/runner-e2e/provider-usage.test.ts b/tests/runner-e2e/provider-usage.test.ts index 6549e15684e..8ce49560f54 100644 --- a/tests/runner-e2e/provider-usage.test.ts +++ b/tests/runner-e2e/provider-usage.test.ts @@ -15,6 +15,20 @@ it("never turns missing usage or incompatible model/pricing into a free charge", expect(estimateProviderCost(zero, price)).toBe("0.000000000"); expect(estimateProviderCost(zero, { ...price, model: "other" })).toBeNull(); }); +it("prices only exact response identities declared by the versioned pricecard", () => { + const selected = { ...price, model: "gpt-5.4-mini", responseModels: ["gpt-5.4-mini-2026-03-17"] }; + const receipt = normalizeProviderUsage("openai", { model: selected.responseModels[0], usage: { + input_tokens: 100, input_tokens_details: { cache_write_tokens: 0, cached_tokens: 40 }, output_tokens: 20, + } }); + expect(estimateProviderCost(receipt, selected)).toBe("0.000360000"); + expect(estimateProviderCost({ ...receipt, model: selected.model }, selected)).toBe("0.000360000"); + expect(estimateProviderCost(receipt, { ...selected, provider: "anthropic" })).toBeNull(); + expect(estimateProviderCost(receipt, { ...selected, responseModels: undefined })).toBeNull(); + expect(estimateProviderCost(receipt, { ...selected, responseModels: selected.responseModels[0] as never })).toBeNull(); + for (const model of [undefined, "gpt-5.4-mini-2026-03-18", "gpt-5.4-mini-2026-03-17-extra", "gpt-5.4", "GPT-5.4-mini-2026-03-17"]) { + expect(estimateProviderCost({ ...receipt, model }, selected)).toBeNull(); + } +}); it("does not add Anthropic cumulative output counters and prices cache writes by TTL", () => { const stream = new ProviderUsageAccumulator("anthropic"); stream.observe({ type: "message_start", message: { id: "message", model: "fixture", usage: { input_tokens: 10, output_tokens: 1, cache_read_input_tokens: 20, cache_creation_input_tokens: 30, cache_creation: { ephemeral_5m_input_tokens: 20, ephemeral_1h_input_tokens: 10 } } } }); @@ -111,6 +125,29 @@ it("prefers provider TTL splits and never repairs malformed or partial provider expect(estimateProviderCost(receipt, policyPrice)).toBeNull(); } }); +it.each([ + { name: "exact reviewed mapping", mapped: true, expected: "0.077600000" }, + { name: "no mapping", mapped: false, expected: null }, + { name: "unlisted snapshot", mapped: true, responseModel: "unlisted-snapshot", expected: null }, + { name: "wrong price provider", mapped: true, provider: "anthropic" as const, expected: null }, + { name: "wrong price request model", mapped: true, model: "another-request", expected: null }, + { name: "unqualified cache policy", mapped: true, request: { prompt_cache_options: { mode: "explicit", ttl: "12h" } }, expected: null }, + { name: "missing write counter", mapped: true, details: { cache_write_tokens: undefined }, expected: null }, + { name: "malformed TTL counters", mapped: true, details: { cache_write_tokens_details: null }, expected: null }, +])("uses a reviewed snapshot for cache attribution only with $name", test => { + const snapshot = "gpt-5.6-sol-reviewed-snapshot"; + const selected: EvalModelPrice = { ...policyPrice, model: test.model ?? policyPrice.model, provider: test.provider ?? "openai", + ...(test.mapped ? { responseModels: [snapshot] } : {}) }; + const stream = new ProviderUsageAccumulator("openai", { model: policyPrice.model, input: "hello", + prompt_cache_options: { mode: "implicit", ttl: "12h" }, ...test.request }, selected); + stream.observe({ type: "response.completed", response: { model: test.responseModel ?? snapshot, usage: { + input_tokens: 12_000, input_tokens_details: { cached_tokens: 4000, cache_write_tokens: 7000, ...test.details }, output_tokens: 100, + } } }); + const receipt = stream.receipt(); + expect(estimateProviderCost(receipt, selected)).toBe(test.expected); + expect(receipt.cacheWriteAttribution).toEqual(test.expected === null ? undefined + : { source: "request_policy", version: "openai-prompt-cache-2026-10-20", ttl: "12h" }); +}); it.each([ ["openai", { input_tokens: 1001, output_tokens: 1 }, 1001], diff --git a/tests/runner-e2e/provider-usage.ts b/tests/runner-e2e/provider-usage.ts index 4b5b702f832..ce55bdaad78 100644 --- a/tests/runner-e2e/provider-usage.ts +++ b/tests/runner-e2e/provider-usage.ts @@ -1,5 +1,5 @@ import { usdToUnits } from "../../packages/shared/src/money.js"; -import type { EvalCostObservation } from "./cost-ledger.js"; +import { validEvalLedgerId, type EvalCostObservation } from "./cost-ledger.js"; type Tokens = NonNullable; type Row = Record; @@ -96,7 +96,8 @@ export class ProviderUsageAccumulator { private usage: Row = {}; private final = false; private readonly cachePolicy: ReturnType; - constructor(readonly provider: "openai" | "anthropic", request?: unknown) { + constructor(readonly provider: "openai" | "anthropic", request?: unknown, + private readonly price?: Pick) { this.cachePolicy = provider === "openai" ? openAiRequestCachePolicy(request) : undefined; } observe(event: unknown) { @@ -124,7 +125,9 @@ export class ProviderUsageAccumulator { receipt() { const receipt = normalizeProviderUsage(this.provider, { ...this.body, usage: this.usage }); const details = row(this.usage.input_tokens_details); - if (this.cachePolicy && receipt.model === this.cachePolicy.model && receipt.complete && receipt.tokens.cachedWrite! > 0 + const policyMatchesResponse = this.cachePolicy && (receipt.model === this.cachePolicy.model + || this.price?.provider === this.provider && this.price.model === this.cachePolicy.model && matchesPriceResponseModel(this.price, receipt.model)); + if (this.cachePolicy && policyMatchesResponse && receipt.complete && receipt.tokens.cachedWrite! > 0 && !("cache_write_tokens_details" in details)) { receipt.cacheWrite30m = this.cachePolicy.ttl === "30m" ? receipt.tokens.cachedWrite : 0; receipt.cacheWrite12h = this.cachePolicy.ttl === "12h" ? receipt.tokens.cachedWrite : 0; @@ -146,13 +149,29 @@ export interface EvalTokenRates { export interface EvalModelPrice extends EvalTokenRates { provider: "openai" | "anthropic"; model: string; + responseModels?: string[]; version: string; sourceUrl: string; longContext?: { aboveInputTokens: number; rates: EvalTokenRates }; } +export function validatePriceResponseModels(price: Pick): void { + const models = price.responseModels; + if (models === undefined) return; + if (!Array.isArray(models) || models.length < 1 || models.length > 16 || new Set(models).size !== models.length) { + throw new Error("Invalid eval price response model mapping"); + } + for (const model of models) { + if (!validEvalLedgerId(model) || model === price.model) throw new Error("Invalid eval price response model mapping"); + } +} + +export function matchesPriceResponseModel(price: Pick, model: unknown): boolean { + return typeof model === "string" && (model === price.model || Array.isArray(price.responseModels) && price.responseModels.includes(model)); +} + export function estimateProviderCost(receipt: ProviderUsageReceipt, price: EvalModelPrice): string | null { - if (!receipt.complete || receipt.model !== price.model || receipt.provider !== price.provider) return null; + if (!receipt.complete || !matchesPriceResponseModel(price, receipt.model) || receipt.provider !== price.provider) return null; const { tokens } = receipt; const totalInput = tokens.input! + tokens.cachedRead! + tokens.cachedWrite!; if (!Number.isSafeInteger(totalInput)) return null;