mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
✨ 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.
This commit is contained in:
parent
4a01b0e402
commit
da5c5194cf
7 changed files with 137 additions and 12 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ export enum ReasoningEffort {
|
|||
medium = 'medium',
|
||||
high = 'high',
|
||||
xhigh = 'xhigh',
|
||||
max = 'max',
|
||||
}
|
||||
|
||||
export enum ReasoningParameterFormat {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
tokenValues,
|
||||
cacheTokenValues,
|
||||
premiumTokenValues,
|
||||
premiumCacheTokenValues,
|
||||
defaultRate,
|
||||
} from './tx';
|
||||
import { matchModelName, findMatchingPattern } from './test-helpers';
|
||||
|
|
@ -464,10 +465,8 @@ describe('getMultiplier', () => {
|
|||
});
|
||||
|
||||
it('should bill gpt-5.6 cache writes at the documented 1.25x input surcharge', () => {
|
||||
const cacheWrites = { 'gpt-5.6': 6.25, 'gpt-5.6-terra': 3.125, 'gpt-5.6-luna': 1.25 };
|
||||
for (const model of ['gpt-5.6', 'gpt-5.6-terra', 'gpt-5.6-luna'] as const) {
|
||||
expect(cacheTokenValues[model].write).toBe(cacheWrites[model]);
|
||||
expect(cacheTokenValues[model].write).toBe(tokenValues[model].prompt * 1.25);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2739,5 +2738,45 @@ describe('GPT-5.6 Long-Context Premium Pricing', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -387,6 +387,23 @@ export const premiumTokenValues: Record<
|
|||
'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(
|
||||
_mongoose: typeof import('mongoose'),
|
||||
txDeps: TxDeps,
|
||||
|
|
@ -433,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<
|
||||
|
|
@ -561,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,
|
||||
|
|
@ -570,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];
|
||||
|
|
@ -587,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) {
|
||||
|
|
@ -599,7 +644,11 @@ export function createTxMethods(
|
|||
return null;
|
||||
}
|
||||
|
||||
return cacheTokenValues[valueKey]?.[cacheType] ?? null;
|
||||
return (
|
||||
getPremiumCacheRate(valueKey, cacheType, inputTokenCount) ??
|
||||
cacheTokenValues[valueKey]?.[cacheType] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue