Skip to content
Merged
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
120 changes: 120 additions & 0 deletions apps/gateway/src/lib/costs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2296,3 +2296,123 @@ describe("shouldBillCancelledRequests", () => {
expect(shouldBillCancelledRequests()).toBe(false);
});
});

describe("peak / off-peak time-of-day pricing (DeepSeek)", () => {
beforeEach(() => {
vi.mocked(mockGetEffectiveDiscount).mockImplementation(async () => ({
discount: "0",
source: "none",
}));
});

afterEach(() => {
vi.useRealTimers();
});

const setTime = (iso: string) => {
vi.useFakeTimers({ shouldAdvanceTime: true });
vi.setSystemTime(new Date(iso));
};

it("bills off-peak rates outside the peak window", async () => {
setTime("2026-08-17T12:00:00Z"); // 12:00 UTC — off-peak

const flash = await calculateCosts(
"deepseek-v4-flash",
"deepseek",
null,
2_000_000,
1_000_000,
1_000_000,
);
expect(flash.inputCost).toBeCloseTo(0.22);
expect(flash.outputCost).toBeCloseTo(0.66);
expect(flash.cachedInputCost).toBeCloseTo(0.007);

const pro = await calculateCosts(
"deepseek-v4-pro",
"deepseek",
null,
2_000_000,
1_000_000,
1_000_000,
);
expect(pro.inputCost).toBeCloseTo(0.66);
expect(pro.outputCost).toBeCloseTo(1.98);
expect(pro.cachedInputCost).toBeCloseTo(0.022);
});

it("bills peak rates inside the peak window", async () => {
setTime("2026-08-17T02:00:00Z"); // 02:00 UTC — peak (01:00-04:00)

const flash = await calculateCosts(
"deepseek-v4-flash",
"deepseek",
null,
2_000_000,
1_000_000,
1_000_000,
);
expect(flash.inputCost).toBeCloseTo(0.44);
expect(flash.outputCost).toBeCloseTo(1.32);
expect(flash.cachedInputCost).toBeCloseTo(0.014);

const pro = await calculateCosts(
"deepseek-v4-pro",
"deepseek",
null,
2_000_000,
1_000_000,
1_000_000,
);
expect(pro.inputCost).toBeCloseTo(1.32);
expect(pro.outputCost).toBeCloseTo(3.96);
expect(pro.cachedInputCost).toBeCloseTo(0.044);
});

it("bills off-peak rates at the first window boundary (04:00 UTC)", async () => {
setTime("2026-08-17T04:00:00Z");

const flash = await calculateCosts(
"deepseek-v4-flash",
"deepseek",
null,
1_000_000,
0,
null,
);
expect(flash.inputCost).toBeCloseTo(0.22);
});

it("bills peak rates in the second window (06:00-10:00 UTC)", async () => {
setTime("2026-08-17T08:00:00Z");

const flash = await calculateCosts(
"deepseek-v4-flash",
"deepseek",
null,
1_000_000,
0,
null,
);
expect(flash.inputCost).toBeCloseTo(0.44);
});

it("bills base (regular flat) rates before effectiveAt", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
vi.setSystemTime(new Date("2026-08-13T08:00:00Z")); // before effectiveAt

const flash = await calculateCosts(
"deepseek-v4-flash",
"deepseek",
null,
2_000_000,
1_000_000,
1_000_000,
);
// The base (regular) flat prices apply before effectiveAt
expect(flash.inputCost).toBeCloseTo(0.14); // base: 0.14e-6 * 1M (prompt - cached = 1M)
expect(flash.outputCost).toBeCloseTo(0.28); // base: 0.28e-6 * 1M
expect(flash.cachedInputCost).toBeCloseTo(0.0028); // base: 0.0028e-6 * 1M
});
});
13 changes: 9 additions & 4 deletions apps/gateway/src/lib/costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type ToolCall,
expandAllProviderRegions,
getSupportedServiceTiers,
resolveTimeBasedPricing,
} from "@llmgateway/models";

/**
Expand Down Expand Up @@ -536,12 +537,16 @@ export async function calculateCosts(
// Set completion tokens to 0 if not available (but still calculate input costs)
calculatedCompletionTokens ??= 0;

// Get pricing based on token count (supports tiered pricing)
// Resolve peak/off-peak time-of-day pricing (DeepSeek first-party): use the
// peak rates while the current UTC hour is inside the mapping's peak window,
// the off-peak base rates otherwise. Tier selection below then overrides by
// token count for mappings that price by context length.
const timeBasedPricing = resolveTimeBasedPricing(providerInfo);
const pricing = getPricingForTokenCount(
providerInfo.pricingTiers,
providerInfo.inputPrice ?? "0",
providerInfo.outputPrice ?? "0",
providerInfo.cachedInputPrice,
timeBasedPricing.inputPrice,
timeBasedPricing.outputPrice,
timeBasedPricing.cachedInputPrice,
providerInfo.cacheReadInputPrice,
providerInfo.cacheWriteInputPrice,
providerInfo.cacheWriteInputPrice1h,
Expand Down
21 changes: 16 additions & 5 deletions packages/actions/src/get-cheapest-from-available-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type AvailableModelProvider,
type ModelWithPricing,
type ProviderModelMapping,
resolveTimeBasedPricing,
} from "@llmgateway/models";
import { randomFloat, randomInt } from "@llmgateway/shared/random";
import {
Expand Down Expand Up @@ -333,20 +334,29 @@ function getPerSecondBillingKeys(

export function getProviderSelectionPrice(
providerInfo:
| Pick<
| (Pick<
ProviderModelMapping,
| "inputPrice"
| "outputPrice"
| "perSecondPrice"
| "perImagePrice"
| "requestPrice"
>
> &
Partial<Pick<ProviderModelMapping, "peakPricing" | "cachedInputPrice">>)
| undefined,
videoPricing?: VideoPricingContext,
now: Date = new Date(),
): Decimal {
const inputPrice = providerInfo?.inputPrice;
const outputPrice = providerInfo?.outputPrice;
const requestPrice = providerInfo?.requestPrice;
// Resolve peak/off-peak time-of-day pricing (DeepSeek first-party): token
// rates vary by UTC hour once the mapping's peakPricing is effective, so
// routing must rank with the same rates billing uses.
const timeBasedPricing = providerInfo
? resolveTimeBasedPricing(providerInfo, now)
: undefined;
const inputPrice = timeBasedPricing?.inputPrice ?? providerInfo?.inputPrice;
const outputPrice =
timeBasedPricing?.outputPrice ?? providerInfo?.outputPrice;
const hasAnyTokenPrice =
inputPrice !== undefined || outputPrice !== undefined;
const hasPositiveTokenPrice =
Expand Down Expand Up @@ -399,7 +409,8 @@ type ProviderSelectionPriceInfo = AvailableModelProvider &
| "perSecondPrice"
| "perImagePrice"
| "requestPrice"
>;
> &
Partial<Pick<ProviderModelMapping, "peakPricing" | "cachedInputPrice">>;

export async function getDiscountedProviderSelectionPrice(
providerInfo: ProviderSelectionPriceInfo | undefined,
Expand Down
126 changes: 126 additions & 0 deletions packages/actions/src/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,132 @@ describe("getCheapestFromAvailableProviders", () => {
).toBe(0.375 / 1e6);
});

describe("time-of-day pricing in provider selection", () => {
// Mirrors the DeepSeek V4 Pro first-party mapping: peak 01:00-04:00 and
// 06:00-10:00 UTC at double the off-peak rates, base (regular) flat
// rates before effectiveAt.
const deepseekLikeMapping = {
inputPrice: "0.435e-6",
outputPrice: "0.87e-6",
cachedInputPrice: "0.003625e-6",
peakPricing: {
effectiveAt: "2026-08-16T16:00:00Z",
peak: {
inputPrice: "1.32e-6",
outputPrice: "3.96e-6",
cachedInputPrice: "0.044e-6",
},
offPeak: {
inputPrice: "0.66e-6",
outputPrice: "1.98e-6",
cachedInputPrice: "0.022e-6",
},
hoursUtc: [
[1, 4],
[6, 10],
] as [number, number][],
},
};

it("returns the peak average during peak hours", () => {
expect(
getProviderSelectionPrice(
deepseekLikeMapping,
undefined,
new Date("2026-08-17T02:00:00Z"),
).toNumber(),
).toBe((1.32e-6 + 3.96e-6) / 2);
});

it("returns the off-peak average outside peak hours", () => {
expect(
getProviderSelectionPrice(
deepseekLikeMapping,
undefined,
new Date("2026-08-17T12:00:00Z"),
).toNumber(),
).toBe((0.66e-6 + 1.98e-6) / 2);
});

it("returns the base average before effectiveAt", () => {
expect(
getProviderSelectionPrice(
deepseekLikeMapping,
undefined,
new Date("2026-08-15T12:00:00Z"),
).toNumber(),
).toBe((0.435e-6 + 0.87e-6) / 2);
});

it("returns the plain average for a mapping without peakPricing", () => {
expect(
getProviderSelectionPrice({
inputPrice: "0.5e-6",
outputPrice: "1.5e-6",
}).toNumber(),
).toBe(1e-6);
});

it("ranks with peak rates through full provider selection", async () => {
// The deepseek mapping's peak window covers every hour and
// effectiveAt is in the past, so the selection price is the peak
// rate regardless of wall clock. At the raw base rates deepseek's
// average (0.375e-6) beats openai's (0.75e-6); with peak rates
// applied deepseek's average rises to 1.125e-6 and openai wins —
// so selecting openai proves time-of-day pricing was applied.
const alwaysPeakModel = {
id: "always-peak-model",
name: "Always Peak Model",
family: "openai" as const,
providers: [
{
providerId: "openai" as const,
externalId: "always-peak",
inputPrice: "0.5e-6",
outputPrice: "1.0e-6",
streaming: true as const,
},
{
providerId: "deepseek" as const,
externalId: "always-peak",
inputPrice: "0.25e-6",
outputPrice: "0.5e-6",
peakPricing: {
effectiveAt: "2020-01-01T00:00:00Z",
peak: {
inputPrice: "0.75e-6",
outputPrice: "1.5e-6",
},
offPeak: {
inputPrice: "0.25e-6",
outputPrice: "0.5e-6",
},
hoursUtc: [[0, 24]] as [number, number][],
},
streaming: true as const,
},
],
};

const equalPriority = resolveRoutingConfig(
{ providerPriorities: { openai: 1, deepseek: 1 } },
buildProviderPriorityDefaults(),
);

const result = await getCheapestFromAvailableProviders(
alwaysPeakModel.providers,
alwaysPeakModel,
{ routingConfig: equalPriority },
);

expect(result?.provider.providerId).toBe("openai");
const deepseekScore = result?.metadata.providerScores.find(
(s) => s.providerId === "deepseek",
);
expect(deepseekScore?.price).toBe((0.75e-6 + 1.5e-6) / 2);
});
});

describe("cache support weighting", () => {
const cacheTestModel = {
id: "cache-test-model",
Expand Down
Loading
Loading