mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-07 18:14:22 +01:00
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* feat: Implement new features for Claude Opus 4.6 model - Added support for tiered pricing based on input token count for the Claude Opus 4.6 model. - Updated token value calculations to include inputTokenCount for accurate pricing. - Enhanced transaction handling to apply premium rates when input tokens exceed defined thresholds. - Introduced comprehensive tests to validate pricing logic for both standard and premium rates across various scenarios. - Updated related utility functions and models to accommodate new pricing structure. This change improves the flexibility and accuracy of token pricing for the Claude Opus 4.6 model, ensuring users are charged appropriately based on their usage. * feat: Add effort field to conversation and preset schemas - Introduced a new optional `effort` field of type `String` in both the `IPreset` and `IConversation` interfaces. - Updated the `conversationPreset` schema to include the `effort` field, enhancing the data structure for better context management. * chore: Clean up unused variable and comments in initialize function * chore: update dependencies and SDK versions - Updated @anthropic-ai/sdk to version 0.73.0 in package.json and overrides. - Updated @anthropic-ai/vertex-sdk to version 0.14.3 in packages/api/package.json. - Updated @librechat/agents to version 3.1.34 in packages/api/package.json. - Refactored imports in packages/api/src/endpoints/anthropic/vertex.ts for consistency. * chore: remove postcss-loader from dependencies * feat: Bedrock model support for adaptive thinking configuration - Updated .env.example to include new Bedrock model IDs for Claude Opus 4.6. - Refactored bedrockInputParser to support adaptive thinking for Opus models, allowing for dynamic thinking configurations. - Introduced a new function to check model compatibility with adaptive thinking. - Added an optional `effort` field to the input schemas and updated related configurations. - Enhanced tests to validate the new adaptive thinking logic and model configurations. * feat: Add tests for Opus 4.6 adaptive thinking configuration * feat: Update model references for Opus 4.6 by removing version suffix * feat: Update @librechat/agents to version 3.1.35 in package.json and package-lock.json * chore: @librechat/agents to version 3.1.36 in package.json and package-lock.json * feat: Normalize inputTokenCount for spendTokens and enhance transaction handling - Introduced normalization for promptTokens to ensure inputTokenCount does not go negative. - Updated transaction logic to reflect normalized inputTokenCount in pricing calculations. - Added comprehensive tests to validate the new normalization logic and its impact on transaction rates for both standard and premium models. - Refactored related functions to improve clarity and maintainability of token value calculations. * chore: Simplify adaptive thinking configuration in helpers.ts - Removed unnecessary type casting for the thinking property in updatedOptions. - Ensured that adaptive thinking is directly assigned when conditions are met, improving code clarity. * refactor: Replace hard-coded token values with dynamic retrieval from maxTokensMap in model tests * fix: Ensure non-negative token values in spendTokens calculations - Updated token value retrieval to use Math.max for prompt and completion tokens, preventing negative values. - Enhanced clarity in token calculations for both prompt and completion transactions. * test: Add test for normalization of negative structured token values in spendStructuredTokens - Implemented a test to ensure that negative structured token values are normalized to zero during token spending. - Verified that the transaction rates remain consistent with the expected standard values after normalization. * refactor: Bedrock model support for adaptive thinking and context handling - Added tests for various alternate naming conventions of Claude models to validate adaptive thinking and context support. - Refactored `supportsAdaptiveThinking` and `supportsContext1m` functions to utilize new parsing methods for model version extraction. - Updated `bedrockInputParser` to handle effort configurations more effectively and strip unnecessary fields for non-adaptive models. - Improved handling of anthropic model configurations in the input parser. * fix: Improve token value retrieval in getMultiplier function - Updated the token value retrieval logic to use optional chaining for better safety against undefined values. - Added a test case to ensure that the function returns the default rate when the provided valueKey does not exist in tokenValues.
2194 lines
88 KiB
JavaScript
2194 lines
88 KiB
JavaScript
/** Note: No hard-coded values should be used in this file. */
|
|
const { maxTokensMap } = require('@librechat/api');
|
|
const { EModelEndpoint } = require('librechat-data-provider');
|
|
const {
|
|
defaultRate,
|
|
tokenValues,
|
|
getValueKey,
|
|
getMultiplier,
|
|
getPremiumRate,
|
|
cacheTokenValues,
|
|
getCacheMultiplier,
|
|
premiumTokenValues,
|
|
} = require('./tx');
|
|
|
|
describe('getValueKey', () => {
|
|
it('should return "16k" for model name containing "gpt-3.5-turbo-16k"', () => {
|
|
expect(getValueKey('gpt-3.5-turbo-16k-some-other-info')).toBe('16k');
|
|
});
|
|
|
|
it('should return "4k" for model name containing "gpt-3.5"', () => {
|
|
expect(getValueKey('gpt-3.5-some-other-info')).toBe('4k');
|
|
});
|
|
|
|
it('should return "32k" for model name containing "gpt-4-32k"', () => {
|
|
expect(getValueKey('gpt-4-32k-some-other-info')).toBe('32k');
|
|
});
|
|
|
|
it('should return "8k" for model name containing "gpt-4"', () => {
|
|
expect(getValueKey('gpt-4-some-other-info')).toBe('8k');
|
|
});
|
|
|
|
it('should return "gpt-5" for model name containing "gpt-5"', () => {
|
|
expect(getValueKey('gpt-5-some-other-info')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5');
|
|
expect(getValueKey('openai/gpt-5')).toBe('gpt-5');
|
|
expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-turbo')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-0130')).toBe('gpt-5');
|
|
});
|
|
|
|
it('should return "gpt-5.1" for model name containing "gpt-5.1"', () => {
|
|
expect(getValueKey('gpt-5.1')).toBe('gpt-5.1');
|
|
expect(getValueKey('gpt-5.1-chat')).toBe('gpt-5.1');
|
|
expect(getValueKey('gpt-5.1-codex')).toBe('gpt-5.1');
|
|
expect(getValueKey('openai/gpt-5.1')).toBe('gpt-5.1');
|
|
});
|
|
|
|
it('should return "gpt-5.2" for model name containing "gpt-5.2"', () => {
|
|
expect(getValueKey('gpt-5.2')).toBe('gpt-5.2');
|
|
expect(getValueKey('gpt-5.2-chat')).toBe('gpt-5.2');
|
|
expect(getValueKey('openai/gpt-5.2')).toBe('gpt-5.2');
|
|
});
|
|
|
|
it('should return "gpt-3.5-turbo-1106" for model name containing "gpt-3.5-turbo-1106"', () => {
|
|
expect(getValueKey('gpt-3.5-turbo-1106-some-other-info')).toBe('gpt-3.5-turbo-1106');
|
|
expect(getValueKey('openai/gpt-3.5-turbo-1106')).toBe('gpt-3.5-turbo-1106');
|
|
expect(getValueKey('gpt-3.5-turbo-1106/openai')).toBe('gpt-3.5-turbo-1106');
|
|
});
|
|
|
|
it('should return "gpt-4-1106" for model name containing "gpt-4-1106"', () => {
|
|
expect(getValueKey('gpt-4-1106-some-other-info')).toBe('gpt-4-1106');
|
|
expect(getValueKey('gpt-4-1106-vision-preview')).toBe('gpt-4-1106');
|
|
expect(getValueKey('gpt-4-1106-preview')).toBe('gpt-4-1106');
|
|
expect(getValueKey('openai/gpt-4-1106')).toBe('gpt-4-1106');
|
|
expect(getValueKey('gpt-4-1106/openai/')).toBe('gpt-4-1106');
|
|
});
|
|
|
|
it('should return "gpt-4-1106" for model type of "gpt-4-1106"', () => {
|
|
expect(getValueKey('gpt-4-vision-preview')).toBe('gpt-4-1106');
|
|
expect(getValueKey('openai/gpt-4-1106')).toBe('gpt-4-1106');
|
|
expect(getValueKey('gpt-4-turbo')).toBe('gpt-4-1106');
|
|
expect(getValueKey('gpt-4-0125')).toBe('gpt-4-1106');
|
|
});
|
|
|
|
it('should return "gpt-4.5" for model type of "gpt-4.5"', () => {
|
|
expect(getValueKey('gpt-4.5-preview')).toBe('gpt-4.5');
|
|
expect(getValueKey('gpt-4.5-2024-08-06')).toBe('gpt-4.5');
|
|
expect(getValueKey('gpt-4.5-2024-08-06-0718')).toBe('gpt-4.5');
|
|
expect(getValueKey('openai/gpt-4.5')).toBe('gpt-4.5');
|
|
expect(getValueKey('openai/gpt-4.5-2024-08-06')).toBe('gpt-4.5');
|
|
expect(getValueKey('gpt-4.5-turbo')).toBe('gpt-4.5');
|
|
expect(getValueKey('gpt-4.5-0125')).toBe('gpt-4.5');
|
|
});
|
|
|
|
it('should return "gpt-4.1" for model type of "gpt-4.1"', () => {
|
|
expect(getValueKey('gpt-4.1-preview')).toBe('gpt-4.1');
|
|
expect(getValueKey('gpt-4.1-2024-08-06')).toBe('gpt-4.1');
|
|
expect(getValueKey('gpt-4.1-2024-08-06-0718')).toBe('gpt-4.1');
|
|
expect(getValueKey('openai/gpt-4.1')).toBe('gpt-4.1');
|
|
expect(getValueKey('openai/gpt-4.1-2024-08-06')).toBe('gpt-4.1');
|
|
expect(getValueKey('gpt-4.1-turbo')).toBe('gpt-4.1');
|
|
expect(getValueKey('gpt-4.1-0125')).toBe('gpt-4.1');
|
|
});
|
|
|
|
it('should return "gpt-4.1-mini" for model type of "gpt-4.1-mini"', () => {
|
|
expect(getValueKey('gpt-4.1-mini-preview')).toBe('gpt-4.1-mini');
|
|
expect(getValueKey('gpt-4.1-mini-2024-08-06')).toBe('gpt-4.1-mini');
|
|
expect(getValueKey('openai/gpt-4.1-mini')).toBe('gpt-4.1-mini');
|
|
expect(getValueKey('gpt-4.1-mini-0125')).toBe('gpt-4.1-mini');
|
|
});
|
|
|
|
it('should return "gpt-4.1-nano" for model type of "gpt-4.1-nano"', () => {
|
|
expect(getValueKey('gpt-4.1-nano-preview')).toBe('gpt-4.1-nano');
|
|
expect(getValueKey('gpt-4.1-nano-2024-08-06')).toBe('gpt-4.1-nano');
|
|
expect(getValueKey('openai/gpt-4.1-nano')).toBe('gpt-4.1-nano');
|
|
expect(getValueKey('gpt-4.1-nano-0125')).toBe('gpt-4.1-nano');
|
|
});
|
|
|
|
it('should return "gpt-5" for model type of "gpt-5"', () => {
|
|
expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5');
|
|
expect(getValueKey('openai/gpt-5')).toBe('gpt-5');
|
|
expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-turbo')).toBe('gpt-5');
|
|
expect(getValueKey('gpt-5-0130')).toBe('gpt-5');
|
|
});
|
|
|
|
it('should return "gpt-5-mini" for model type of "gpt-5-mini"', () => {
|
|
expect(getValueKey('gpt-5-mini-2025-01-30')).toBe('gpt-5-mini');
|
|
expect(getValueKey('openai/gpt-5-mini')).toBe('gpt-5-mini');
|
|
expect(getValueKey('gpt-5-mini-0130')).toBe('gpt-5-mini');
|
|
expect(getValueKey('gpt-5-mini-2025-01-30-0130')).toBe('gpt-5-mini');
|
|
});
|
|
|
|
it('should return "gpt-5-nano" for model type of "gpt-5-nano"', () => {
|
|
expect(getValueKey('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano');
|
|
expect(getValueKey('openai/gpt-5-nano')).toBe('gpt-5-nano');
|
|
expect(getValueKey('gpt-5-nano-0130')).toBe('gpt-5-nano');
|
|
expect(getValueKey('gpt-5-nano-2025-01-30-0130')).toBe('gpt-5-nano');
|
|
});
|
|
|
|
it('should return "gpt-5-pro" for model type of "gpt-5-pro"', () => {
|
|
expect(getValueKey('gpt-5-pro-2025-01-30')).toBe('gpt-5-pro');
|
|
expect(getValueKey('openai/gpt-5-pro')).toBe('gpt-5-pro');
|
|
expect(getValueKey('gpt-5-pro-0130')).toBe('gpt-5-pro');
|
|
expect(getValueKey('gpt-5-pro-2025-01-30-0130')).toBe('gpt-5-pro');
|
|
expect(getValueKey('gpt-5-pro-preview')).toBe('gpt-5-pro');
|
|
});
|
|
|
|
it('should return "gpt-4o" for model type of "gpt-4o"', () => {
|
|
expect(getValueKey('gpt-4o-2024-08-06')).toBe('gpt-4o');
|
|
expect(getValueKey('gpt-4o-2024-08-06-0718')).toBe('gpt-4o');
|
|
expect(getValueKey('openai/gpt-4o')).toBe('gpt-4o');
|
|
expect(getValueKey('openai/gpt-4o-2024-08-06')).toBe('gpt-4o');
|
|
expect(getValueKey('gpt-4o-turbo')).toBe('gpt-4o');
|
|
expect(getValueKey('gpt-4o-0125')).toBe('gpt-4o');
|
|
});
|
|
|
|
it('should return "gpt-4o-mini" for model type of "gpt-4o-mini"', () => {
|
|
expect(getValueKey('gpt-4o-mini-2024-07-18')).toBe('gpt-4o-mini');
|
|
expect(getValueKey('openai/gpt-4o-mini')).toBe('gpt-4o-mini');
|
|
expect(getValueKey('gpt-4o-mini-0718')).toBe('gpt-4o-mini');
|
|
expect(getValueKey('gpt-4o-2024-08-06-0718')).not.toBe('gpt-4o-mini');
|
|
});
|
|
|
|
it('should return "gpt-4o-2024-05-13" for model type of "gpt-4o-2024-05-13"', () => {
|
|
expect(getValueKey('gpt-4o-2024-05-13')).toBe('gpt-4o-2024-05-13');
|
|
expect(getValueKey('openai/gpt-4o-2024-05-13')).toBe('gpt-4o-2024-05-13');
|
|
expect(getValueKey('gpt-4o-2024-05-13-0718')).toBe('gpt-4o-2024-05-13');
|
|
expect(getValueKey('gpt-4o-2024-05-13-0718')).not.toBe('gpt-4o');
|
|
});
|
|
|
|
it('should return "gpt-4o" for model type of "chatgpt-4o"', () => {
|
|
expect(getValueKey('chatgpt-4o-latest')).toBe('gpt-4o');
|
|
expect(getValueKey('openai/chatgpt-4o-latest')).toBe('gpt-4o');
|
|
expect(getValueKey('chatgpt-4o-latest-0916')).toBe('gpt-4o');
|
|
expect(getValueKey('chatgpt-4o-latest-0718')).toBe('gpt-4o');
|
|
});
|
|
|
|
it('should return "claude-3-7-sonnet" for model type of "claude-3-7-sonnet-"', () => {
|
|
expect(getValueKey('claude-3-7-sonnet-20240620')).toBe('claude-3-7-sonnet');
|
|
expect(getValueKey('anthropic/claude-3-7-sonnet')).toBe('claude-3-7-sonnet');
|
|
expect(getValueKey('claude-3-7-sonnet-turbo')).toBe('claude-3-7-sonnet');
|
|
expect(getValueKey('claude-3-7-sonnet-0125')).toBe('claude-3-7-sonnet');
|
|
});
|
|
|
|
it('should return "claude-3.7-sonnet" for model type of "claude-3.7-sonnet-"', () => {
|
|
expect(getValueKey('claude-3.7-sonnet-20240620')).toBe('claude-3.7-sonnet');
|
|
expect(getValueKey('anthropic/claude-3.7-sonnet')).toBe('claude-3.7-sonnet');
|
|
expect(getValueKey('claude-3.7-sonnet-turbo')).toBe('claude-3.7-sonnet');
|
|
expect(getValueKey('claude-3.7-sonnet-0125')).toBe('claude-3.7-sonnet');
|
|
});
|
|
|
|
it('should return "claude-3-5-sonnet" for model type of "claude-3-5-sonnet-"', () => {
|
|
expect(getValueKey('claude-3-5-sonnet-20240620')).toBe('claude-3-5-sonnet');
|
|
expect(getValueKey('anthropic/claude-3-5-sonnet')).toBe('claude-3-5-sonnet');
|
|
expect(getValueKey('claude-3-5-sonnet-turbo')).toBe('claude-3-5-sonnet');
|
|
expect(getValueKey('claude-3-5-sonnet-0125')).toBe('claude-3-5-sonnet');
|
|
});
|
|
|
|
it('should return "claude-3.5-sonnet" for model type of "claude-3.5-sonnet-"', () => {
|
|
expect(getValueKey('claude-3.5-sonnet-20240620')).toBe('claude-3.5-sonnet');
|
|
expect(getValueKey('anthropic/claude-3.5-sonnet')).toBe('claude-3.5-sonnet');
|
|
expect(getValueKey('claude-3.5-sonnet-turbo')).toBe('claude-3.5-sonnet');
|
|
expect(getValueKey('claude-3.5-sonnet-0125')).toBe('claude-3.5-sonnet');
|
|
});
|
|
|
|
it('should return "claude-3-5-haiku" for model type of "claude-3-5-haiku-"', () => {
|
|
expect(getValueKey('claude-3-5-haiku-20240620')).toBe('claude-3-5-haiku');
|
|
expect(getValueKey('anthropic/claude-3-5-haiku')).toBe('claude-3-5-haiku');
|
|
expect(getValueKey('claude-3-5-haiku-turbo')).toBe('claude-3-5-haiku');
|
|
expect(getValueKey('claude-3-5-haiku-0125')).toBe('claude-3-5-haiku');
|
|
});
|
|
|
|
it('should return "claude-3.5-haiku" for model type of "claude-3.5-haiku-"', () => {
|
|
expect(getValueKey('claude-3.5-haiku-20240620')).toBe('claude-3.5-haiku');
|
|
expect(getValueKey('anthropic/claude-3.5-haiku')).toBe('claude-3.5-haiku');
|
|
expect(getValueKey('claude-3.5-haiku-turbo')).toBe('claude-3.5-haiku');
|
|
expect(getValueKey('claude-3.5-haiku-0125')).toBe('claude-3.5-haiku');
|
|
});
|
|
|
|
it('should return expected value keys for "gpt-oss" models', () => {
|
|
expect(getValueKey('openai/gpt-oss-120b')).toBe('gpt-oss-120b');
|
|
expect(getValueKey('openai/gpt-oss:120b')).toBe('gpt-oss:120b');
|
|
expect(getValueKey('openai/gpt-oss-570b')).toBe('gpt-oss');
|
|
expect(getValueKey('gpt-oss-570b')).toBe('gpt-oss');
|
|
expect(getValueKey('groq/gpt-oss-1080b')).toBe('gpt-oss');
|
|
expect(getValueKey('gpt-oss-20b')).toBe('gpt-oss-20b');
|
|
expect(getValueKey('oai/gpt-oss:20b')).toBe('gpt-oss:20b');
|
|
});
|
|
});
|
|
|
|
describe('getMultiplier', () => {
|
|
it('should return the correct multiplier for a given valueKey and tokenType', () => {
|
|
expect(getMultiplier({ valueKey: '8k', tokenType: 'prompt' })).toBe(tokenValues['8k'].prompt);
|
|
expect(getMultiplier({ valueKey: '8k', tokenType: 'completion' })).toBe(
|
|
tokenValues['8k'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct multipliers for o4-mini and o3', () => {
|
|
['o4-mini', 'o3'].forEach((model) => {
|
|
const prompt = getMultiplier({ model, tokenType: 'prompt' });
|
|
const completion = getMultiplier({ model, tokenType: 'completion' });
|
|
expect(prompt).toBe(tokenValues[model].prompt);
|
|
expect(completion).toBe(tokenValues[model].completion);
|
|
});
|
|
});
|
|
|
|
it('should return defaultRate if tokenType is provided but not found in tokenValues', () => {
|
|
expect(getMultiplier({ valueKey: '8k', tokenType: 'unknownType' })).toBe(defaultRate);
|
|
});
|
|
|
|
it('should return defaultRate if valueKey does not exist in tokenValues', () => {
|
|
expect(getMultiplier({ valueKey: 'non-existent-model', tokenType: 'prompt' })).toBe(
|
|
defaultRate,
|
|
);
|
|
expect(getMultiplier({ valueKey: 'non-existent-model', tokenType: 'completion' })).toBe(
|
|
defaultRate,
|
|
);
|
|
});
|
|
|
|
it('should derive the valueKey from the model if not provided', () => {
|
|
expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-4-some-other-info' })).toBe(
|
|
tokenValues['8k'].prompt,
|
|
);
|
|
});
|
|
|
|
it('should return 1 if only model or tokenType is missing', () => {
|
|
expect(getMultiplier({ tokenType: 'prompt' })).toBe(1);
|
|
expect(getMultiplier({ model: 'gpt-4-some-other-info' })).toBe(1);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-3.5-turbo-1106', () => {
|
|
expect(getMultiplier({ valueKey: 'gpt-3.5-turbo-1106', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-3.5-turbo-1106'].prompt,
|
|
);
|
|
expect(getMultiplier({ valueKey: 'gpt-3.5-turbo-1106', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-3.5-turbo-1106'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-4-1106', () => {
|
|
expect(getMultiplier({ valueKey: 'gpt-4-1106', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4-1106'].prompt,
|
|
);
|
|
expect(getMultiplier({ valueKey: 'gpt-4-1106', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5', () => {
|
|
const valueKey = getValueKey('gpt-5-2025-01-30');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5-mini', () => {
|
|
const valueKey = getValueKey('gpt-5-mini-2025-01-30');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-mini'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-mini'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5-mini-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5-mini', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-mini'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5-nano', () => {
|
|
const valueKey = getValueKey('gpt-5-nano-2025-01-30');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-nano'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-nano'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5-nano-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5-nano'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5-nano', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-nano'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5-pro', () => {
|
|
const valueKey = getValueKey('gpt-5-pro-2025-01-30');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-pro'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-pro'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5-pro-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5-pro'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5-pro', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5-pro'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5.1', () => {
|
|
expect(getMultiplier({ model: 'gpt-5.1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5.1'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5.1', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5.1'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5.1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5.1'].prompt,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-5.2', () => {
|
|
expect(getMultiplier({ model: 'gpt-5.2', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5.2'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-5.2', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-5.2'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-5.2', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-5.2'].prompt,
|
|
);
|
|
});
|
|
|
|
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);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4o'].completion,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).not.toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-4.1', () => {
|
|
const valueKey = getValueKey('gpt-4.1-2024-08-06');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4.1'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-4.1-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4.1'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-4.1', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-4.1-mini', () => {
|
|
const valueKey = getValueKey('gpt-4.1-mini-2024-08-06');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4.1-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1-mini'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-4.1-mini-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4.1-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-4.1-mini', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1-mini'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-4.1-nano', () => {
|
|
const valueKey = getValueKey('gpt-4.1-nano-2024-08-06');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4.1-nano'].prompt,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1-nano'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'gpt-4.1-nano-preview', tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4.1-nano'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'openai/gpt-4.1-nano', tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4.1-nano'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for gpt-4o-mini', () => {
|
|
const valueKey = getValueKey('gpt-4o-mini-2024-07-18');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(
|
|
tokenValues['gpt-4o-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4o-mini'].completion,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).not.toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return the correct multiplier for chatgpt-4o-latest', () => {
|
|
const valueKey = getValueKey('chatgpt-4o-latest');
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
|
tokenValues['gpt-4o'].completion,
|
|
);
|
|
expect(getMultiplier({ valueKey, tokenType: 'completion' })).not.toBe(
|
|
tokenValues['gpt-4o-mini'].completion,
|
|
);
|
|
});
|
|
|
|
it('should derive the valueKey from the model if not provided for new models', () => {
|
|
expect(
|
|
getMultiplier({ tokenType: 'prompt', model: 'gpt-3.5-turbo-1106-some-other-info' }),
|
|
).toBe(tokenValues['gpt-3.5-turbo-1106'].prompt);
|
|
expect(getMultiplier({ tokenType: 'completion', model: 'gpt-4-1106-vision-preview' })).toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
expect(getMultiplier({ tokenType: 'completion', model: 'gpt-4-0125-preview' })).toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
expect(getMultiplier({ tokenType: 'completion', model: 'gpt-4-turbo-vision-preview' })).toBe(
|
|
tokenValues['gpt-4-1106'].completion,
|
|
);
|
|
expect(getMultiplier({ tokenType: 'completion', model: 'gpt-3.5-turbo-0125' })).toBe(
|
|
tokenValues['gpt-3.5-turbo-0125'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return defaultRate if derived valueKey does not match any known patterns', () => {
|
|
expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-10-some-other-info' })).toBe(
|
|
defaultRate,
|
|
);
|
|
});
|
|
|
|
it('should return correct multipliers for GPT-OSS models', () => {
|
|
const models = ['gpt-oss-20b', 'gpt-oss-120b'];
|
|
models.forEach((key) => {
|
|
const expectedPrompt = tokenValues[key].prompt;
|
|
const expectedCompletion = tokenValues[key].completion;
|
|
expect(getMultiplier({ valueKey: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
|
expect(getMultiplier({ valueKey: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
|
expect(getMultiplier({ model: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
|
expect(getMultiplier({ model: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
|
});
|
|
});
|
|
|
|
it('should return correct multipliers for GLM models', () => {
|
|
const models = ['glm-4.6', 'glm-4.5v', 'glm-4.5-air', 'glm-4.5', 'glm-4-32b', 'glm-4', 'glm4'];
|
|
models.forEach((key) => {
|
|
const expectedPrompt = tokenValues[key].prompt;
|
|
const expectedCompletion = tokenValues[key].completion;
|
|
expect(getMultiplier({ valueKey: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
|
expect(getMultiplier({ valueKey: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
|
expect(getMultiplier({ model: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
|
expect(getMultiplier({ model: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('AWS Bedrock Model Tests', () => {
|
|
const awsModels = [
|
|
'anthropic.claude-3-5-haiku-20241022-v1:0',
|
|
'anthropic.claude-3-haiku-20240307-v1:0',
|
|
'anthropic.claude-3-sonnet-20240229-v1:0',
|
|
'anthropic.claude-3-opus-20240229-v1:0',
|
|
'anthropic.claude-3-5-sonnet-20240620-v1:0',
|
|
'anthropic.claude-v2:1',
|
|
'anthropic.claude-instant-v1',
|
|
'meta.llama2-13b-chat-v1',
|
|
'meta.llama2-70b-chat-v1',
|
|
'meta.llama3-8b-instruct-v1:0',
|
|
'meta.llama3-70b-instruct-v1:0',
|
|
'meta.llama3-1-8b-instruct-v1:0',
|
|
'meta.llama3-1-70b-instruct-v1:0',
|
|
'meta.llama3-1-405b-instruct-v1:0',
|
|
'mistral.mistral-7b-instruct-v0:2',
|
|
'mistral.mistral-small-2402-v1:0',
|
|
'mistral.mixtral-8x7b-instruct-v0:1',
|
|
'mistral.mistral-large-2402-v1:0',
|
|
'mistral.mistral-large-2407-v1:0',
|
|
'cohere.command-text-v14',
|
|
'cohere.command-light-text-v14',
|
|
'cohere.command-r-v1:0',
|
|
'cohere.command-r-plus-v1:0',
|
|
'ai21.j2-mid-v1',
|
|
'ai21.j2-ultra-v1',
|
|
'amazon.titan-text-lite-v1',
|
|
'amazon.titan-text-express-v1',
|
|
'amazon.nova-micro-v1:0',
|
|
'amazon.nova-lite-v1:0',
|
|
'amazon.nova-pro-v1:0',
|
|
];
|
|
|
|
it('should return the correct prompt multipliers for all models', () => {
|
|
const results = awsModels.map((model) => {
|
|
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
|
|
const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' });
|
|
return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt;
|
|
});
|
|
expect(results.every(Boolean)).toBe(true);
|
|
});
|
|
|
|
it('should return the correct completion multipliers for all models', () => {
|
|
const results = awsModels.map((model) => {
|
|
const valueKey = getValueKey(model, EModelEndpoint.bedrock);
|
|
const multiplier = getMultiplier({ valueKey, tokenType: 'completion' });
|
|
return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion;
|
|
});
|
|
expect(results.every(Boolean)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Amazon Model Tests', () => {
|
|
describe('Amazon Nova Models', () => {
|
|
it('should return correct pricing for nova-premier', () => {
|
|
expect(getMultiplier({ model: 'nova-premier', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-premier'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'nova-premier', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-premier'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-premier-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-premier'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-premier-v1:0', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-premier'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for nova-pro', () => {
|
|
expect(getMultiplier({ model: 'nova-pro', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-pro'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'nova-pro', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-pro'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-pro-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-pro'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-pro-v1:0', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-pro'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for nova-lite', () => {
|
|
expect(getMultiplier({ model: 'nova-lite', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-lite'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'nova-lite', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-lite'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-lite-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-lite'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-lite-v1:0', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-lite'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for nova-micro', () => {
|
|
expect(getMultiplier({ model: 'nova-micro', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-micro'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'nova-micro', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-micro'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-micro-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['nova-micro'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.nova-micro-v1:0', tokenType: 'completion' })).toBe(
|
|
tokenValues['nova-micro'].completion,
|
|
);
|
|
});
|
|
|
|
it('should match both short and full model names to the same pricing', () => {
|
|
const models = ['nova-micro', 'nova-lite', 'nova-pro', 'nova-premier'];
|
|
const fullModels = [
|
|
'amazon.nova-micro-v1:0',
|
|
'amazon.nova-lite-v1:0',
|
|
'amazon.nova-pro-v1:0',
|
|
'amazon.nova-premier-v1:0',
|
|
];
|
|
|
|
models.forEach((shortModel, i) => {
|
|
const fullModel = fullModels[i];
|
|
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
|
|
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
|
|
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
|
|
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
|
|
|
|
expect(shortPrompt).toBe(fullPrompt);
|
|
expect(shortCompletion).toBe(fullCompletion);
|
|
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
|
|
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Amazon Titan Models', () => {
|
|
it('should return correct pricing for titan-text-premier', () => {
|
|
expect(getMultiplier({ model: 'titan-text-premier', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-premier'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'titan-text-premier', tokenType: 'completion' })).toBe(
|
|
tokenValues['titan-text-premier'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.titan-text-premier-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-premier'].prompt,
|
|
);
|
|
expect(
|
|
getMultiplier({ model: 'amazon.titan-text-premier-v1:0', tokenType: 'completion' }),
|
|
).toBe(tokenValues['titan-text-premier'].completion);
|
|
});
|
|
|
|
it('should return correct pricing for titan-text-express', () => {
|
|
expect(getMultiplier({ model: 'titan-text-express', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-express'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'titan-text-express', tokenType: 'completion' })).toBe(
|
|
tokenValues['titan-text-express'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.titan-text-express-v1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-express'].prompt,
|
|
);
|
|
expect(
|
|
getMultiplier({ model: 'amazon.titan-text-express-v1', tokenType: 'completion' }),
|
|
).toBe(tokenValues['titan-text-express'].completion);
|
|
});
|
|
|
|
it('should return correct pricing for titan-text-lite', () => {
|
|
expect(getMultiplier({ model: 'titan-text-lite', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-lite'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'titan-text-lite', tokenType: 'completion' })).toBe(
|
|
tokenValues['titan-text-lite'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.titan-text-lite-v1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['titan-text-lite'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'amazon.titan-text-lite-v1', tokenType: 'completion' })).toBe(
|
|
tokenValues['titan-text-lite'].completion,
|
|
);
|
|
});
|
|
|
|
it('should match both short and full model names to the same pricing', () => {
|
|
const models = ['titan-text-lite', 'titan-text-express', 'titan-text-premier'];
|
|
const fullModels = [
|
|
'amazon.titan-text-lite-v1',
|
|
'amazon.titan-text-express-v1',
|
|
'amazon.titan-text-premier-v1:0',
|
|
];
|
|
|
|
models.forEach((shortModel, i) => {
|
|
const fullModel = fullModels[i];
|
|
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
|
|
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
|
|
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
|
|
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
|
|
|
|
expect(shortPrompt).toBe(fullPrompt);
|
|
expect(shortCompletion).toBe(fullCompletion);
|
|
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
|
|
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('AI21 Model Tests', () => {
|
|
describe('AI21 J2 Models', () => {
|
|
it('should return correct pricing for j2-mid', () => {
|
|
expect(getMultiplier({ model: 'j2-mid', tokenType: 'prompt' })).toBe(
|
|
tokenValues['j2-mid'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'j2-mid', tokenType: 'completion' })).toBe(
|
|
tokenValues['j2-mid'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.j2-mid-v1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['j2-mid'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.j2-mid-v1', tokenType: 'completion' })).toBe(
|
|
tokenValues['j2-mid'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for j2-ultra', () => {
|
|
expect(getMultiplier({ model: 'j2-ultra', tokenType: 'prompt' })).toBe(
|
|
tokenValues['j2-ultra'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'j2-ultra', tokenType: 'completion' })).toBe(
|
|
tokenValues['j2-ultra'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.j2-ultra-v1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['j2-ultra'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.j2-ultra-v1', tokenType: 'completion' })).toBe(
|
|
tokenValues['j2-ultra'].completion,
|
|
);
|
|
});
|
|
|
|
it('should match both short and full model names to the same pricing', () => {
|
|
const models = ['j2-mid', 'j2-ultra'];
|
|
const fullModels = ['ai21.j2-mid-v1', 'ai21.j2-ultra-v1'];
|
|
|
|
models.forEach((shortModel, i) => {
|
|
const fullModel = fullModels[i];
|
|
const shortPrompt = getMultiplier({ model: shortModel, tokenType: 'prompt' });
|
|
const fullPrompt = getMultiplier({ model: fullModel, tokenType: 'prompt' });
|
|
const shortCompletion = getMultiplier({ model: shortModel, tokenType: 'completion' });
|
|
const fullCompletion = getMultiplier({ model: fullModel, tokenType: 'completion' });
|
|
|
|
expect(shortPrompt).toBe(fullPrompt);
|
|
expect(shortCompletion).toBe(fullCompletion);
|
|
expect(shortPrompt).toBe(tokenValues[shortModel].prompt);
|
|
expect(shortCompletion).toBe(tokenValues[shortModel].completion);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('AI21 Jamba Models', () => {
|
|
it('should return correct pricing for jamba-instruct', () => {
|
|
expect(getMultiplier({ model: 'jamba-instruct', tokenType: 'prompt' })).toBe(
|
|
tokenValues['jamba-instruct'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'jamba-instruct', tokenType: 'completion' })).toBe(
|
|
tokenValues['jamba-instruct'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.jamba-instruct-v1:0', tokenType: 'prompt' })).toBe(
|
|
tokenValues['jamba-instruct'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'ai21.jamba-instruct-v1:0', tokenType: 'completion' })).toBe(
|
|
tokenValues['jamba-instruct'].completion,
|
|
);
|
|
});
|
|
|
|
it('should match both short and full model names to the same pricing', () => {
|
|
const shortPrompt = getMultiplier({ model: 'jamba-instruct', tokenType: 'prompt' });
|
|
const fullPrompt = getMultiplier({
|
|
model: 'ai21.jamba-instruct-v1:0',
|
|
tokenType: 'prompt',
|
|
});
|
|
const shortCompletion = getMultiplier({ model: 'jamba-instruct', tokenType: 'completion' });
|
|
const fullCompletion = getMultiplier({
|
|
model: 'ai21.jamba-instruct-v1:0',
|
|
tokenType: 'completion',
|
|
});
|
|
|
|
expect(shortPrompt).toBe(fullPrompt);
|
|
expect(shortCompletion).toBe(fullCompletion);
|
|
expect(shortPrompt).toBe(tokenValues['jamba-instruct'].prompt);
|
|
expect(shortCompletion).toBe(tokenValues['jamba-instruct'].completion);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Deepseek Model Tests', () => {
|
|
const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner', 'deepseek.r1'];
|
|
|
|
it('should return the correct prompt multipliers for all models', () => {
|
|
const results = deepseekModels.map((model) => {
|
|
const valueKey = getValueKey(model);
|
|
const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' });
|
|
return tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt;
|
|
});
|
|
expect(results.every(Boolean)).toBe(true);
|
|
});
|
|
|
|
it('should return the correct completion multipliers for all models', () => {
|
|
const results = deepseekModels.map((model) => {
|
|
const valueKey = getValueKey(model);
|
|
const multiplier = getMultiplier({ valueKey, tokenType: 'completion' });
|
|
return tokenValues[valueKey].completion && multiplier === tokenValues[valueKey].completion;
|
|
});
|
|
expect(results.every(Boolean)).toBe(true);
|
|
});
|
|
|
|
it('should return the correct prompt multipliers for reasoning model', () => {
|
|
const model = 'deepseek-reasoner';
|
|
const valueKey = getValueKey(model);
|
|
expect(valueKey).toBe(model);
|
|
const multiplier = getMultiplier({ valueKey, tokenType: 'prompt' });
|
|
const result = tokenValues[valueKey].prompt && multiplier === tokenValues[valueKey].prompt;
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return correct pricing for deepseek-chat', () => {
|
|
expect(getMultiplier({ model: 'deepseek-chat', tokenType: 'prompt' })).toBe(
|
|
tokenValues['deepseek-chat'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'deepseek-chat', tokenType: 'completion' })).toBe(
|
|
tokenValues['deepseek-chat'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for deepseek-reasoner', () => {
|
|
expect(getMultiplier({ model: 'deepseek-reasoner', tokenType: 'prompt' })).toBe(
|
|
tokenValues['deepseek-reasoner'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'deepseek-reasoner', tokenType: 'completion' })).toBe(
|
|
tokenValues['deepseek-reasoner'].completion,
|
|
);
|
|
});
|
|
|
|
it('should handle DeepSeek model name variations with provider prefixes', () => {
|
|
const modelVariations = [
|
|
'deepseek/deepseek-chat',
|
|
'openrouter/deepseek-chat',
|
|
'deepseek/deepseek-reasoner',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' });
|
|
const completionMultiplier = getMultiplier({ model, tokenType: 'completion' });
|
|
expect(promptMultiplier).toBe(tokenValues['deepseek-chat'].prompt);
|
|
expect(completionMultiplier).toBe(tokenValues['deepseek-chat'].completion);
|
|
});
|
|
});
|
|
|
|
it('should return correct cache multipliers for DeepSeek models', () => {
|
|
expect(getCacheMultiplier({ model: 'deepseek-chat', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['deepseek-chat'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'deepseek-chat', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['deepseek-chat'].read,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'deepseek-reasoner', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['deepseek-reasoner'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'deepseek-reasoner', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['deepseek-reasoner'].read,
|
|
);
|
|
});
|
|
|
|
it('should have consistent cache pricing across DeepSeek model variants', () => {
|
|
expect(cacheTokenValues['deepseek'].write).toBe(cacheTokenValues['deepseek-chat'].write);
|
|
expect(cacheTokenValues['deepseek'].read).toBe(cacheTokenValues['deepseek-chat'].read);
|
|
expect(cacheTokenValues['deepseek-reasoner'].write).toBe(
|
|
cacheTokenValues['deepseek-chat'].write,
|
|
);
|
|
expect(cacheTokenValues['deepseek-reasoner'].read).toBe(cacheTokenValues['deepseek-chat'].read);
|
|
});
|
|
|
|
it('should handle DeepSeek cache multipliers with model variations', () => {
|
|
const modelVariations = ['deepseek/deepseek-chat', 'openrouter/deepseek-reasoner'];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' });
|
|
const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' });
|
|
expect(writeMultiplier).toBe(cacheTokenValues['deepseek-chat'].write);
|
|
expect(readMultiplier).toBe(cacheTokenValues['deepseek-chat'].read);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Moonshot/Kimi Model Tests - Pricing', () => {
|
|
describe('Kimi Models', () => {
|
|
it('should return correct pricing for kimi base pattern', () => {
|
|
expect(getMultiplier({ model: 'kimi', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for kimi-k2.5', () => {
|
|
expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi-k2.5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2.5', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi-k2.5'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for kimi-k2 series', () => {
|
|
expect(getMultiplier({ model: 'kimi-k2', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi-k2'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi-k2'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for kimi-k2-turbo (higher pricing)', () => {
|
|
expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi-k2-turbo'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2-turbo', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi-k2-turbo'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for kimi-k2-thinking models', () => {
|
|
expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi-k2-thinking'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2-thinking', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi-k2-thinking'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'prompt' })).toBe(
|
|
tokenValues['kimi-k2-thinking-turbo'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'kimi-k2-thinking-turbo', tokenType: 'completion' })).toBe(
|
|
tokenValues['kimi-k2-thinking-turbo'].completion,
|
|
);
|
|
});
|
|
|
|
it('should handle Kimi model variations with provider prefixes', () => {
|
|
const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi-k2.5', 'openrouter/kimi'];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' });
|
|
const completionMultiplier = getMultiplier({ model, tokenType: 'completion' });
|
|
expect(promptMultiplier).toBe(tokenValues['kimi'].prompt);
|
|
expect([tokenValues['kimi'].completion, tokenValues['kimi-k2.5'].completion]).toContain(
|
|
completionMultiplier,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Moonshot Models', () => {
|
|
it('should return correct pricing for moonshot base pattern (128k pricing)', () => {
|
|
expect(getMultiplier({ model: 'moonshot', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for moonshot-v1-8k', () => {
|
|
expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-8k'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-8k', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-8k'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for moonshot-v1-32k', () => {
|
|
expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-32k'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-32k', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-32k'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for moonshot-v1-128k', () => {
|
|
expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-128k'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-128k', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-128k'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for moonshot-v1 vision models', () => {
|
|
expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-8k-vision'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-8k-vision', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-8k-vision'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-32k-vision'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-32k-vision', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-32k-vision'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot-v1-128k-vision'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot-v1-128k-vision', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot-v1-128k-vision'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Kimi Cache Multipliers', () => {
|
|
it('should return correct cache multipliers for kimi-k2 models', () => {
|
|
expect(getCacheMultiplier({ model: 'kimi', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['kimi'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'kimi', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['kimi'].read,
|
|
);
|
|
});
|
|
|
|
it('should return correct cache multipliers for kimi-k2.5 (lower read price)', () => {
|
|
expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['kimi-k2.5'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'kimi-k2.5', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['kimi-k2.5'].read,
|
|
);
|
|
});
|
|
|
|
it('should return correct cache multipliers for kimi-k2-turbo', () => {
|
|
expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['kimi-k2-turbo'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'kimi-k2-turbo', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['kimi-k2-turbo'].read,
|
|
);
|
|
});
|
|
|
|
it('should handle Kimi cache multipliers with model variations', () => {
|
|
const modelVariations = ['openrouter/kimi-k2', 'openrouter/kimi'];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' });
|
|
const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' });
|
|
expect(writeMultiplier).toBe(cacheTokenValues['kimi'].write);
|
|
expect(readMultiplier).toBe(cacheTokenValues['kimi'].read);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Bedrock Moonshot Models', () => {
|
|
it('should return correct pricing for Bedrock moonshot models', () => {
|
|
expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot.kimi'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot.kimi', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot.kimi'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot.kimi-k2', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot.kimi-k2'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'prompt' })).toBe(
|
|
tokenValues['moonshot.kimi-k2.5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'moonshot.kimi-k2.5', tokenType: 'completion' })).toBe(
|
|
tokenValues['moonshot.kimi-k2.5'].completion,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Qwen3 Model Tests', () => {
|
|
describe('Qwen3 Base Models', () => {
|
|
it('should return correct pricing for qwen3 base pattern', () => {
|
|
expect(getMultiplier({ model: 'qwen3', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-4b (falls back to qwen3)', () => {
|
|
expect(getMultiplier({ model: 'qwen3-4b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-4b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-8b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-8b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-8b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-8b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-8b'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-14b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-14b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-14b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-14b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-14b'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-235b-a22b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-235b-a22b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-235b-a22b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-235b-a22b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-235b-a22b'].completion,
|
|
);
|
|
});
|
|
|
|
it('should handle model name variations with provider prefixes', () => {
|
|
const models = [
|
|
{ input: 'qwen3', expected: 'qwen3' },
|
|
{ input: 'qwen3-4b', expected: 'qwen3' },
|
|
{ input: 'qwen3-8b', expected: 'qwen3-8b' },
|
|
{ input: 'qwen3-32b', expected: 'qwen3-32b' },
|
|
];
|
|
models.forEach(({ input, expected }) => {
|
|
const withPrefix = `alibaba/${input}`;
|
|
expect(getMultiplier({ model: withPrefix, tokenType: 'prompt' })).toBe(
|
|
tokenValues[expected].prompt,
|
|
);
|
|
expect(getMultiplier({ model: withPrefix, tokenType: 'completion' })).toBe(
|
|
tokenValues[expected].completion,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Qwen3 VL (Vision-Language) Models', () => {
|
|
it('should return correct pricing for qwen3-vl-8b-thinking', () => {
|
|
expect(getMultiplier({ model: 'qwen3-vl-8b-thinking', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-vl-8b-thinking'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-vl-8b-thinking', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-vl-8b-thinking'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-vl-8b-instruct', () => {
|
|
expect(getMultiplier({ model: 'qwen3-vl-8b-instruct', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-vl-8b-instruct'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-vl-8b-instruct', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-vl-8b-instruct'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-vl-30b-a3b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-vl-30b-a3b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-vl-30b-a3b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-vl-30b-a3b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-vl-30b-a3b'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-vl-235b-a22b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-vl-235b-a22b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-vl-235b-a22b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-vl-235b-a22b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-vl-235b-a22b'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Qwen3 Specialized Models', () => {
|
|
it('should return correct pricing for qwen3-max', () => {
|
|
expect(getMultiplier({ model: 'qwen3-max', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-max'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-max', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-max'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-coder', () => {
|
|
expect(getMultiplier({ model: 'qwen3-coder', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-coder'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-coder', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-coder'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-coder-plus', () => {
|
|
expect(getMultiplier({ model: 'qwen3-coder-plus', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-coder-plus'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-coder-plus', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-coder-plus'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-coder-flash', () => {
|
|
expect(getMultiplier({ model: 'qwen3-coder-flash', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-coder-flash'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-coder-flash', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-coder-flash'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct pricing for qwen3-next-80b-a3b', () => {
|
|
expect(getMultiplier({ model: 'qwen3-next-80b-a3b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['qwen3-next-80b-a3b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'qwen3-next-80b-a3b', tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3-next-80b-a3b'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Qwen3 Model Variations', () => {
|
|
it('should handle all qwen3 models with provider prefixes', () => {
|
|
const models = ['qwen3', 'qwen3-8b', 'qwen3-max', 'qwen3-coder', 'qwen3-vl-8b-instruct'];
|
|
const prefixes = ['alibaba', 'qwen', 'openrouter'];
|
|
|
|
models.forEach((model) => {
|
|
prefixes.forEach((prefix) => {
|
|
const fullModel = `${prefix}/${model}`;
|
|
expect(getMultiplier({ model: fullModel, tokenType: 'prompt' })).toBe(
|
|
tokenValues[model].prompt,
|
|
);
|
|
expect(getMultiplier({ model: fullModel, tokenType: 'completion' })).toBe(
|
|
tokenValues[model].completion,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle qwen3-4b falling back to qwen3 base pattern', () => {
|
|
const testCases = ['qwen3-4b', 'alibaba/qwen3-4b', 'qwen/qwen3-4b-preview'];
|
|
testCases.forEach((model) => {
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues['qwen3'].prompt);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['qwen3'].completion,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getCacheMultiplier', () => {
|
|
it('should return the correct cache multiplier for a given valueKey and cacheType', () => {
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-3-5-sonnet'].write,
|
|
);
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-3-5-sonnet'].read,
|
|
);
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-3-5-haiku'].write,
|
|
);
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-5-haiku', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-3-5-haiku'].read,
|
|
);
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-3-haiku'].write,
|
|
);
|
|
expect(getCacheMultiplier({ valueKey: 'claude-3-haiku', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-3-haiku'].read,
|
|
);
|
|
});
|
|
|
|
it('should return null if cacheType is provided but not found in cacheTokenValues', () => {
|
|
expect(
|
|
getCacheMultiplier({ valueKey: 'claude-3-5-sonnet', cacheType: 'unknownType' }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('should derive the valueKey from the model if not provided', () => {
|
|
expect(getCacheMultiplier({ cacheType: 'write', model: 'claude-3-5-sonnet-20240620' })).toBe(
|
|
cacheTokenValues['claude-3-5-sonnet'].write,
|
|
);
|
|
expect(getCacheMultiplier({ cacheType: 'read', model: 'claude-3-haiku-20240307' })).toBe(
|
|
cacheTokenValues['claude-3-haiku'].read,
|
|
);
|
|
});
|
|
|
|
it('should return null if only model or cacheType is missing', () => {
|
|
expect(getCacheMultiplier({ cacheType: 'write' })).toBeNull();
|
|
expect(getCacheMultiplier({ model: 'claude-3-5-sonnet' })).toBeNull();
|
|
});
|
|
|
|
it('should return null if derived valueKey does not match any known patterns', () => {
|
|
expect(getCacheMultiplier({ cacheType: 'write', model: 'gpt-4-some-other-info' })).toBeNull();
|
|
});
|
|
|
|
it('should handle endpointTokenConfig if provided', () => {
|
|
const endpointTokenConfig = {
|
|
'custom-model': {
|
|
write: 5,
|
|
read: 1,
|
|
},
|
|
};
|
|
expect(
|
|
getCacheMultiplier({ model: 'custom-model', cacheType: 'write', endpointTokenConfig }),
|
|
).toBe(endpointTokenConfig['custom-model'].write);
|
|
expect(
|
|
getCacheMultiplier({ model: 'custom-model', cacheType: 'read', endpointTokenConfig }),
|
|
).toBe(endpointTokenConfig['custom-model'].read);
|
|
});
|
|
|
|
it('should return null if model is not found in endpointTokenConfig', () => {
|
|
const endpointTokenConfig = {
|
|
'custom-model': {
|
|
write: 5,
|
|
read: 1,
|
|
},
|
|
};
|
|
expect(
|
|
getCacheMultiplier({ model: 'unknown-model', cacheType: 'write', endpointTokenConfig }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('should handle models with "bedrock/" prefix', () => {
|
|
expect(
|
|
getCacheMultiplier({
|
|
model: 'bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0',
|
|
cacheType: 'write',
|
|
}),
|
|
).toBe(cacheTokenValues['claude-3-5-sonnet'].write);
|
|
expect(
|
|
getCacheMultiplier({
|
|
model: 'bedrock/anthropic.claude-3-haiku-20240307-v1:0',
|
|
cacheType: 'read',
|
|
}),
|
|
).toBe(cacheTokenValues['claude-3-haiku'].read);
|
|
});
|
|
});
|
|
|
|
describe('Google Model Tests', () => {
|
|
const googleModels = [
|
|
'gemini-3',
|
|
'gemini-2.5-pro',
|
|
'gemini-2.5-flash',
|
|
'gemini-2.5-flash-lite',
|
|
'gemini-2.5-pro-preview-05-06',
|
|
'gemini-2.5-flash-preview-04-17',
|
|
'gemini-2.5-exp',
|
|
'gemini-2.0-flash-lite-preview-02-05',
|
|
'gemini-2.0-flash-001',
|
|
'gemini-2.0-flash-exp',
|
|
'gemini-2.0-pro-exp-02-05',
|
|
'gemini-1.5-flash-8b',
|
|
'gemini-1.5-flash-thinking',
|
|
'gemini-1.5-pro-latest',
|
|
'gemini-1.5-pro-preview-0409',
|
|
'gemini-pro-vision',
|
|
'gemini-1.0',
|
|
'gemini-pro',
|
|
];
|
|
|
|
it('should return the correct prompt and completion rates for all models', () => {
|
|
const results = googleModels.map((model) => {
|
|
const valueKey = getValueKey(model, EModelEndpoint.google);
|
|
const promptRate = getMultiplier({
|
|
model,
|
|
tokenType: 'prompt',
|
|
endpoint: EModelEndpoint.google,
|
|
});
|
|
const completionRate = getMultiplier({
|
|
model,
|
|
tokenType: 'completion',
|
|
endpoint: EModelEndpoint.google,
|
|
});
|
|
return { model, valueKey, promptRate, completionRate };
|
|
});
|
|
|
|
results.forEach(({ valueKey, promptRate, completionRate }) => {
|
|
expect(promptRate).toBe(tokenValues[valueKey].prompt);
|
|
expect(completionRate).toBe(tokenValues[valueKey].completion);
|
|
});
|
|
});
|
|
|
|
it('should map to the correct model keys', () => {
|
|
const expected = {
|
|
'gemini-3': 'gemini-3',
|
|
'gemini-2.5-pro': 'gemini-2.5-pro',
|
|
'gemini-2.5-flash': 'gemini-2.5-flash',
|
|
'gemini-2.5-flash-lite': 'gemini-2.5-flash-lite',
|
|
'gemini-2.5-pro-preview-05-06': 'gemini-2.5-pro',
|
|
'gemini-2.5-flash-preview-04-17': 'gemini-2.5-flash',
|
|
'gemini-2.5-exp': 'gemini-2.5',
|
|
'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite',
|
|
'gemini-2.0-flash-001': 'gemini-2.0-flash',
|
|
'gemini-2.0-flash-exp': 'gemini-2.0-flash',
|
|
'gemini-2.0-pro-exp-02-05': 'gemini-2.0',
|
|
'gemini-1.5-flash-8b': 'gemini-1.5-flash-8b',
|
|
'gemini-1.5-flash-thinking': 'gemini-1.5-flash',
|
|
'gemini-1.5-pro-latest': 'gemini-1.5',
|
|
'gemini-1.5-pro-preview-0409': 'gemini-1.5',
|
|
'gemini-pro-vision': 'gemini-pro-vision',
|
|
'gemini-1.0': 'gemini',
|
|
'gemini-pro': 'gemini',
|
|
};
|
|
|
|
Object.entries(expected).forEach(([model, expectedKey]) => {
|
|
const valueKey = getValueKey(model, EModelEndpoint.google);
|
|
expect(valueKey).toBe(expectedKey);
|
|
});
|
|
});
|
|
|
|
it('should handle model names with different formats', () => {
|
|
const testCases = [
|
|
{ input: 'google/gemini-pro', expected: 'gemini' },
|
|
{ input: 'gemini-pro/google', expected: 'gemini' },
|
|
{ input: 'google/gemini-2.0-flash-lite', expected: 'gemini-2.0-flash-lite' },
|
|
];
|
|
|
|
testCases.forEach(({ input, expected }) => {
|
|
const valueKey = getValueKey(input, EModelEndpoint.google);
|
|
expect(valueKey).toBe(expected);
|
|
expect(
|
|
getMultiplier({ model: input, tokenType: 'prompt', endpoint: EModelEndpoint.google }),
|
|
).toBe(tokenValues[expected].prompt);
|
|
expect(
|
|
getMultiplier({ model: input, tokenType: 'completion', endpoint: EModelEndpoint.google }),
|
|
).toBe(tokenValues[expected].completion);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Grok Model Tests - Pricing', () => {
|
|
describe('getMultiplier', () => {
|
|
test('should return correct prompt and completion rates for Grok vision models', () => {
|
|
const models = ['grok-2-vision-1212', 'grok-2-vision', 'grok-2-vision-latest'];
|
|
models.forEach((model) => {
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-2-vision'].prompt,
|
|
);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-2-vision'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok text models', () => {
|
|
const models = ['grok-2-1212', 'grok-2', 'grok-2-latest'];
|
|
models.forEach((model) => {
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues['grok-2'].prompt);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-2'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok beta models', () => {
|
|
expect(getMultiplier({ model: 'grok-vision-beta', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-vision-beta'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-vision-beta', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-vision-beta'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-beta', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-beta'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-beta', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-beta'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 3 models', () => {
|
|
expect(getMultiplier({ model: 'grok-3', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-fast'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-mini', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-mini', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-mini'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-mini-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-mini-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-3-mini-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-mini-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4 model', () => {
|
|
expect(getMultiplier({ model: 'grok-4-0709', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-4-0709', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4 Fast model', () => {
|
|
expect(getMultiplier({ model: 'grok-4-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-4-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4.1 Fast models', () => {
|
|
expect(getMultiplier({ model: 'grok-4-1-fast-reasoning', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-1-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-4-1-fast-reasoning', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4-1-fast'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-4-1-fast-non-reasoning', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-1-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-4-1-fast-non-reasoning', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4-1-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok Code Fast model', () => {
|
|
expect(getMultiplier({ model: 'grok-code-fast-1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-code-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'grok-code-fast-1', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-code-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 3 models with prefixes', () => {
|
|
expect(getMultiplier({ model: 'xai/grok-3', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-fast'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-mini', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-mini'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-mini', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-mini'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-mini-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-3-mini-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-3-mini-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-3-mini-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4 model with prefixes', () => {
|
|
expect(getMultiplier({ model: 'xai/grok-4-0709', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-4-0709', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4 Fast model with prefixes', () => {
|
|
expect(getMultiplier({ model: 'xai/grok-4-fast', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-4-fast', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4-fast'].completion,
|
|
);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok 4.1 Fast models with prefixes', () => {
|
|
expect(getMultiplier({ model: 'xai/grok-4-1-fast-reasoning', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-1-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-4-1-fast-reasoning', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-4-1-fast'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-4-1-fast-non-reasoning', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-4-1-fast'].prompt,
|
|
);
|
|
expect(
|
|
getMultiplier({ model: 'xai/grok-4-1-fast-non-reasoning', tokenType: 'completion' }),
|
|
).toBe(tokenValues['grok-4-1-fast'].completion);
|
|
});
|
|
|
|
test('should return correct prompt and completion rates for Grok Code Fast model with prefixes', () => {
|
|
expect(getMultiplier({ model: 'xai/grok-code-fast-1', tokenType: 'prompt' })).toBe(
|
|
tokenValues['grok-code-fast'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'xai/grok-code-fast-1', tokenType: 'completion' })).toBe(
|
|
tokenValues['grok-code-fast'].completion,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GLM Model Tests', () => {
|
|
it('should return expected value keys for GLM models', () => {
|
|
expect(getValueKey('glm-4.6')).toBe('glm-4.6');
|
|
expect(getValueKey('glm-4.5')).toBe('glm-4.5');
|
|
expect(getValueKey('glm-4.5v')).toBe('glm-4.5v');
|
|
expect(getValueKey('glm-4.5-air')).toBe('glm-4.5-air');
|
|
expect(getValueKey('glm-4-32b')).toBe('glm-4-32b');
|
|
expect(getValueKey('glm-4')).toBe('glm-4');
|
|
expect(getValueKey('glm4')).toBe('glm4');
|
|
});
|
|
|
|
it('should match GLM model variations with provider prefixes', () => {
|
|
expect(getValueKey('z-ai/glm-4.6')).toBe('glm-4.6');
|
|
expect(getValueKey('z-ai/glm-4.5')).toBe('glm-4.5');
|
|
expect(getValueKey('z-ai/glm-4.5-air')).toBe('glm-4.5-air');
|
|
expect(getValueKey('z-ai/glm-4.5v')).toBe('glm-4.5v');
|
|
expect(getValueKey('z-ai/glm-4-32b')).toBe('glm-4-32b');
|
|
|
|
expect(getValueKey('zai/glm-4.6')).toBe('glm-4.6');
|
|
expect(getValueKey('zai/glm-4.5')).toBe('glm-4.5');
|
|
expect(getValueKey('zai/glm-4.5-air')).toBe('glm-4.5-air');
|
|
expect(getValueKey('zai/glm-4.5v')).toBe('glm-4.5v');
|
|
|
|
expect(getValueKey('zai-org/GLM-4.6')).toBe('glm-4.6');
|
|
expect(getValueKey('zai-org/GLM-4.5')).toBe('glm-4.5');
|
|
expect(getValueKey('zai-org/GLM-4.5-Air')).toBe('glm-4.5-air');
|
|
expect(getValueKey('zai-org/GLM-4.5V')).toBe('glm-4.5v');
|
|
expect(getValueKey('zai-org/GLM-4-32B-0414')).toBe('glm-4-32b');
|
|
});
|
|
|
|
it('should match GLM model variations with suffixes', () => {
|
|
expect(getValueKey('glm-4.6-fp8')).toBe('glm-4.6');
|
|
expect(getValueKey('zai-org/GLM-4.6-FP8')).toBe('glm-4.6');
|
|
expect(getValueKey('zai-org/GLM-4.5-Air-FP8')).toBe('glm-4.5-air');
|
|
});
|
|
|
|
it('should prioritize more specific GLM model patterns', () => {
|
|
expect(getValueKey('glm-4.5-air-something')).toBe('glm-4.5-air');
|
|
expect(getValueKey('glm-4.5-something')).toBe('glm-4.5');
|
|
expect(getValueKey('glm-4.5v-something')).toBe('glm-4.5v');
|
|
});
|
|
|
|
it('should return correct multipliers for all GLM models', () => {
|
|
expect(getMultiplier({ model: 'glm-4.6', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.6'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4.6', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4.6'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm-4.5v', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.5v'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4.5v', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4.5v'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm-4.5-air', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.5-air'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4.5-air', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4.5-air'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm-4.5', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4.5', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4.5'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm-4-32b', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4-32b'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4-32b', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4-32b'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm-4', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'glm-4', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4'].completion,
|
|
);
|
|
|
|
expect(getMultiplier({ model: 'glm4', tokenType: 'prompt' })).toBe(tokenValues['glm4'].prompt);
|
|
expect(getMultiplier({ model: 'glm4', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm4'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct multipliers for GLM models with provider prefixes', () => {
|
|
expect(getMultiplier({ model: 'z-ai/glm-4.6', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.6'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'zai/glm-4.5-air', tokenType: 'completion' })).toBe(
|
|
tokenValues['glm-4.5-air'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'zai-org/GLM-4.5V', tokenType: 'prompt' })).toBe(
|
|
tokenValues['glm-4.5v'].prompt,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Claude Model Tests', () => {
|
|
it('should return correct prompt and completion rates for Claude 4 models', () => {
|
|
expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-sonnet-4'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-sonnet-4', tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-sonnet-4'].completion,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-opus-4'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-opus-4', tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-opus-4'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct prompt and completion rates for Claude Haiku 4.5', () => {
|
|
expect(getMultiplier({ model: 'claude-haiku-4-5', tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-haiku-4-5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-haiku-4-5', tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-haiku-4-5'].completion,
|
|
);
|
|
});
|
|
|
|
it('should return correct prompt and completion rates for Claude Opus 4.5', () => {
|
|
expect(getMultiplier({ model: 'claude-opus-4-5', tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-opus-4-5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-opus-4-5', tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-opus-4-5'].completion,
|
|
);
|
|
});
|
|
|
|
it('should handle Claude Haiku 4.5 model name variations', () => {
|
|
const modelVariations = [
|
|
'claude-haiku-4-5',
|
|
'claude-haiku-4-5-20250420',
|
|
'claude-haiku-4-5-latest',
|
|
'anthropic/claude-haiku-4-5',
|
|
'claude-haiku-4-5/anthropic',
|
|
'claude-haiku-4-5-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const valueKey = getValueKey(model);
|
|
expect(valueKey).toBe('claude-haiku-4-5');
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-haiku-4-5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-haiku-4-5'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should handle Claude Opus 4.5 model name variations', () => {
|
|
const modelVariations = [
|
|
'claude-opus-4-5',
|
|
'claude-opus-4-5-20250420',
|
|
'claude-opus-4-5-latest',
|
|
'anthropic/claude-opus-4-5',
|
|
'claude-opus-4-5/anthropic',
|
|
'claude-opus-4-5-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const valueKey = getValueKey(model);
|
|
expect(valueKey).toBe('claude-opus-4-5');
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-opus-4-5'].prompt,
|
|
);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-opus-4-5'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should handle Claude 4 model name variations with different prefixes and suffixes', () => {
|
|
const modelVariations = [
|
|
'claude-sonnet-4',
|
|
'claude-sonnet-4-20240229',
|
|
'claude-sonnet-4-latest',
|
|
'anthropic/claude-sonnet-4',
|
|
'claude-sonnet-4/anthropic',
|
|
'claude-sonnet-4-preview',
|
|
'claude-sonnet-4-20240229-preview',
|
|
'claude-opus-4',
|
|
'claude-opus-4-20240229',
|
|
'claude-opus-4-latest',
|
|
'anthropic/claude-opus-4',
|
|
'claude-opus-4/anthropic',
|
|
'claude-opus-4-preview',
|
|
'claude-opus-4-20240229-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const valueKey = getValueKey(model);
|
|
const isSonnet = model.includes('sonnet');
|
|
const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4';
|
|
|
|
expect(valueKey).toBe(expectedKey);
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(tokenValues[expectedKey].prompt);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues[expectedKey].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should return correct cache rates for Claude 4 models', () => {
|
|
expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-sonnet-4'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'claude-sonnet-4', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-sonnet-4'].read,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-opus-4'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-opus-4'].read,
|
|
);
|
|
});
|
|
|
|
it('should return correct cache rates for Claude Opus 4.5', () => {
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4-5', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-opus-4-5'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4-5', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-opus-4-5'].read,
|
|
);
|
|
});
|
|
|
|
it('should handle Claude 4 model cache rates with different prefixes and suffixes', () => {
|
|
const modelVariations = [
|
|
'claude-sonnet-4',
|
|
'claude-sonnet-4-20240229',
|
|
'claude-sonnet-4-latest',
|
|
'anthropic/claude-sonnet-4',
|
|
'claude-sonnet-4/anthropic',
|
|
'claude-sonnet-4-preview',
|
|
'claude-sonnet-4-20240229-preview',
|
|
'claude-opus-4',
|
|
'claude-opus-4-20240229',
|
|
'claude-opus-4-latest',
|
|
'anthropic/claude-opus-4',
|
|
'claude-opus-4/anthropic',
|
|
'claude-opus-4-preview',
|
|
'claude-opus-4-20240229-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const isSonnet = model.includes('sonnet');
|
|
const expectedKey = isSonnet ? 'claude-sonnet-4' : 'claude-opus-4';
|
|
|
|
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(
|
|
cacheTokenValues[expectedKey].write,
|
|
);
|
|
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(
|
|
cacheTokenValues[expectedKey].read,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should return correct prompt and completion rates for Claude Opus 4.6', () => {
|
|
expect(getMultiplier({ model: 'claude-opus-4-6', tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-opus-4-6'].prompt,
|
|
);
|
|
expect(getMultiplier({ model: 'claude-opus-4-6', tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-opus-4-6'].completion,
|
|
);
|
|
});
|
|
|
|
it('should handle Claude Opus 4.6 model name variations', () => {
|
|
const modelVariations = [
|
|
'claude-opus-4-6',
|
|
'claude-opus-4-6-20250801',
|
|
'claude-opus-4-6-latest',
|
|
'anthropic/claude-opus-4-6',
|
|
'claude-opus-4-6/anthropic',
|
|
'claude-opus-4-6-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
const valueKey = getValueKey(model);
|
|
expect(valueKey).toBe('claude-opus-4-6');
|
|
expect(getMultiplier({ model, tokenType: 'prompt' })).toBe(
|
|
tokenValues['claude-opus-4-6'].prompt,
|
|
);
|
|
expect(getMultiplier({ model, tokenType: 'completion' })).toBe(
|
|
tokenValues['claude-opus-4-6'].completion,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should return correct cache rates for Claude Opus 4.6', () => {
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4-6', cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-opus-4-6'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model: 'claude-opus-4-6', cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-opus-4-6'].read,
|
|
);
|
|
});
|
|
|
|
it('should handle Claude Opus 4.6 cache rates with model name variations', () => {
|
|
const modelVariations = [
|
|
'claude-opus-4-6',
|
|
'claude-opus-4-6-20250801',
|
|
'claude-opus-4-6-latest',
|
|
'anthropic/claude-opus-4-6',
|
|
'claude-opus-4-6/anthropic',
|
|
'claude-opus-4-6-preview',
|
|
];
|
|
|
|
modelVariations.forEach((model) => {
|
|
expect(getCacheMultiplier({ model, cacheType: 'write' })).toBe(
|
|
cacheTokenValues['claude-opus-4-6'].write,
|
|
);
|
|
expect(getCacheMultiplier({ model, cacheType: 'read' })).toBe(
|
|
cacheTokenValues['claude-opus-4-6'].read,
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Premium Token Pricing', () => {
|
|
const premiumModel = 'claude-opus-4-6';
|
|
const premiumEntry = premiumTokenValues[premiumModel];
|
|
const { threshold } = premiumEntry;
|
|
const belowThreshold = threshold - 1;
|
|
const aboveThreshold = threshold + 1;
|
|
const wellAboveThreshold = threshold * 2;
|
|
|
|
it('should have premium pricing defined for claude-opus-4-6', () => {
|
|
expect(premiumEntry).toBeDefined();
|
|
expect(premiumEntry.threshold).toBeDefined();
|
|
expect(premiumEntry.prompt).toBeDefined();
|
|
expect(premiumEntry.completion).toBeDefined();
|
|
expect(premiumEntry.prompt).toBeGreaterThan(tokenValues[premiumModel].prompt);
|
|
expect(premiumEntry.completion).toBeGreaterThan(tokenValues[premiumModel].completion);
|
|
});
|
|
|
|
it('should return null from getPremiumRate when inputTokenCount is below threshold', () => {
|
|
expect(getPremiumRate(premiumModel, 'prompt', belowThreshold)).toBeNull();
|
|
expect(getPremiumRate(premiumModel, 'completion', belowThreshold)).toBeNull();
|
|
expect(getPremiumRate(premiumModel, 'prompt', threshold)).toBeNull();
|
|
});
|
|
|
|
it('should return premium rate from getPremiumRate when inputTokenCount exceeds threshold', () => {
|
|
expect(getPremiumRate(premiumModel, 'prompt', aboveThreshold)).toBe(premiumEntry.prompt);
|
|
expect(getPremiumRate(premiumModel, 'completion', aboveThreshold)).toBe(
|
|
premiumEntry.completion,
|
|
);
|
|
expect(getPremiumRate(premiumModel, 'prompt', wellAboveThreshold)).toBe(premiumEntry.prompt);
|
|
});
|
|
|
|
it('should return null from getPremiumRate when inputTokenCount is undefined or null', () => {
|
|
expect(getPremiumRate(premiumModel, 'prompt', undefined)).toBeNull();
|
|
expect(getPremiumRate(premiumModel, 'prompt', null)).toBeNull();
|
|
});
|
|
|
|
it('should return null from getPremiumRate for models without premium pricing', () => {
|
|
expect(getPremiumRate('claude-opus-4-5', 'prompt', wellAboveThreshold)).toBeNull();
|
|
expect(getPremiumRate('claude-sonnet-4', 'prompt', wellAboveThreshold)).toBeNull();
|
|
expect(getPremiumRate('gpt-4o', 'prompt', wellAboveThreshold)).toBeNull();
|
|
});
|
|
|
|
it('should return standard rate from getMultiplier when inputTokenCount is below threshold', () => {
|
|
expect(
|
|
getMultiplier({
|
|
model: premiumModel,
|
|
tokenType: 'prompt',
|
|
inputTokenCount: belowThreshold,
|
|
}),
|
|
).toBe(tokenValues[premiumModel].prompt);
|
|
expect(
|
|
getMultiplier({
|
|
model: premiumModel,
|
|
tokenType: 'completion',
|
|
inputTokenCount: belowThreshold,
|
|
}),
|
|
).toBe(tokenValues[premiumModel].completion);
|
|
});
|
|
|
|
it('should return premium rate from getMultiplier when inputTokenCount exceeds threshold', () => {
|
|
expect(
|
|
getMultiplier({
|
|
model: premiumModel,
|
|
tokenType: 'prompt',
|
|
inputTokenCount: aboveThreshold,
|
|
}),
|
|
).toBe(premiumEntry.prompt);
|
|
expect(
|
|
getMultiplier({
|
|
model: premiumModel,
|
|
tokenType: 'completion',
|
|
inputTokenCount: aboveThreshold,
|
|
}),
|
|
).toBe(premiumEntry.completion);
|
|
});
|
|
|
|
it('should return standard rate from getMultiplier when inputTokenCount is exactly at threshold', () => {
|
|
expect(
|
|
getMultiplier({ model: premiumModel, tokenType: 'prompt', inputTokenCount: threshold }),
|
|
).toBe(tokenValues[premiumModel].prompt);
|
|
});
|
|
|
|
it('should return premium rate from getMultiplier when inputTokenCount is one above threshold', () => {
|
|
expect(
|
|
getMultiplier({ model: premiumModel, tokenType: 'prompt', inputTokenCount: aboveThreshold }),
|
|
).toBe(premiumEntry.prompt);
|
|
});
|
|
|
|
it('should not apply premium pricing to models without premium entries', () => {
|
|
expect(
|
|
getMultiplier({
|
|
model: 'claude-opus-4-5',
|
|
tokenType: 'prompt',
|
|
inputTokenCount: wellAboveThreshold,
|
|
}),
|
|
).toBe(tokenValues['claude-opus-4-5'].prompt);
|
|
expect(
|
|
getMultiplier({
|
|
model: 'claude-sonnet-4',
|
|
tokenType: 'prompt',
|
|
inputTokenCount: wellAboveThreshold,
|
|
}),
|
|
).toBe(tokenValues['claude-sonnet-4'].prompt);
|
|
});
|
|
|
|
it('should use standard rate when inputTokenCount is not provided', () => {
|
|
expect(getMultiplier({ model: premiumModel, tokenType: 'prompt' })).toBe(
|
|
tokenValues[premiumModel].prompt,
|
|
);
|
|
expect(getMultiplier({ model: premiumModel, tokenType: 'completion' })).toBe(
|
|
tokenValues[premiumModel].completion,
|
|
);
|
|
});
|
|
|
|
it('should apply premium pricing through getMultiplier with valueKey path', () => {
|
|
const valueKey = getValueKey(premiumModel);
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: aboveThreshold })).toBe(
|
|
premiumEntry.prompt,
|
|
);
|
|
expect(
|
|
getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: aboveThreshold }),
|
|
).toBe(premiumEntry.completion);
|
|
});
|
|
|
|
it('should apply standard pricing through getMultiplier with valueKey path when below threshold', () => {
|
|
const valueKey = getValueKey(premiumModel);
|
|
expect(getMultiplier({ valueKey, tokenType: 'prompt', inputTokenCount: belowThreshold })).toBe(
|
|
tokenValues[premiumModel].prompt,
|
|
);
|
|
expect(
|
|
getMultiplier({ valueKey, tokenType: 'completion', inputTokenCount: belowThreshold }),
|
|
).toBe(tokenValues[premiumModel].completion);
|
|
});
|
|
});
|
|
|
|
describe('tokens.ts and tx.js sync validation', () => {
|
|
it('should resolve all models in maxTokensMap to pricing via getValueKey', () => {
|
|
const tokensKeys = Object.keys(maxTokensMap[EModelEndpoint.openAI]);
|
|
const txKeys = Object.keys(tokenValues);
|
|
|
|
const unresolved = [];
|
|
|
|
tokensKeys.forEach((key) => {
|
|
// Skip legacy token size mappings (e.g., '4k', '8k', '16k', '32k')
|
|
if (/^\d+k$/.test(key)) return;
|
|
|
|
// Skip generic pattern keys (end with '-' or ':')
|
|
if (key.endsWith('-') || key.endsWith(':')) return;
|
|
|
|
// Try to resolve via getValueKey
|
|
const resolvedKey = getValueKey(key);
|
|
|
|
// If it resolves and the resolved key has pricing, success
|
|
if (resolvedKey && txKeys.includes(resolvedKey)) return;
|
|
|
|
// If it resolves to a legacy key (4k, 8k, etc), also OK
|
|
if (resolvedKey && /^\d+k$/.test(resolvedKey)) return;
|
|
|
|
// If we get here, this model can't get pricing - flag it
|
|
unresolved.push({
|
|
key,
|
|
resolvedKey: resolvedKey || 'undefined',
|
|
context: maxTokensMap[EModelEndpoint.openAI][key],
|
|
});
|
|
});
|
|
|
|
if (unresolved.length > 0) {
|
|
console.log('\nModels that cannot resolve to pricing via getValueKey:');
|
|
unresolved.forEach(({ key, resolvedKey, context }) => {
|
|
console.log(` - '${key}' → '${resolvedKey}' (context: ${context})`);
|
|
});
|
|
}
|
|
|
|
expect(unresolved).toEqual([]);
|
|
});
|
|
|
|
it('should not have redundant dated variants with same pricing and context as base model', () => {
|
|
const txKeys = Object.keys(tokenValues);
|
|
const redundant = [];
|
|
|
|
txKeys.forEach((key) => {
|
|
// Check if this is a dated variant (ends with -YYYY-MM-DD)
|
|
if (key.match(/.*-\d{4}-\d{2}-\d{2}$/)) {
|
|
const baseKey = key.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
|
|
if (txKeys.includes(baseKey)) {
|
|
const variantPricing = tokenValues[key];
|
|
const basePricing = tokenValues[baseKey];
|
|
const variantContext = maxTokensMap[EModelEndpoint.openAI][key];
|
|
const baseContext = maxTokensMap[EModelEndpoint.openAI][baseKey];
|
|
|
|
const samePricing =
|
|
variantPricing.prompt === basePricing.prompt &&
|
|
variantPricing.completion === basePricing.completion;
|
|
const sameContext = variantContext === baseContext;
|
|
|
|
if (samePricing && sameContext) {
|
|
redundant.push({
|
|
key,
|
|
baseKey,
|
|
pricing: `${variantPricing.prompt}/${variantPricing.completion}`,
|
|
context: variantContext,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (redundant.length > 0) {
|
|
console.log('\nRedundant dated variants found (same pricing and context as base):');
|
|
redundant.forEach(({ key, baseKey, pricing, context }) => {
|
|
console.log(` - '${key}' → '${baseKey}' (pricing: ${pricing}, context: ${context})`);
|
|
console.log(` Can be removed - pattern matching will handle it`);
|
|
});
|
|
}
|
|
|
|
expect(redundant).toEqual([]);
|
|
});
|
|
|
|
it('should have context windows in tokens.ts for all models with pricing in tx.js (openAI catch-all)', () => {
|
|
const txKeys = Object.keys(tokenValues);
|
|
const missingContext = [];
|
|
|
|
txKeys.forEach((key) => {
|
|
// Skip legacy token size mappings (4k, 8k, 16k, 32k)
|
|
if (/^\d+k$/.test(key)) return;
|
|
|
|
// Check if this model has a context window defined
|
|
const context = maxTokensMap[EModelEndpoint.openAI][key];
|
|
|
|
if (!context) {
|
|
const pricing = tokenValues[key];
|
|
missingContext.push({
|
|
key,
|
|
pricing: `${pricing.prompt}/${pricing.completion}`,
|
|
});
|
|
}
|
|
});
|
|
|
|
if (missingContext.length > 0) {
|
|
console.log('\nModels with pricing but missing context in tokens.ts:');
|
|
missingContext.forEach(({ key, pricing }) => {
|
|
console.log(` - '${key}' (pricing: ${pricing})`);
|
|
console.log(` Add to tokens.ts openAIModels/bedrockModels/etc.`);
|
|
});
|
|
}
|
|
|
|
expect(missingContext).toEqual([]);
|
|
});
|
|
});
|