From d1d733956fb5cbf387aba616c925a5f41535ed89 Mon Sep 17 00:00:00 2001 From: Mark Yan Date: Sat, 22 Aug 2026 15:55:16 +0000 Subject: [PATCH] fix(deepinfra): derive tiered cache prices from the published multiplier DeepInfra publishes each tier's cached price twice: as the structured rate_per_input_token_cached multiplier, and restated as an absolute price inside the free-text `full` string. The tiered path read the string, the flat path read the multiplier, so one field had two derivations. They disagree on ByteDance/Seed-2.0-mini, whose string prices cached input above 128K at $0.2/Mtok - identical to its fresh input price - where the multiplier gives $0.04/Mtok. Every other published tier segment agrees. Also emit cache_write per tier. Both cache rates are multipliers on the segment's input price, so a tier that doubles input doubles them too; pricing cache_write only on the base tier would bill a long-context request at the short-context write rate. --- packages/core/src/sync/providers/deepinfra.ts | 39 +++++- packages/core/test/deepinfra.test.ts | 132 ++++++++++++++++++ .../models/ByteDance/Seed-2.0-mini.toml | 2 +- 3 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 packages/core/test/deepinfra.test.ts diff --git a/packages/core/src/sync/providers/deepinfra.ts b/packages/core/src/sync/providers/deepinfra.ts index 635edd62888..e1d0b49d2c9 100644 --- a/packages/core/src/sync/providers/deepinfra.ts +++ b/packages/core/src/sync/providers/deepinfra.ts @@ -148,6 +148,33 @@ function cacheCost(inputCost: number, rate: number | null | undefined) { return rate == null ? undefined : round(inputCost * rate); } +// Within one tier segment the cached price is published twice: as the structured +// `rate_per_input_token_cached` multiplier, and restated as an absolute price in +// the free-text `full` string. They normally agree, but the string is +// hand-written and can drift — `ByteDance/Seed-2.0-mini` currently reads +// `$0.2 in ... $0.2 cached` above 128K, i.e. cached input priced identically to +// fresh input, where the multiplier gives $0.04. +// +// Derive from the multiplier so that a segment's cached price is computed the +// same way here as it is on the flat-pricing path below, and warn when the +// string disagrees so a genuine rate change is visible rather than silent. +function segmentCacheRead( + modelName: string, + input: number, + stated: number | undefined, + rate: number | null | undefined, +) { + if (rate == null) return stated === undefined ? undefined : round(stated); + const derived = round(input * rate); + if (stated !== undefined && Math.abs(stated - derived) > 1e-9) { + console.warn( + `Deep Infra: ${modelName} states $${stated}/Mtok cached against a $${input}/Mtok input tier, ` + + `but rate_per_input_token_cached=${rate} implies $${derived}/Mtok; using $${derived}/Mtok.`, + ); + } + return derived; +} + function buildCost( model: DeepInfraModel, existing: ExistingModel | undefined, @@ -157,6 +184,7 @@ function buildCost( // No usable API price — leave the curated cost untouched. if (inputCost === undefined || outputCost === undefined) return existing?.cost; + const cacheReadRate = model.pricing?.rate_per_input_token_cached; const cacheWriteRate = model.pricing?.rate_per_input_token_cache_write; const tiered = parseTieredPricing(model.pricing?.full); @@ -166,13 +194,18 @@ function buildCost( input: round(base.input), output: round(base.output), reasoning: existing?.cost?.reasoning, - cache_read: base.cache_read === undefined ? undefined : round(base.cache_read), - cache_write: cacheWriteRate == null ? undefined : round(base.input * cacheWriteRate), + cache_read: segmentCacheRead(model.model_name, base.input, base.cache_read, cacheReadRate), + cache_write: cacheCost(base.input, cacheWriteRate), tiers: tiered.tiers.map((tier) => ({ tier: { type: "context" as const, size: tier.size }, input: round(tier.input), output: round(tier.output), - cache_read: tier.cache_read === undefined ? undefined : round(tier.cache_read), + cache_read: segmentCacheRead(model.model_name, tier.input, tier.cache_read, cacheReadRate), + // Both cache rates are multipliers on the *segment's* input price, so a + // context tier that doubles input doubles its cache prices too. Emitting + // cache_write only on the base tier would leave a long-context request + // priced at the short-context write rate. + cache_write: cacheCost(tier.input, cacheWriteRate), })), }; } diff --git a/packages/core/test/deepinfra.test.ts b/packages/core/test/deepinfra.test.ts new file mode 100644 index 00000000000..4082d866715 --- /dev/null +++ b/packages/core/test/deepinfra.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "bun:test"; + +import { + buildDeepInfraModel, + type DeepInfraModel, +} from "../src/sync/providers/deepinfra.js"; + +function deepinfraModel(overrides: Partial = {}): DeepInfraModel { + return { + model_name: "ByteDance/Seed-2.0-pro", + type: "text-generation", + tags: ["tools", "multimodal"], + pricing: { + type: "tokens", + cents_per_input_token: 5e-5, + cents_per_output_token: 3e-4, + rate_per_input_token_cached: 0.2, + full: "$0.50 in $3 out $0.10 cached <= 128K, $1 in $6 out $0.20 cached", + }, + max_tokens: 256_000, + ...overrides, + }; +} + +test("derives tiered cache_read from the cached-rate multiplier", () => { + const built = buildDeepInfraModel(deepinfraModel(), undefined); + + expect(built.cost).toMatchObject({ + input: 0.5, + output: 3, + cache_read: 0.1, + tiers: [ + { tier: { type: "context", size: 128_000 }, input: 1, output: 6, cache_read: 0.2 }, + ], + }); +}); + +test("prefers the cached-rate multiplier over a disagreeing price string", () => { + // Live payload for ByteDance/Seed-2.0-mini: the string restates the tier's + // cached price as `$0.2`, which is its *input* price — cached input would + // cost the same as fresh input. The multiplier gives $0.04, and agrees with + // the string on every other segment DeepInfra publishes. + const built = buildDeepInfraModel( + deepinfraModel({ + model_name: "ByteDance/Seed-2.0-mini", + pricing: { + cents_per_input_token: 1e-5, + cents_per_output_token: 4e-5, + rate_per_input_token_cached: 0.2, + full: "$0.10 in $0.40 out $0.02 cached <= 128K, $0.2 in $0.80 out $0.2 cached", + }, + }), + undefined, + ); + + expect(built.cost).toMatchObject({ + input: 0.1, + cache_read: 0.02, + tiers: [ + { tier: { type: "context", size: 128_000 }, input: 0.2, cache_read: 0.04 }, + ], + }); + // The cached price must stay below the fresh-input price it discounts. + const tier = built.cost?.tiers?.[0]; + expect(tier?.cache_read).toBeLessThan(tier!.input!); +}); + +test("keeps the stated cached price when no multiplier is published", () => { + const built = buildDeepInfraModel( + deepinfraModel({ + pricing: { + cents_per_input_token: 5e-5, + cents_per_output_token: 3e-4, + rate_per_input_token_cached: null, + full: "$0.50 in $3 out $0.10 cached <= 128K, $1 in $6 out $0.20 cached", + }, + }), + undefined, + ); + + expect(built.cost).toMatchObject({ + cache_read: 0.1, + tiers: [{ tier: { type: "context", size: 128_000 }, cache_read: 0.2 }], + }); +}); + +test("scales cache_write per tier instead of only pricing the base tier", () => { + const built = buildDeepInfraModel( + deepinfraModel({ + pricing: { + cents_per_input_token: 5e-5, + cents_per_output_token: 3e-4, + rate_per_input_token_cached: 0.2, + rate_per_input_token_cache_write: 1.25, + full: "$0.50 in $3 out $0.10 cached <= 128K, $1 in $6 out $0.20 cached", + }, + }), + undefined, + ); + + expect(built.cost).toMatchObject({ + cache_write: 0.625, + tiers: [{ tier: { type: "context", size: 128_000 }, cache_write: 1.25 }], + }); +}); + +test("carries every band of a three-tier price string", () => { + const built = buildDeepInfraModel( + deepinfraModel({ + model_name: "Qwen/Qwen3.7-Max", + pricing: { + cents_per_input_token: 2.5e-4, + cents_per_output_token: 7.5e-4, + rate_per_input_token_cached: 0.2, + full: + "$2.50 in $7.50 out $0.50 cached <= 32K, $5.0 in $15 out $1.0 cached <= 128K, " + + "$6.25 in $18.50 out $1.25 cached > 128K", + }, + }), + undefined, + ); + + expect(built.cost).toMatchObject({ + input: 2.5, + output: 7.5, + cache_read: 0.5, + tiers: [ + { tier: { type: "context", size: 32_000 }, input: 5, output: 15, cache_read: 1 }, + { tier: { type: "context", size: 128_000 }, input: 6.25, output: 18.5, cache_read: 1.25 }, + ], + }); +}); diff --git a/providers/deepinfra/models/ByteDance/Seed-2.0-mini.toml b/providers/deepinfra/models/ByteDance/Seed-2.0-mini.toml index e148079c5f4..19a244f2ce2 100644 --- a/providers/deepinfra/models/ByteDance/Seed-2.0-mini.toml +++ b/providers/deepinfra/models/ByteDance/Seed-2.0-mini.toml @@ -11,7 +11,7 @@ cache_read = 0.02 tier = { type = "context", size = 128_000 } input = 0.2 output = 0.8 -cache_read = 0.2 +cache_read = 0.04 [modalities] input = ["text", "image"]