Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions packages/core/src/sync/providers/deepinfra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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),
})),
};
}
Expand Down
132 changes: 132 additions & 0 deletions packages/core/test/deepinfra.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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 },
],
});
});
2 changes: 1 addition & 1 deletion providers/deepinfra/models/ByteDance/Seed-2.0-mini.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading