From c0be999c7cc651b32497e6d62aa116efd95cbeef Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:53:25 +0800 Subject: [PATCH 1/2] Add MiniMax M3 pricing fallback --- server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server.js b/server.js index cb53736..03a0c62 100644 --- a/server.js +++ b/server.js @@ -41,6 +41,7 @@ const PROVIDER_MAP = { const CUSTOM_PRICING = { 'minimax-m2': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.03 }, 'minimax-m2.7': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.03 }, + 'minimax-m3': { input_mtok: 0.60, output_mtok: 2.40, cache_read_mtok: 0.12 }, }; // Calculate cost in USD from token counts From a61c9b70d8dbe29a4a948cff919aa79f194c3abb Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:01:24 +0800 Subject: [PATCH 2/2] feat: add MiniMax Flash pricing fallback --- lib/pricing.js | 65 ++++++++++++++++++++++++++++++++++++++++++++ server.js | 61 +---------------------------------------- test/pricing.test.js | 58 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 60 deletions(-) create mode 100644 lib/pricing.js create mode 100644 test/pricing.test.js diff --git a/lib/pricing.js b/lib/pricing.js new file mode 100644 index 0000000..cb83d69 --- /dev/null +++ b/lib/pricing.js @@ -0,0 +1,65 @@ +// ── LLM Pricing (via @pydantic/genai-prices) ── +const { calcPrice } = require('@pydantic/genai-prices'); + +// Free models (no cost regardless of pricing data) +const FREE_MODELS = new Set([ + 'xiaomi/mimo-v2-pro', + 'mimo-v2-pro', +]); + +// Hermes billing_provider → genai-prices providerId mapping +const PROVIDER_MAP = { + openrouter: 'openrouter', + 'openai-codex': 'openai', + 'opencode-go': 'openrouter', // minimax available via OpenRouter pricing +}; + +// Custom pricing for models not in genai-prices (per million tokens). +// MiniMax-M3 uses the standard <=512k baseline; the published long-context +// threshold has no exact integer expansion, so the fallback avoids guessing it. +const CUSTOM_PRICING = { + 'minimax-m2': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.03 }, + 'minimax-m2.7': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.06 }, + 'minimax-m3.1-flash': { input_mtok: 0.15, output_mtok: 0.60, cache_read_mtok: 0.015 }, + 'minimax-m3': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.06 }, +}; + +// Calculate cost in USD from token counts +function calculateCost(model, inputTokens, outputTokens, cacheReadTokens = 0, billingProvider) { + if (!model || FREE_MODELS.has(model)) return 0; + + // genai-prices expects input_tokens = total input (including cache) + // Our DB stores input and cache separately, so combine them + const usage = { + input_tokens: inputTokens + cacheReadTokens, + output_tokens: outputTokens, + cache_read_tokens: cacheReadTokens, + }; + + // 1. Try with provider hint + const providerId = PROVIDER_MAP[billingProvider]; + if (providerId) { + try { + const r = calcPrice(usage, model, { providerId }); + if (r) return r.total_price; + } catch (_) { /* fallback to next */ } + } + + // 2. Try without provider (match across all providers) + try { + const r = calcPrice(usage, model); + if (r) return r.total_price; + } catch (_) { /* fallback to custom pricing */ } + + // 3. Custom pricing fallback + const custom = CUSTOM_PRICING[String(model).toLowerCase()]; + if (custom) { + return (inputTokens / 1e6) * custom.input_mtok + + (outputTokens / 1e6) * custom.output_mtok + + (cacheReadTokens / 1e6) * (custom.cache_read_mtok || custom.input_mtok * 0.1); + } + + return 0; +} + +module.exports = { calculateCost }; diff --git a/server.js b/server.js index 03a0c62..91ef085 100644 --- a/server.js +++ b/server.js @@ -21,66 +21,7 @@ const { // ── TUI Gateway Bridge ── const { getBridge, killAllBridges } = require('./lib/tui-gateway-bridge'); -// ── LLM Pricing (via @pydantic/genai-prices) ── -const { calcPrice } = require('@pydantic/genai-prices'); - -// Free models (no cost regardless of pricing data) -const FREE_MODELS = new Set([ - 'xiaomi/mimo-v2-pro', - 'mimo-v2-pro', -]); - -// Hermes billing_provider → genai-prices providerId mapping -const PROVIDER_MAP = { - 'openrouter': 'openrouter', - 'openai-codex': 'openai', - 'opencode-go': 'openrouter', // minimax available via OpenRouter pricing -}; - -// Custom pricing for models not in genai-prices (per million tokens) -const CUSTOM_PRICING = { - 'minimax-m2': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.03 }, - 'minimax-m2.7': { input_mtok: 0.30, output_mtok: 1.20, cache_read_mtok: 0.03 }, - 'minimax-m3': { input_mtok: 0.60, output_mtok: 2.40, cache_read_mtok: 0.12 }, -}; - -// Calculate cost in USD from token counts -function calculateCost(model, inputTokens, outputTokens, cacheReadTokens = 0, billingProvider) { - if (!model || FREE_MODELS.has(model)) return 0; - - // genai-prices expects input_tokens = total input (including cache) - // Our DB stores input and cache separately, so combine them - const usage = { - input_tokens: inputTokens + cacheReadTokens, - output_tokens: outputTokens, - cache_read_tokens: cacheReadTokens, - }; - - // 1. Try with provider hint - const providerId = PROVIDER_MAP[billingProvider]; - if (providerId) { - try { - const r = calcPrice(usage, model, { providerId }); - if (r) return r.total_price; - } catch (_) { /* fallback to next */ } - } - - // 2. Try without provider (match across all providers) - try { - const r = calcPrice(usage, model); - if (r) return r.total_price; - } catch (_) { /* fallback to custom pricing */ } - - // 3. Custom pricing fallback - const custom = CUSTOM_PRICING[model]; - if (custom) { - return (inputTokens / 1e6) * custom.input_mtok - + (outputTokens / 1e6) * custom.output_mtok - + (cacheReadTokens / 1e6) * (custom.cache_read_mtok || custom.input_mtok * 0.1); - } - - return 0; -} +const { calculateCost } = require('./lib/pricing'); // Async shell execution utility (non-blocking) function parseShellTimeout(value) { diff --git a/test/pricing.test.js b/test/pricing.test.js new file mode 100644 index 0000000..4704074 --- /dev/null +++ b/test/pricing.test.js @@ -0,0 +1,58 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Module = require('node:module'); + +function loadPricing(calcPrice) { + const originalLoad = Module._load; + Module._load = function load(request, parent, isMain) { + if (request === '@pydantic/genai-prices') return { calcPrice }; + return originalLoad.call(this, request, parent, isMain); + }; + + try { + delete require.cache[require.resolve('../lib/pricing')]; + return require('../lib/pricing'); + } finally { + Module._load = originalLoad; + } +} + +test('MiniMax M3.1 Flash fallback uses the documented direct rates and provider hint', () => { + const calls = []; + const { calculateCost } = loadPricing((usage, model, options) => { + calls.push({ usage, model, options }); + return null; + }); + + assert.equal( + calculateCost('MiniMax-M3.1-Flash', 1_000_000, 1_000_000, 1_000_000, 'opencode-go'), + 0.765 + ); + assert.deepEqual(calls, [ + { + usage: { input_tokens: 2_000_000, output_tokens: 1_000_000, cache_read_tokens: 1_000_000 }, + model: 'MiniMax-M3.1-Flash', + options: { providerId: 'openrouter' }, + }, + { + usage: { input_tokens: 2_000_000, output_tokens: 1_000_000, cache_read_tokens: 1_000_000 }, + model: 'MiniMax-M3.1-Flash', + options: undefined, + }, + ]); +}); + +test('MiniMax M3 fallback stays on the documented standard baseline', () => { + const { calculateCost } = loadPricing(() => null); + + assert.equal(calculateCost('minimax-m3', 1_000_000, 1_000_000, 1_000_000), 1.56); +}); + +test('SDK pricing remains authoritative and legacy/free/unknown behavior is preserved', () => { + const { calculateCost: calculateWithSdk } = loadPricing(() => ({ total_price: 7.25 })); + assert.equal(calculateWithSdk('minimax-m3.1-flash', 1_000_000, 1_000_000), 7.25); + + const { calculateCost: calculateWithFallback } = loadPricing(() => null); + assert.equal(calculateWithFallback('mimo-v2-pro', 1_000_000, 1_000_000), 0); + assert.equal(calculateWithFallback('custom/saved-model', 1_000_000, 1_000_000), 0); +});