From d76032316e1c8e7b876120ef66a81379cfc176e9 Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:22:21 +0800 Subject: [PATCH] feat: add first-class MiniMax provider support --- .env.example | 6 ++ src/index.ts | 5 +- src/lib/__tests__/minimax.test.ts | 40 ++++++++++ src/lib/__tests__/models.test.ts | 30 ++++++++ src/lib/__tests__/task-dispatch.test.ts | 9 ++- src/lib/__tests__/token-pricing.test.ts | 20 +++++ src/lib/minimax.ts | 50 +++++++++++++ src/lib/models.ts | 38 +++++++++- src/lib/provider-subscriptions.ts | 5 +- src/lib/task-dispatch.ts | 98 ++++++++++++++++++++++--- src/lib/token-pricing.ts | 4 + src/store/index.ts | 5 +- 12 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 src/lib/__tests__/minimax.test.ts create mode 100644 src/lib/minimax.ts diff --git a/.env.example b/.env.example index a10305c7..4dd93f3c 100644 --- a/.env.example +++ b/.env.example @@ -104,6 +104,12 @@ NEXT_PUBLIC_GATEWAY_PORT=18789 # Core CRUD features work; live gateway events do not. # NEXT_PUBLIC_GATEWAY_OPTIONAL=true +# MiniMax direct dispatch (optional) +# MINIMAX_API_KEY= +# MINIMAX_REGION=global_en +# MINIMAX_API_PROTOCOL=openai +# Region values: global_en, cn_zh. Protocol values: openai, anthropic. + # ═══════════════════════════════════════════════════════════════════════════════ # OpenClaw Integration # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/index.ts b/src/index.ts index 0c127ea3..1faa10ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,7 +81,10 @@ export interface ModelConfig { provider: string description: string /** USD per MILLION tokens (input/output) — mirrors ModelConfig in '@/lib/models' */ - costPerMTok: { input: number; output: number } + costPerMTok: { input: number; output: number; cacheRead?: number; cacheWrite?: number | null } + contextWindow?: number + inputModalities?: Array<'text' | 'image' | 'video'> + thinking?: Array<'adaptive' | 'disabled' | 'always_on'> } // Mission Control Phase 2 Types diff --git a/src/lib/__tests__/minimax.test.ts b/src/lib/__tests__/minimax.test.ts new file mode 100644 index 00000000..938c2225 --- /dev/null +++ b/src/lib/__tests__/minimax.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { getMiniMaxApiKey, MINIMAX_REGIONS, resolveMiniMaxEndpoint } from '@/lib/minimax' + +describe('MiniMax endpoint configuration', () => { + it('defaults to the global OpenAI-compatible endpoint', () => { + expect(resolveMiniMaxEndpoint({})).toEqual({ + region: 'global_en', + protocol: 'openai', + baseUrl: 'https://api.minimax.io/v1', + docsRoot: 'https://platform.minimax.io/docs', + }) + }) + + it('resolves both China compatibility endpoints', () => { + expect(resolveMiniMaxEndpoint({ MINIMAX_REGION: 'cn_zh' }).baseUrl) + .toBe('https://api.minimaxi.com/v1') + expect(resolveMiniMaxEndpoint({ MINIMAX_REGION: 'cn_zh', MINIMAX_API_PROTOCOL: 'anthropic' }).baseUrl) + .toBe('https://api.minimaxi.com/anthropic') + }) + + it('resolves the global Anthropic-compatible endpoint', () => { + expect(resolveMiniMaxEndpoint({ MINIMAX_API_PROTOCOL: 'anthropic' }).baseUrl) + .toBe('https://api.minimax.io/anthropic') + }) + + it('keeps both regional documentation roots registered', () => { + expect(MINIMAX_REGIONS.global_en.docsRoot).toBe('https://platform.minimax.io/docs') + expect(MINIMAX_REGIONS.cn_zh.docsRoot).toBe('https://platform.minimaxi.com/docs') + }) + + it('rejects unsupported regions and protocols', () => { + expect(() => resolveMiniMaxEndpoint({ MINIMAX_REGION: 'invalid' })).toThrow('MINIMAX_REGION') + expect(() => resolveMiniMaxEndpoint({ MINIMAX_API_PROTOCOL: 'invalid' })).toThrow('MINIMAX_API_PROTOCOL') + }) + + it('trims the configured API key', () => { + expect(getMiniMaxApiKey({ MINIMAX_API_KEY: ' configured-key ' })).toBe('configured-key') + expect(getMiniMaxApiKey({ MINIMAX_API_KEY: ' ' })).toBeNull() + }) +}) diff --git a/src/lib/__tests__/models.test.ts b/src/lib/__tests__/models.test.ts index ff768c6d..a548890a 100644 --- a/src/lib/__tests__/models.test.ts +++ b/src/lib/__tests__/models.test.ts @@ -118,3 +118,33 @@ describe('classifyModelProvider (catalog-derived classification)', () => { expect(classifyModelProvider('')).toBeUndefined() }) }) + +describe('MiniMax model catalog', () => { + it('registers current models with pricing and capabilities', () => { + const m3 = getModelByAlias('minimax') + const m27 = getModelByAlias('minimax-m2.7') + + expect(m3).toMatchObject({ + name: 'minimax/MiniMax-M3', + provider: 'minimax', + contextWindow: 1_000_000, + inputModalities: ['text', 'image', 'video'], + thinking: ['adaptive', 'disabled'], + costPerMTok: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: null }, + }) + expect(m27).toMatchObject({ + name: 'minimax/MiniMax-M2.7', + provider: 'minimax', + contextWindow: 204_800, + inputModalities: ['text'], + thinking: ['always_on'], + costPerMTok: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + }) + }) + + it('classifies exact model IDs without changing their API casing', () => { + expect(classifyModelProvider('MiniMax-M3')).toBe('minimax') + expect(classifyModelProvider('MiniMax-M2.7')).toBe('minimax') + expect(getDispatchModelId(getModelByAlias('minimax')!)).toBe('MiniMax-M3') + }) +}) diff --git a/src/lib/__tests__/task-dispatch.test.ts b/src/lib/__tests__/task-dispatch.test.ts index 61e02d3b..77fae9ab 100644 --- a/src/lib/__tests__/task-dispatch.test.ts +++ b/src/lib/__tests__/task-dispatch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import Database from 'better-sqlite3' -import { insertDispatchTokenUsage, resolveTaskDispatchModelOverride } from '@/lib/task-dispatch' +import { insertDispatchTokenUsage, pickProvider, resolveTaskDispatchModelOverride } from '@/lib/task-dispatch' describe('insertDispatchTokenUsage', () => { it('persists dispatch usage using the current token_usage schema', () => { @@ -56,3 +56,10 @@ describe('resolveTaskDispatchModelOverride', () => { expect(resolveTaskDispatchModelOverride({ agent_config: '{not json' })).toBeNull() }) }) + +describe('MiniMax direct dispatch routing', () => { + it('selects the dedicated provider for both current model IDs', () => { + expect(pickProvider('MiniMax-M3')).toBe('minimax') + expect(pickProvider('minimax/MiniMax-M2.7')).toBe('minimax') + }) +}) diff --git a/src/lib/__tests__/token-pricing.test.ts b/src/lib/__tests__/token-pricing.test.ts index 32d68539..a3a12733 100644 --- a/src/lib/__tests__/token-pricing.test.ts +++ b/src/lib/__tests__/token-pricing.test.ts @@ -42,6 +42,26 @@ describe('token pricing', () => { expect(getModelPricing('minimax/minimax-m2.1')).toMatchObject({ inputPerMTok: 0.3, outputPerMTok: 1.2 }) }) + it('uses current MiniMax pricing, including cache rates', () => { + expect(getModelPricing('minimax/MiniMax-M3')).toMatchObject({ + inputPerMTok: 0.6, + outputPerMTok: 2.4, + cacheReadPerMTok: 0.12, + cacheWritePerMTok: null, + }) + expect(getModelPricing('minimax/MiniMax-M2.7')).toMatchObject({ + inputPerMTok: 0.3, + outputPerMTok: 1.2, + cacheReadPerMTok: 0.06, + cacheWritePerMTok: 0.375, + }) + }) + + it('maps bare MiniMax model IDs to the provider', () => { + expect(getProviderFromModel('MiniMax-M3')).toBe('minimax') + expect(getProviderFromModel('MiniMax-M2.7')).toBe('minimax') + }) + it('keeps local models at zero cost', () => { const cost = calculateTokenCost('ollama/qwen2.5-coder:14b', 50_000, 50_000) expect(cost).toBe(0) diff --git a/src/lib/minimax.ts b/src/lib/minimax.ts new file mode 100644 index 00000000..566316a2 --- /dev/null +++ b/src/lib/minimax.ts @@ -0,0 +1,50 @@ +export type MiniMaxRegion = 'global_en' | 'cn_zh' +export type MiniMaxProtocol = 'openai' | 'anthropic' + +export interface MiniMaxEnvironment { + MINIMAX_API_KEY?: string + MINIMAX_REGION?: string + MINIMAX_API_PROTOCOL?: string +} + +export const MINIMAX_REGIONS = { + global_en: { + openaiBaseUrl: 'https://api.minimax.io/v1', + anthropicBaseUrl: 'https://api.minimax.io/anthropic', + docsRoot: 'https://platform.minimax.io/docs', + }, + cn_zh: { + openaiBaseUrl: 'https://api.minimaxi.com/v1', + anthropicBaseUrl: 'https://api.minimaxi.com/anthropic', + docsRoot: 'https://platform.minimaxi.com/docs', + }, +} as const + +export function getMiniMaxApiKey(env: MiniMaxEnvironment = process.env as MiniMaxEnvironment): string | null { + return (env.MINIMAX_API_KEY || '').trim() || null +} + +export function resolveMiniMaxEndpoint(env: MiniMaxEnvironment = process.env as MiniMaxEnvironment): { + region: MiniMaxRegion + protocol: MiniMaxProtocol + baseUrl: string + docsRoot: string +} { + const region = (env.MINIMAX_REGION || 'global_en').trim().toLowerCase() + if (region !== 'global_en' && region !== 'cn_zh') { + throw new Error('MINIMAX_REGION must be global_en or cn_zh') + } + + const protocol = (env.MINIMAX_API_PROTOCOL || 'openai').trim().toLowerCase() + if (protocol !== 'openai' && protocol !== 'anthropic') { + throw new Error('MINIMAX_API_PROTOCOL must be openai or anthropic') + } + + const regionalConfig = MINIMAX_REGIONS[region] + return { + region, + protocol, + baseUrl: protocol === 'anthropic' ? regionalConfig.anthropicBaseUrl : regionalConfig.openaiBaseUrl, + docsRoot: regionalConfig.docsRoot, + } +} diff --git a/src/lib/models.ts b/src/lib/models.ts index b2915f76..84fff112 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -13,14 +13,24 @@ export interface ModelCostPerMTok { input: number /** USD per 1M output tokens */ output: number + /** USD per 1M cached input tokens read */ + cacheRead?: number + /** USD per 1M cached input tokens written, when supported */ + cacheWrite?: number | null } +export type ModelInputModality = 'text' | 'image' | 'video' +export type ModelThinkingMode = 'adaptive' | 'disabled' | 'always_on' + export interface ModelConfig { alias: string name: string provider: string description: string costPerMTok: ModelCostPerMTok + contextWindow?: number + inputModalities?: ModelInputModality[] + thinking?: ModelThinkingMode[] } export const MODEL_CATALOG: ModelConfig[] = [ @@ -42,11 +52,31 @@ export const MODEL_CATALOG: ModelConfig[] = [ // Groq (hosted inference) — https://groq.com/pricing (verified 2026-07-04) { alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPerMTok: { input: 0.05, output: 0.08 } }, { alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPerMTok: { input: 0.59, output: 0.79 } }, + // MiniMax — https://platform.minimax.io/docs/guides/pricing-paygo (verified 2026-07-24) + { + alias: 'minimax', + name: 'minimax/MiniMax-M3', + provider: 'minimax', + description: 'Multimodal model with configurable thinking', + costPerMTok: { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: null }, + contextWindow: 1_000_000, + inputModalities: ['text', 'image', 'video'], + thinking: ['adaptive', 'disabled'], + }, + { + alias: 'minimax-m2.7', + name: 'minimax/MiniMax-M2.7', + provider: 'minimax', + description: 'Text model with always-on thinking', + costPerMTok: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, + contextWindow: 204_800, + inputModalities: ['text'], + thinking: ['always_on'], + }, // Other providers — Moonshot list price via https://openrouter.ai/moonshotai/kimi-k2.5; - // Venice https://docs.venice.ai/overview/pricing; MiniMax https://platform.minimax.io/docs/guides/pricing-paygo + // Venice https://docs.venice.ai/overview/pricing. { alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPerMTok: { input: 0.6, output: 3.0 } }, { alias: 'venice-llama-3.3-70b', name: 'venice/llama-3.3-70b', provider: 'venice', description: 'Venice AI Llama 3.3 70B', costPerMTok: { input: 0.7, output: 2.8 } }, - { alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective, strong coding', costPerMTok: { input: 0.3, output: 1.2 } }, ] export function getModelByAlias(alias: string): ModelConfig | undefined { @@ -84,7 +114,9 @@ export function classifyModelProvider(model: string): string | undefined { const normalized = model.trim().toLowerCase() if (!normalized) return undefined const entry = MODEL_CATALOG.find(m => - m.name === normalized || getDispatchModelId(m) === normalized || m.alias === normalized + m.name.toLowerCase() === normalized + || getDispatchModelId(m).toLowerCase() === normalized + || m.alias.toLowerCase() === normalized ) return entry?.provider } diff --git a/src/lib/provider-subscriptions.ts b/src/lib/provider-subscriptions.ts index bcf007fe..803c8b8c 100644 --- a/src/lib/provider-subscriptions.ts +++ b/src/lib/provider-subscriptions.ts @@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process' import path from 'node:path' import os from 'node:os' import { config } from '@/lib/config' +import { classifyModelProvider } from '@/lib/models' interface ProviderSubscription { provider: string @@ -213,6 +214,9 @@ export function getProviderFromModel(modelName: string): string { const normalized = modelName.trim().toLowerCase() if (!normalized) return 'unknown' + const catalogProvider = classifyModelProvider(normalized) + if (catalogProvider) return catalogProvider + const [prefix] = normalized.split('/') if (prefix && !prefix.includes(':')) { // Most models are provider-prefixed, e.g., "anthropic/claude-sonnet-4-5". @@ -225,4 +229,3 @@ export function getProviderFromModel(modelName: string): string { if (normalized.includes('gpt') || normalized.includes('codex') || normalized.includes('o1') || normalized.includes('o3')) return 'openai' return 'unknown' } - diff --git a/src/lib/task-dispatch.ts b/src/lib/task-dispatch.ts index 3109e3d8..9c8f4a9c 100644 --- a/src/lib/task-dispatch.ts +++ b/src/lib/task-dispatch.ts @@ -15,6 +15,7 @@ import { getAllGatewaySessions } from './sessions' import { parseJsonlTranscript, readSessionJsonl, type TranscriptMessage } from './transcript-parser' import { syncTaskOutbound } from './github-sync-engine' import { classifyModelProvider, getDispatchModelId, getModelByAlias } from './models' +import { getMiniMaxApiKey, resolveMiniMaxEndpoint } from './minimax' import type Database from 'better-sqlite3' const AGENT_DISPATCH_ACCEPT_TIMEOUT_MS = 60_000 @@ -806,7 +807,7 @@ async function callClaudeDirectly( } // --------------------------------------------------------------------------- -// Direct OpenAI / OpenAI-compatible local dispatch — also gateway-free. +// Direct compatibility API dispatch — also gateway-free. // // The "local" provider path is intentionally generic: it speaks the OpenAI // `/v1/chat/completions` REST shape, which is what LMStudio, Ollama, vLLM and @@ -820,7 +821,7 @@ async function callClaudeDirectly( // anything else (incl. "claude-*") → Anthropic // --------------------------------------------------------------------------- -type DirectProvider = 'anthropic' | 'openai' | 'local' +export type DirectProvider = 'anthropic' | 'openai' | 'local' | 'minimax' function getOpenAIApiKey(): string | null { return (process.env.OPENAI_API_KEY || '').trim() || null @@ -841,26 +842,27 @@ function getLocalApiKey(): string | null { return (process.env.LOCAL_LLM_API_KEY || '').trim() || null } -function pickProvider(model: string): DirectProvider { +export function pickProvider(model: string): DirectProvider { // Consult MODEL_CATALOG first (single source of truth). Only providers // with a direct dispatch path map to a DirectProvider; catalog providers - // without one (google, groq, moonshot, venice, minimax) fall through to - // the prefix rules below, which keeps their routing identical to before. + // without one fall through to the prefix rules below. const catalogProvider = classifyModelProvider(model) if (catalogProvider === 'anthropic') return 'anthropic' if (catalogProvider === 'openai') return 'openai' if (catalogProvider === 'ollama') return 'local' + if (catalogProvider === 'minimax') return 'minimax' // Prefix-match fallback for models not in the catalog — behavior for // unknown IDs is unchanged (default remains 'anthropic'). const m = model.toLowerCase() if (m.startsWith('openai/') || m.startsWith('gpt-') || m.startsWith('o1-') || m.startsWith('o3-')) return 'openai' if (m.startsWith('local/') || m.startsWith('ollama/') || m.startsWith('lmstudio/') || m.startsWith('litellm/')) return 'local' + if (m.startsWith('minimax/')) return 'minimax' return 'anthropic' } function stripProviderPrefix(model: string): string { - return model.replace(/^(openai|local|ollama|lmstudio|litellm|anthropic)\//, '') + return model.replace(/^(openai|local|ollama|lmstudio|litellm|anthropic|minimax)\//i, '') } /** @@ -913,7 +915,9 @@ function isDirectDispatchAvailable(provider?: DirectProvider): boolean { if (provider === 'anthropic') return !!getAnthropicApiKey() || isClaudeCliAvailable() if (provider === 'openai') return !!getOpenAIApiKey() || isCodexCliAvailable() if (provider === 'local') return !!getLocalEndpoint() - return !!getAnthropicApiKey() || !!getOpenAIApiKey() || !!getLocalEndpoint() || isClaudeCliAvailable() || isCodexCliAvailable() + if (provider === 'minimax') return !!getMiniMaxApiKey() + return !!getAnthropicApiKey() || !!getOpenAIApiKey() || !!getLocalEndpoint() + || !!getMiniMaxApiKey() || isClaudeCliAvailable() || isCodexCliAvailable() } /** @@ -1137,6 +1141,78 @@ async function callOpenAICompatible( return { text, sessionId: null } } +async function callMiniMaxAnthropicCompatible( + task: DispatchableTask, + prompt: string, + endpoint: string, + apiKey: string, + model: string, +): Promise { + const soul = getAgentSoulContent(task) + const messages: Array<{ role: string; content: string }> = [ + { role: 'user', content: prompt }, + ] + const body: Record = { model, max_tokens: 4096, messages } + if (soul) body.system = soul + + logger.info( + { taskId: task.id, model, agent: task.agent_name, provider: 'minimax' }, + 'Dispatching task via direct MiniMax API', + ) + + const res = await fetch(endpoint.replace(/\/$/, '') + '/v1/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + apiKey, + 'anthropic-version': '2023-06-01', + }, + body: JSON.stringify(body), + }) + + if (!res.ok) { + const errorBody = await res.text().catch(() => '') + throw new Error('MiniMax API ' + res.status + ': ' + errorBody.substring(0, 500)) + } + + const data = await res.json() as { + content?: Array<{ type: string; text?: string }> + usage?: { input_tokens?: number; output_tokens?: number } + } + const text = data.content + ?.filter(block => block.type === 'text') + .map(block => block.text || '') + .join('\n') || null + + if (data.usage) { + recordDispatchTokenUsage({ + model, + sessionId: 'task-' + task.id, + inputTokens: data.usage.input_tokens || 0, + outputTokens: data.usage.output_tokens || 0, + workspaceId: task.workspace_id, + }) + } + + return { text, sessionId: null } +} + +async function callMiniMaxDirectly( + task: DispatchableTask, + prompt: string, + model: string, +): Promise { + const apiKey = getMiniMaxApiKey() + if (!apiKey) throw new Error('MINIMAX_API_KEY not set — cannot dispatch to MiniMax without gateway') + + const { baseUrl, protocol } = resolveMiniMaxEndpoint() + const modelId = stripProviderPrefix(model) + if (protocol === 'anthropic') { + return callMiniMaxAnthropicCompatible(task, prompt, baseUrl, apiKey, modelId) + } + return callOpenAICompatible(task, prompt, baseUrl, apiKey, modelId, 'minimax') +} + async function callOpenAIDirectly(task: DispatchableTask, prompt: string, model: string): Promise { const apiKey = getOpenAIApiKey() if (!apiKey) { @@ -1225,6 +1301,7 @@ async function callLocalDirectly(task: DispatchableTask, prompt: string, model: async function callDirectly(task: DispatchableTask, prompt: string): Promise { const model = classifyDirectModel(task) const provider = pickProvider(model) + if (provider === 'minimax') return callMiniMaxDirectly(task, prompt, model) if (provider === 'openai') return callOpenAIDirectly(task, prompt, model) if (provider === 'local') return callLocalDirectly(task, prompt, model) // Anthropic: prefer the host Claude Code CLI when available — it uses the @@ -1348,7 +1425,7 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string let agentResponse: AgentResponseParsed if (!isGatewayAvailable() && isDirectDispatchAvailable()) { - // Direct API review — no gateway needed (Anthropic / OpenAI / local). + // Direct API review through the configured provider, with no gateway required. // Pass through agent_config so Aegis honors per-agent dispatchModel // overrides and routes to the matching provider. const reviewTask: DispatchableTask = { @@ -1519,7 +1596,7 @@ export async function requeueStaleTasks(): Promise<{ ok: boolean; message: strin // When MC runs in direct-API mode (no gateway), the agent has no heartbeat // and stays "offline" by design — but tasks still get dispatched via the - // direct provider (Anthropic/OpenAI/local). Skip the offline-stale check + // configured direct provider. Skip the offline-stale check // entirely in that mode, otherwise every task is failed after 5 cycles // before any direct-API dispatch can run. const directApiSkipsStaleCheck = !isGatewayAvailable() && isDirectDispatchAvailable() @@ -1679,8 +1756,7 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s // less restrictive provider; failures surface as dispatch failures. agentResponse = await dispatchViaClaudeSession(task, prompt) } else if (useDirectApi && !targetSession) { - // Direct API dispatch — provider chosen by `dispatchModel` prefix - // (Anthropic / OpenAI / OpenAI-compatible local). No gateway needed. + // Direct API dispatch — provider chosen by `dispatchModel`. No gateway needed. agentResponse = await callDirectly(task, prompt) } else if (targetSession) { // Dispatch to a specific existing session via chat.send diff --git a/src/lib/token-pricing.ts b/src/lib/token-pricing.ts index 3f6c23b7..e64a0f3d 100644 --- a/src/lib/token-pricing.ts +++ b/src/lib/token-pricing.ts @@ -3,6 +3,8 @@ import { getProviderFromModel } from '@/lib/provider-subscriptions' interface ModelPricing { inputPerMTok: number outputPerMTok: number + cacheReadPerMTok?: number + cacheWritePerMTok?: number | null } const DEFAULT_MODEL_PRICING: ModelPricing = { @@ -50,6 +52,8 @@ const MODEL_PRICING: Record = { 'groq/llama-3.1-8b-instant': { inputPerMTok: 0.05, outputPerMTok: 0.08 }, 'groq/llama-3.3-70b-versatile': { inputPerMTok: 0.59, outputPerMTok: 0.79 }, 'minimax/minimax-m2.1': { inputPerMTok: 0.3, outputPerMTok: 1.2 }, + 'minimax/minimax-m3': { inputPerMTok: 0.6, outputPerMTok: 2.4, cacheReadPerMTok: 0.12, cacheWritePerMTok: null }, + 'minimax/minimax-m2.7': { inputPerMTok: 0.3, outputPerMTok: 1.2, cacheReadPerMTok: 0.06, cacheWritePerMTok: 0.375 }, 'moonshot/kimi-k2.5': { inputPerMTok: 0.6, outputPerMTok: 3.0 }, 'ollama/deepseek-r1:14b': { inputPerMTok: 0.0, outputPerMTok: 0.0 }, 'ollama/qwen2.5-coder:14b': { inputPerMTok: 0.0, outputPerMTok: 0.0 }, diff --git a/src/store/index.ts b/src/store/index.ts index 35150171..4322dfde 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -94,7 +94,10 @@ export interface ModelConfig { provider: string description: string /** USD per MILLION tokens (input/output) — mirrors ModelConfig in '@/lib/models' */ - costPerMTok: { input: number; output: number } + costPerMTok: { input: number; output: number; cacheRead?: number; cacheWrite?: number | null } + contextWindow?: number + inputModalities?: Array<'text' | 'image' | 'video'> + thinking?: Array<'adaptive' | 'disabled' | 'always_on'> } // Mission Control Phase 2 Types