🤖 feat: Add GPT-5.6 (Sol/Terra/Luna) OpenAI Models (#14206)

*  feat: Add GPT-5.6 (Sol/Terra/Luna) OpenAI models

Adds the GPT-5.6 family (GA 2026-07-09) across the context, output,
pricing, cache, premium, and default-model maps, mirroring gpt-5.5.

- gpt-5.6 (Sol alias), gpt-5.6-terra, gpt-5.6-luna
- 1.05M context / 128K output for all tiers
- Standard + long-context (>272K input) tiered pricing and cache rates

* 🐛 fix: Bill GPT-5.6 cache writes at documented 1.25x input surcharge

OpenAI prices GPT-5.6 cache writes above the base input rate (Sol $6.25,
Terra $3.125, Luna $1.25 per 1M vs $5/$2.50/$1 input). Correct the
cacheTokenValues write rates so explicit prompt-caching usage is billed
and reported accurately, and lock the surcharge with a test.

*  feat: Expose GPT-5.6 max reasoning effort + long-context cache premium

Folds in the two deferred Codex findings:

1. Add `max` to the OpenAI `ReasoningEffort` enum and the reasoning_effort
   parameter options/labels so GPT-5.6 (Sol/Terra/Luna) can request its
   documented highest reasoning setting. Backend passthrough and zod
   validation pick it up via the nativeEnum schema.

2. Apply the long-context (>272K input) premium to cache tokens. Adds
   `premiumCacheTokenValues` + `getPremiumCacheRate`, threads
   `inputTokenCount` into `getCacheMultiplier`, and wires it through both
   structured-spend paths. Covers the gpt-5.4/5.5/5.6 family whose cache
   write/read previously stayed at flat base rates on long-context calls.

* 🐛 fix: Bill GPT-5.6 cache writes + map max effort for OpenRouter Claude

Addresses Codex round-3 findings:

1. (P1) splitUsage only read `cache_creation`/`cache_creation_input_tokens`,
   so OpenAI GPT-5.6's `cache_write_tokens` fell into inputOnly and billed at
   the input rate instead of the 1.25x write rate. Extend UsageMetadata and
   single-source the cache-creation read to also recognize `cache_write_tokens`
   (nested and top-level).

2. (P2) `max` was exposed via OpenRouter (spreads OpenAI settings) but the
   adaptive-Claude verbosity map had no `max`, so it was silently dropped. Map
   max -> 'max' verbosity.

* 🐛 fix: Forward GPT-5.6 cache_write_tokens into emitted usage

Local Codex review (P1): the cache-write fix reached balance billing
(splitUsage/getCacheCreationTokens) but not the emitted-usage pipeline.
ModelEndHandler built the emitted event's cache_creation from only
cache_creation/cache_creation_input_tokens, so GPT-5.6 cache_write_tokens
were dropped and aggregateEmittedUsage classified them as ordinary input —
displayed/persisted cost undercounted and disagreed with the balance charge.
Fold cache_write_tokens (nested + top-level) into the emitted cache_creation.
This commit is contained in:
Danny Avila 2026-07-12 07:52:59 -04:00 committed by GitHub
parent 6e8f44d72b
commit b753da163e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 320 additions and 18 deletions

View file

@ -131,10 +131,14 @@ class ModelEndHandler {
this.collectedUsage.push(taggedUsage);
if (this.emitUsage) {
/** Normalize Anthropic/Bedrock-style top-level cache fields into details */
/** Normalize Anthropic/Bedrock top-level and OpenAI GPT-5.6
* `cache_write_tokens` cache fields into details so the emitted/persisted
* usage cost matches what billing charges (getCacheCreationTokens). */
const cache_creation =
taggedUsage.input_token_details?.cache_creation ??
taggedUsage.cache_creation_input_tokens;
taggedUsage.input_token_details?.cache_write_tokens ??
taggedUsage.cache_creation_input_tokens ??
taggedUsage.cache_write_tokens;
const cache_read =
taggedUsage.input_token_details?.cache_read ?? taggedUsage.cache_read_input_tokens;
try {

View file

@ -258,6 +258,20 @@ describe('getModelMaxTokens', () => {
);
});
test('should return correct tokens for gpt-5.6 matches', () => {
expect(getModelMaxTokens('gpt-5.6')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.6']);
expect(getModelMaxTokens('gpt-5.6-sol')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5.6']);
expect(getModelMaxTokens('openai/gpt-5.6')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5.6'],
);
expect(getModelMaxTokens('gpt-5.6-terra')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5.6-terra'],
);
expect(getModelMaxTokens('gpt-5.6-luna-2026-07-09')).toBe(
maxTokensMap[EModelEndpoint.openAI]['gpt-5.6-luna'],
);
});
test('should return correct tokens for Anthropic models', () => {
const models = [
'claude-2.1',
@ -577,6 +591,9 @@ describe('getModelMaxTokens', () => {
'gpt-5.4-pro',
'gpt-5.5',
'gpt-5.5-pro',
'gpt-5.6',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5-mini',
'gpt-5-nano',
'gpt-5-pro',

View file

@ -17,6 +17,7 @@ interface GetCacheMultiplierParams {
cacheType: 'write' | 'read';
model?: string;
endpointTokenConfig?: EndpointTokenConfig;
inputTokenCount?: number;
}
export interface PricingFns {
@ -124,11 +125,19 @@ function calculateStructuredTokenValue(
inputTokenCount,
});
const writeMultiplier =
pricing.getCacheMultiplier({ cacheType: 'write', model, endpointTokenConfig }) ??
inputMultiplier;
pricing.getCacheMultiplier({
cacheType: 'write',
model,
endpointTokenConfig,
inputTokenCount,
}) ?? inputMultiplier;
const readMultiplier =
pricing.getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig }) ??
inputMultiplier;
pricing.getCacheMultiplier({
cacheType: 'read',
model,
endpointTokenConfig,
inputTokenCount,
}) ?? inputMultiplier;
const inputAbs = Math.abs(txData.inputTokens ?? 0);
const writeAbs = Math.abs(txData.writeTokens ?? 0);

View file

@ -1609,6 +1609,41 @@ describe('computeUsageCostUSD', () => {
);
expect(cost).toBeCloseTo((1000 * 3 + 2000 * 3.75 + 10000 * 0.3 + 500 * 15) / 1e6);
});
it('routes nested cache_write_tokens to the write bucket, not the input rate', () => {
/** OpenAI GPT-5.6 reports cache writes as `cache_write_tokens`. Those 2000
* tokens must bill at the write rate (3.75), not fold into input (3);
* inputOnly = 13000 - 2000 write - 10000 read = 1000. */
const cost = computeUsageCostUSD(
{
input_tokens: 13000,
output_tokens: 500,
model: 'gpt-5.6',
provider: 'openAI',
input_token_details: { cache_read: 10000, cache_write_tokens: 2000 },
},
pricing,
);
expect(cost).toBeCloseTo((1000 * 3 + 2000 * 3.75 + 10000 * 0.3 + 500 * 15) / 1e6);
});
it('routes top-level cache_write_tokens to the write bucket alongside the premium input tier', () => {
/** Top-level `cache_write_tokens` (Chat/Responses flattened shape) is also
* recognized; inputOnly = 280000 - 5000 - 15000 = 260000, above the premium
* threshold so input/completion price at the premium tier (8 / 40). */
const cost = computeUsageCostUSD(
{
input_tokens: 280000,
output_tokens: 500,
model: 'gpt-5.6',
provider: 'openAI',
input_token_details: { cache_read: 15000 },
cache_write_tokens: 5000,
},
pricing,
);
expect(cost).toBeCloseTo((260000 * 8 + 5000 * 3.75 + 15000 * 0.3 + 500 * 40) / 1e6);
});
});
describe('aggregateEmittedUsage', () => {

View file

@ -33,6 +33,22 @@ type SpendStructuredTokensFn = (
tokenUsage: StructuredTokenUsage,
) => Promise<unknown>;
/**
* Cache-creation (write) tokens across provider shapes: langchain's
* `input_token_details.cache_creation`, Anthropic's `cache_creation_input_tokens`,
* and OpenAI GPT-5.6+'s `cache_write_tokens` (nested or top-level). Kept in one
* place so the completion-token and billing splits never diverge.
*/
function getCacheCreationTokens(usage: UsageMetadata): number {
return (
Number(usage.input_token_details?.cache_creation) ||
Number(usage.input_token_details?.cache_write_tokens) ||
Number(usage.cache_creation_input_tokens) ||
Number(usage.cache_write_tokens) ||
0
);
}
/**
* Resolves `completionTokens` for billing, repairing providers whose
* `usage_metadata.output_tokens` undercounts.
@ -70,10 +86,7 @@ function resolveCompletionTokens(usage: UsageMetadata): number {
// Subset providers fold cache into input_tokens, so their adjustment is 0.
const cacheRead =
Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0;
const cacheCreation =
Number(usage.input_token_details?.cache_creation) ||
Number(usage.cache_creation_input_tokens) ||
0;
const cacheCreation = getCacheCreationTokens(usage);
const cacheAdjustment = inputTokensIncludesCache(usage.provider) ? 0 : cacheRead + cacheCreation;
if (total > input + output + cacheAdjustment) {
@ -94,10 +107,7 @@ interface SplitUsage {
}
function splitUsage(usage: UsageMetadata): SplitUsage {
const cacheCreation =
Number(usage.input_token_details?.cache_creation) ||
Number(usage.cache_creation_input_tokens) ||
0;
const cacheCreation = getCacheCreationTokens(usage);
const cacheRead =
Number(usage.input_token_details?.cache_read) || Number(usage.cache_read_input_tokens) || 0;
const rawInput = Number(usage.input_tokens) || 0;

View file

@ -833,6 +833,23 @@ describe('getOpenAILLMConfig', () => {
expect(result.llmConfig).toHaveProperty('verbosity', 'max');
});
it('should map OpenRouter adaptive Claude max effort to max verbosity', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
useOpenRouter: true,
modelOptions: {
model: 'anthropic/claude-sonnet-4.6',
reasoning_effort: 'max' as ReasoningEffort,
},
});
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
enabled: true,
});
expect(result.llmConfig).toHaveProperty('verbosity', 'max');
});
it('should preserve extra-high OpenRouter verbosity for future adaptive Claude models', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',

View file

@ -168,6 +168,7 @@ const openRouterAnthropicVerbosityByEffort: Record<
medium: 'medium',
high: 'high',
xhigh: 'xhigh',
max: 'max',
};
function isStringVerbosity(value: unknown): value is string {

View file

@ -202,12 +202,20 @@ export interface UsageMetadata {
cache_creation?: number;
/** Tokens read from cache */
cache_read?: number;
/** OpenAI GPT-5.6+ cache-write tokens (billed above the input rate) */
cache_write_tokens?: number;
};
/**
* Anthropic-style cache creation tokens.
* Present for Claude models. Mutually exclusive with input_token_details.
*/
cache_creation_input_tokens?: number;
/**
* OpenAI GPT-5.6+ cache-write tokens, reported at the top level of
* `prompt_tokens_details`/`input_tokens_details`. Distinct from cached
* (read) tokens and billed at a premium over the input rate.
*/
cache_write_tokens?: number;
/**
* Anthropic-style cache read tokens.
* Present for Claude models. Mutually exclusive with input_token_details.

View file

@ -31,3 +31,19 @@ describe('getModelMaxOutputTokens partial-override fallback', () => {
expect(fallback).toBeGreaterThan(0);
});
});
describe('gpt-5.6 tiers', () => {
it('resolves 1.05M context and 128K output for every tier and the sol alias', () => {
for (const model of ['gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
expect(getModelMaxTokens(model, EModelEndpoint.openAI)).toBe(1050000);
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(128000);
}
});
it('matches the longest tier key over the shorter gpt-5 pattern', () => {
expect(getModelMaxTokens('openai/gpt-5.6-terra-2026-07-09', EModelEndpoint.openAI)).toBe(
1050000,
);
expect(getModelMaxTokens('gpt-5', EModelEndpoint.openAI)).toBe(400000);
});
});

View file

@ -61,6 +61,9 @@ const openAIModels = {
'gpt-5.4-nano': 400000,
'gpt-5.5': 1050000,
'gpt-5.5-pro': 1050000,
'gpt-5.6': 1050000,
'gpt-5.6-terra': 1050000,
'gpt-5.6-luna': 1050000,
'chat-latest': 400000,
'gpt-5-mini': 400000,
'gpt-5-nano': 400000,
@ -433,6 +436,9 @@ export const modelMaxOutputs = {
'gpt-5.4-nano': 128000,
'gpt-5.5': 128000,
'gpt-5.5-pro': 128000,
'gpt-5.6': 128000,
'gpt-5.6-terra': 128000,
'gpt-5.6-luna': 128000,
'chat-latest': 128000,
'gpt-5-mini': 128000,
'gpt-5-nano': 128000,

View file

@ -2011,6 +2011,9 @@ export const alternateName = {
};
const sharedOpenAIModels = [
'gpt-5.6',
'gpt-5.6-terra',
'gpt-5.6-luna',
'gpt-5.5',
'gpt-5.5-pro',
'chat-latest',

View file

@ -244,6 +244,7 @@ const openAIParams: Record<string, SettingDefinition> = {
ReasoningEffort.medium,
ReasoningEffort.high,
ReasoningEffort.xhigh,
ReasoningEffort.max,
],
enumMappings: {
[ReasoningEffort.unset]: 'com_ui_auto',
@ -253,6 +254,7 @@ const openAIParams: Record<string, SettingDefinition> = {
[ReasoningEffort.medium]: 'com_ui_medium',
[ReasoningEffort.high]: 'com_ui_high',
[ReasoningEffort.xhigh]: 'com_ui_xhigh',
[ReasoningEffort.max]: 'com_ui_max',
},
optionType: 'model',
columnSpan: 4,

View file

@ -1,9 +1,11 @@
import {
AnthropicEffort,
ReasoningEffort,
googleSettings,
anthropicSettings,
compactGoogleSchema,
eAnthropicEffortSchema,
eReasoningEffortSchema,
} from './schemas';
describe('anthropicSettings', () => {
@ -567,3 +569,20 @@ describe('AnthropicEffort', () => {
expect(() => eAnthropicEffortSchema.parse('ultra')).toThrow();
});
});
describe('ReasoningEffort', () => {
it('exposes max as the highest OpenAI reasoning effort, after xhigh', () => {
expect(ReasoningEffort.max).toBe('max');
const keys = Object.keys(ReasoningEffort);
expect(keys.indexOf('max')).toBeGreaterThan(keys.indexOf('xhigh'));
});
it('accepts max through the zod schema', () => {
expect(eReasoningEffortSchema.parse('max')).toBe('max');
expect(eReasoningEffortSchema.parse(ReasoningEffort.max)).toBe('max');
});
it('still rejects unknown effort values', () => {
expect(() => eReasoningEffortSchema.parse('ultra')).toThrow();
});
});

View file

@ -202,6 +202,7 @@ export enum ReasoningEffort {
medium = 'medium',
high = 'high',
xhigh = 'xhigh',
max = 'max',
}
export enum ReasoningParameterFormat {

View file

@ -17,6 +17,7 @@ type CacheMultiplierParams = {
cacheType?: 'write' | 'read';
model?: string;
endpointTokenConfig?: Record<string, Record<string, number>>;
inputTokenCount?: number;
};
/** Fields read/written by the internal token value calculators */
@ -142,10 +143,15 @@ export function createTransactionMethods(
cacheType: 'write',
model,
endpointTokenConfig: etConfig,
inputTokenCount,
}) ?? inputMultiplier;
const readMultiplier =
txMethods.getCacheMultiplier({ cacheType: 'read', model, endpointTokenConfig: etConfig }) ??
inputMultiplier;
txMethods.getCacheMultiplier({
cacheType: 'read',
model,
endpointTokenConfig: etConfig,
inputTokenCount,
}) ?? inputMultiplier;
txn.rateDetail = {
input: inputMultiplier,

View file

@ -5,6 +5,7 @@ import {
tokenValues,
cacheTokenValues,
premiumTokenValues,
premiumCacheTokenValues,
defaultRate,
} from './tx';
import { matchModelName, findMatchingPattern } from './test-helpers';
@ -449,6 +450,26 @@ describe('getMultiplier', () => {
);
});
it('should return the correct multiplier for gpt-5.6 tiers', () => {
for (const model of ['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
expect(getValueKey(model)).toBe(model);
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[model].prompt);
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(tokenValues[model].completion);
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(cacheTokenValues[model].write);
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(cacheTokenValues[model].read);
}
expect(getValueKey('gpt-5.6-sol')).toBe('gpt-5.6');
expect(getMultiplier({ model: 'openai/gpt-5.6-terra', tokenType: 'completion' })).toBe(
tokenValues['gpt-5.6-terra'].completion,
);
});
it('should bill gpt-5.6 cache writes at the documented 1.25x input surcharge', () => {
for (const model of ['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna']) {
expect(cacheTokenValues[model].write).toBeCloseTo(tokenValues[model].prompt * 1.25);
}
});
it('should return the correct multiplier for gpt-4o', () => {
const valueKey = getValueKey('gpt-4o-2024-08-06');
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt);
@ -2688,5 +2709,74 @@ describe('Premium Token Pricing', () => {
});
});
describe('GPT-5.6 Long-Context Premium Pricing', () => {
const tiers = ['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna'];
it('should define a premium entry above standard rates for every tier', () => {
for (const model of tiers) {
const premiumEntry = premiumTokenValues[model];
expect(premiumEntry).toBeDefined();
expect(premiumEntry.threshold).toBe(272000);
expect(premiumEntry.prompt).toBe(tokenValues[model].prompt * 2);
expect(premiumEntry.completion).toBe(tokenValues[model].completion * 1.5);
}
});
it('should bill standard rates at or below threshold and premium rates above', () => {
for (const model of tiers) {
const { threshold, prompt, completion } = premiumTokenValues[model];
expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: threshold })).toBe(
tokenValues[model].prompt,
);
expect(getMultiplier({ model, tokenType: 'prompt', inputTokenCount: threshold + 1 })).toBe(
prompt,
);
expect(
getMultiplier({ model, tokenType: 'completion', inputTokenCount: threshold + 1 }),
).toBe(completion);
}
});
});
describe('Long-Context Premium Cache Pricing', () => {
const premiumCacheModels = Object.keys(premiumCacheTokenValues);
it('should scale cache write/read by the same long-context multiplier as input', () => {
for (const model of premiumCacheModels) {
const inputRatio = premiumTokenValues[model].prompt / tokenValues[model].prompt;
expect(premiumCacheTokenValues[model].write).toBeCloseTo(
cacheTokenValues[model].write * inputRatio,
);
expect(premiumCacheTokenValues[model].read).toBeCloseTo(
cacheTokenValues[model].read * inputRatio,
);
}
});
it('should return premium cache rates above threshold and standard rates at/below', () => {
for (const model of premiumCacheModels) {
const { threshold } = premiumCacheTokenValues[model];
for (const cacheType of ['write', 'read'] as const) {
expect(getCacheMultiplier({ model, cacheType, inputTokenCount: threshold + 1 })).toBe(
premiumCacheTokenValues[model][cacheType],
);
expect(getCacheMultiplier({ model, cacheType, inputTokenCount: threshold })).toBe(
cacheTokenValues[model][cacheType],
);
expect(getCacheMultiplier({ model, cacheType })).toBe(cacheTokenValues[model][cacheType]);
}
}
});
it('should not apply premium cache rates to models without a premium cache entry', () => {
for (const model of ['gpt-5-mini', 'gpt-5.4-mini', 'gpt-4o']) {
expect(premiumCacheTokenValues[model]).toBeUndefined();
expect(getCacheMultiplier({ model, cacheType: 'read', inputTokenCount: 5_000_000 })).toBe(
getCacheMultiplier({ model, cacheType: 'read' }),
);
}
});
});
// Cross-package sync validation tests (tokens.ts ↔ tx.ts) moved to
// packages/api tests since they require maxTokensMap from @librechat/api.

View file

@ -134,6 +134,9 @@ export const tokenValues: Record<string, { prompt: number; completion: number }>
'gpt-5.4-nano': { prompt: 0.2, completion: 1.25 },
'gpt-5.5': { prompt: 5, completion: 30 },
'gpt-5.5-pro': { prompt: 30, completion: 180 },
'gpt-5.6': { prompt: 5, completion: 30 },
'gpt-5.6-terra': { prompt: 2.5, completion: 15 },
'gpt-5.6-luna': { prompt: 1, completion: 6 },
'chat-latest': { prompt: 5, completion: 30 },
'gpt-5-chat-latest': { prompt: 1.25, completion: 10 },
'gpt-5.1-chat-latest': { prompt: 1.25, completion: 10 },
@ -329,6 +332,9 @@ export const cacheTokenValues: Record<string, { write: number; read: number }> =
'gpt-5.4-mini': { write: 0.75, read: 0.075 },
'gpt-5.4-nano': { write: 0.2, read: 0.02 },
'gpt-5.5': { write: 5, read: 0.5 },
'gpt-5.6': { write: 6.25, read: 0.5 },
'gpt-5.6-terra': { write: 3.125, read: 0.25 },
'gpt-5.6-luna': { write: 1.25, read: 0.1 },
'chat-latest': { write: 5, read: 0.5 },
'gpt-5-chat-latest': { write: 1.25, read: 0.125 },
'gpt-5.1-chat-latest': { write: 1.25, read: 0.125 },
@ -376,6 +382,26 @@ export const premiumTokenValues: Record<
'gpt-5.4-pro': { threshold: 272000, prompt: 60, completion: 270 },
'gpt-5.5': { threshold: 272000, prompt: 10, completion: 45 },
'gpt-5.5-pro': { threshold: 272000, prompt: 60, completion: 270 },
'gpt-5.6': { threshold: 272000, prompt: 10, completion: 45 },
'gpt-5.6-terra': { threshold: 272000, prompt: 5, completion: 22.5 },
'gpt-5.6-luna': { threshold: 272000, prompt: 2, completion: 9 },
};
/**
* Premium (tiered) cache pricing for models whose cache rates change once the
* prompt crosses the long-context threshold. Cache write/read scale by the same
* multiplier the long-context tier applies to input (e.g. 2x for the gpt-5.x
* family), so these mirror `premiumTokenValues` on the cache dimension.
*/
export const premiumCacheTokenValues: Record<
string,
{ threshold: number; write: number; read: number }
> = {
'gpt-5.4': { threshold: 272000, write: 5, read: 0.5 },
'gpt-5.5': { threshold: 272000, write: 10, read: 1 },
'gpt-5.6': { threshold: 272000, write: 12.5, read: 1 },
'gpt-5.6-terra': { threshold: 272000, write: 6.25, read: 0.5 },
'gpt-5.6-luna': { threshold: 272000, write: 2.5, read: 0.2 },
};
export function createTxMethods(
@ -424,12 +450,14 @@ export function createTxMethods(
model,
endpoint,
endpointTokenConfig,
inputTokenCount,
}: {
valueKey?: string;
cacheType?: 'write' | 'read';
model?: string;
endpoint?: string;
endpointTokenConfig?: Record<string, Record<string, number>>;
inputTokenCount?: number | null;
}) => number | null;
defaultRate: number;
cacheTokenValues: Record<
@ -552,8 +580,28 @@ export function createTxMethods(
return tokenValues[valueKey]?.[tokenType] ?? defaultRate;
}
/**
* Checks if premium (tiered) cache pricing applies and returns the premium rate.
*/
function getPremiumCacheRate(
valueKey: string,
cacheType: 'write' | 'read',
inputTokenCount?: number | null,
): number | null {
if (inputTokenCount == null) {
return null;
}
const premiumEntry = premiumCacheTokenValues[valueKey];
if (!premiumEntry || inputTokenCount <= premiumEntry.threshold) {
return null;
}
return premiumEntry[cacheType] ?? null;
}
/**
* Retrieves the cache multiplier for a given value key and token type.
* When `inputTokenCount` crosses a model's long-context threshold, the
* premium cache rate applies instead of the standard one.
*/
function getCacheMultiplier({
valueKey,
@ -561,12 +609,14 @@ export function createTxMethods(
model,
endpoint,
endpointTokenConfig,
inputTokenCount,
}: {
valueKey?: string;
cacheType?: 'write' | 'read';
model?: string;
endpoint?: string;
endpointTokenConfig?: Record<string, Record<string, number>>;
inputTokenCount?: number | null;
}): number | null {
if (endpointTokenConfig && model) {
const modelConfig = endpointTokenConfig[model];
@ -578,7 +628,11 @@ export function createTxMethods(
}
if (valueKey && cacheType) {
return cacheTokenValues[valueKey]?.[cacheType] ?? null;
return (
getPremiumCacheRate(valueKey, cacheType, inputTokenCount) ??
cacheTokenValues[valueKey]?.[cacheType] ??
null
);
}
if (!cacheType || !model) {
@ -590,7 +644,11 @@ export function createTxMethods(
return null;
}
return cacheTokenValues[valueKey]?.[cacheType] ?? null;
return (
getPremiumCacheRate(valueKey, cacheType, inputTokenCount) ??
cacheTokenValues[valueKey]?.[cacheType] ??
null
);
}
return {