diff --git a/python/mini_claude/agent.py b/python/mini_claude/agent.py index bab186a..0e8c9eb 100644 --- a/python/mini_claude/agent.py +++ b/python/mini_claude/agent.py @@ -60,6 +60,7 @@ print_confirmation, print_divider, print_cost, + get_model_pricing, print_retry, print_info, print_sub_agent_start, @@ -109,6 +110,8 @@ async def _with_retry(fn, max_retries: int = 3): "claude-opus-4-20250514": 200000, "gpt-4o": 128000, "gpt-4o-mini": 128000, + "MiniMax-M3": 1000000, + "MiniMax-M2.7": 204800, } @@ -125,12 +128,16 @@ def _model_supports_thinking(model: str) -> bool: return False if "claude" in m and any(x in m for x in ("opus", "sonnet", "haiku")): return True + # MiniMax-M3 (adaptive/off) and MiniMax-M2.7 (always-on) both expose thinking. + if "minimax" in m: + return True return False def _model_supports_adaptive_thinking(model: str) -> bool: m = model.lower() - return "opus-4-6" in m or "sonnet-4-6" in m + # MiniMax-M2.7 keeps thinking always-on, so only MiniMax-M3 is adaptive. + return "opus-4-6" in m or "sonnet-4-6" in m or "minimax-m3" in m def _get_max_output_tokens(model: str) -> int: @@ -514,14 +521,16 @@ def show_cost(self) -> None: print_info(f"Tokens: {self.total_input_tokens} in / {self.total_output_tokens} out{cache_info}\n Estimated cost: ${total:.4f}{budget_info}{turn_info}") def _get_current_cost_usd(self) -> float: - # Base input $3/Mtok. Cache read is 0.1x, cache write is 1.25x — the - # fixed multipliers Claude Code uses across every model tier. + # Per-million-token rates are resolved per model (see get_model_pricing). + # The default matches the fixed Claude multipliers (input $3, output $15, + # cache read 0.1x, cache write 1.25x); models with published rates override it. M = 1_000_000 + p = get_model_pricing(self.model) return ( - (self.total_input_tokens / M) * 3 - + (self.total_cache_read_tokens / M) * 0.3 - + (self.total_cache_creation_tokens / M) * 3.75 - + (self.total_output_tokens / M) * 15 + (self.total_input_tokens / M) * p["input"] + + (self.total_cache_read_tokens / M) * p["cache_read"] + + (self.total_cache_creation_tokens / M) * p["cache_write"] + + (self.total_output_tokens / M) * p["output"] ) def _check_budget(self) -> dict: @@ -1542,7 +1551,7 @@ def _on_tool_block(block: dict): if not tool_uses: if not self.is_sub_agent: - print_cost(self.total_input_tokens, self.total_output_tokens, self.total_cache_read_tokens, self.total_cache_creation_tokens) + print_cost(self.total_input_tokens, self.total_output_tokens, self.total_cache_read_tokens, self.total_cache_creation_tokens, self.model) break self.current_turns += 1 @@ -1763,7 +1772,7 @@ async def _chat_openai(self, user_message: str) -> None: tool_calls = message.get("tool_calls") if not tool_calls: if not self.is_sub_agent: - print_cost(self.total_input_tokens, self.total_output_tokens, self.total_cache_read_tokens, self.total_cache_creation_tokens) + print_cost(self.total_input_tokens, self.total_output_tokens, self.total_cache_read_tokens, self.total_cache_creation_tokens, self.model) break self.current_turns += 1 diff --git a/python/mini_claude/ui.py b/python/mini_claude/ui.py index c50d828..0f559bd 100644 --- a/python/mini_claude/ui.py +++ b/python/mini_claude/ui.py @@ -81,13 +81,33 @@ def print_divider() -> None: console.print(f"\n[dim] {'─' * 50}[/dim]") -def print_cost(input_tokens: int, output_tokens: int, cache_read: int = 0, cache_creation: int = 0) -> None: - # Cache read is billed 0.1x, cache write 1.25x (see agent _get_current_cost_usd). +# ─── Per-model pricing (USD per million tokens) ───────────── +# The default rates are the fixed multipliers used across Claude tiers +# (input $3, output $15, cache read 0.1x = $0.3, cache write 1.25x = $3.75). +# Models with published per-model rates override the default below so budget +# enforcement and the per-turn cost line bill at the correct rate. +DEFAULT_PRICING = {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75} + +MODEL_PRICING = { + # MiniMax-M3 publishes no separate cache-write rate, so cache writes are + # billed at the input rate. + "MiniMax-M3": {"input": 0.6, "output": 2.4, "cache_read": 0.12, "cache_write": 0.6}, + "MiniMax-M2.7": {"input": 0.3, "output": 1.2, "cache_read": 0.06, "cache_write": 0.375}, +} + + +def get_model_pricing(model: str) -> dict: + return MODEL_PRICING.get(model, DEFAULT_PRICING) + + +def print_cost(input_tokens: int, output_tokens: int, cache_read: int = 0, cache_creation: int = 0, model: str | None = None) -> None: + # Cache read/write and per-token rates are model-specific (see get_model_pricing). + p = get_model_pricing(model) if model else DEFAULT_PRICING total = ( - (input_tokens / 1_000_000) * 3 - + (cache_read / 1_000_000) * 0.3 - + (cache_creation / 1_000_000) * 3.75 - + (output_tokens / 1_000_000) * 15 + (input_tokens / 1_000_000) * p["input"] + + (cache_read / 1_000_000) * p["cache_read"] + + (cache_creation / 1_000_000) * p["cache_write"] + + (output_tokens / 1_000_000) * p["output"] ) cache_str = f", {cache_read} cached" if cache_read else "" console.print(f"\n[dim] Tokens: {input_tokens} in / {output_tokens} out{cache_str} (~${total:.4f})[/dim]") diff --git a/src/agent.ts b/src/agent.ts index f2025b7..6a3b2c6 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -10,6 +10,7 @@ import { printConfirmation, printDivider, printCost, + getModelPricing, printRetry, printInfo, printSubAgentStart, @@ -79,6 +80,8 @@ const MODEL_CONTEXT: Record = { "claude-opus-4-20250514": 200000, "gpt-4o": 128000, "gpt-4o-mini": 128000, + "MiniMax-M3": 1000000, + "MiniMax-M2.7": 204800, }; function getContextWindow(model: string): number { @@ -93,12 +96,15 @@ function modelSupportsThinking(model: string): boolean { // Claude 4+ models support thinking (not Claude 3.x) if (m.includes("claude-3-") || m.includes("3-5-") || m.includes("3-7-")) return false; if (m.includes("claude") && (m.includes("opus") || m.includes("sonnet") || m.includes("haiku"))) return true; - return false; // non-Claude models (GPT, etc.) — no thinking + // MiniMax-M3 (adaptive/off) and MiniMax-M2.7 (always-on) both expose thinking. + if (m.includes("minimax")) return true; + return false; // other non-Claude models (GPT, etc.) — no thinking } function modelSupportsAdaptiveThinking(model: string): boolean { const m = model.toLowerCase(); - return m.includes("opus-4-6") || m.includes("sonnet-4-6"); + // MiniMax-M2.7 keeps thinking always-on, so only MiniMax-M3 is adaptive. + return m.includes("opus-4-6") || m.includes("sonnet-4-6") || m.includes("minimax-m3"); } // Max output tokens by model (following the same caps Claude Code uses publicly) @@ -553,12 +559,14 @@ export class Agent { private getCurrentCostUsd(): number { const M = 1_000_000; - // Base input $3/Mtok. Cache read is 0.1x, cache write is 1.25x — the fixed - // multipliers Claude Code uses across every model tier (utils/modelCost.ts). - const costIn = (this.totalInputTokens / M) * 3; - const costCacheRead = (this.totalCacheReadTokens / M) * 0.3; - const costCacheWrite = (this.totalCacheCreationTokens / M) * 3.75; - const costOut = (this.totalOutputTokens / M) * 15; + // Per-million-token rates are resolved per model (see getModelPricing). + // The default matches the fixed Claude multipliers (input $3, output $15, + // cache read 0.1x, cache write 1.25x); models with published rates override it. + const p = getModelPricing(this.model); + const costIn = (this.totalInputTokens / M) * p.input; + const costCacheRead = (this.totalCacheReadTokens / M) * p.cacheRead; + const costCacheWrite = (this.totalCacheCreationTokens / M) * p.cacheWrite; + const costOut = (this.totalOutputTokens / M) * p.output; return costIn + costCacheRead + costCacheWrite + costOut; } @@ -1710,7 +1718,7 @@ IMPORTANT: When your plan is complete, you MUST call exit_plan_mode. Do NOT ask if (toolUses.length === 0) { if (!this.isSubAgent) { - printCost(this.totalInputTokens, this.totalOutputTokens, this.totalCacheReadTokens, this.totalCacheCreationTokens); + printCost(this.totalInputTokens, this.totalOutputTokens, this.totalCacheReadTokens, this.totalCacheCreationTokens, this.model); } break; } @@ -1949,7 +1957,7 @@ IMPORTANT: When your plan is complete, you MUST call exit_plan_mode. Do NOT ask const toolCalls = message.tool_calls; if (!toolCalls || toolCalls.length === 0) { if (!this.isSubAgent) { - printCost(this.totalInputTokens, this.totalOutputTokens, this.totalCacheReadTokens, this.totalCacheCreationTokens); + printCost(this.totalInputTokens, this.totalOutputTokens, this.totalCacheReadTokens, this.totalCacheCreationTokens, this.model); } break; } diff --git a/src/ui.ts b/src/ui.ts index a98a9fa..8a75b9c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -83,13 +83,44 @@ export function printDivider() { console.log(chalk.gray("\n " + "─".repeat(50))); } -export function printCost(inputTokens: number, outputTokens: number, cacheRead = 0, cacheCreation = 0) { - // Cache read is billed 0.1x, cache write 1.25x (see agent getCurrentCostUsd). +// ─── Per-model pricing (USD per million tokens) ───────────── +// The default rates are the fixed multipliers used across Claude tiers +// (input $3, output $15, cache read 0.1x = $0.3, cache write 1.25x = $3.75). +// Models with published per-model rates override the default below so budget +// enforcement and the per-turn cost line bill at the correct rate. +export interface ModelPricing { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; +} + +export const DEFAULT_PRICING: ModelPricing = { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, +}; + +export const MODEL_PRICING: Record = { + // MiniMax-M3 publishes no separate cache-write rate, so cache writes are + // billed at the input rate. + "MiniMax-M3": { input: 0.6, output: 2.4, cacheRead: 0.12, cacheWrite: 0.6 }, + "MiniMax-M2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0.375 }, +}; + +export function getModelPricing(model: string): ModelPricing { + return MODEL_PRICING[model] || DEFAULT_PRICING; +} + +export function printCost(inputTokens: number, outputTokens: number, cacheRead = 0, cacheCreation = 0, model?: string) { + // Cache read/write and per-token rates are model-specific (see getModelPricing). + const p = model ? getModelPricing(model) : DEFAULT_PRICING; const total = - (inputTokens / 1_000_000) * 3 + - (cacheRead / 1_000_000) * 0.3 + - (cacheCreation / 1_000_000) * 3.75 + - (outputTokens / 1_000_000) * 15; + (inputTokens / 1_000_000) * p.input + + (cacheRead / 1_000_000) * p.cacheRead + + (cacheCreation / 1_000_000) * p.cacheWrite + + (outputTokens / 1_000_000) * p.output; const cacheStr = cacheRead ? `, ${cacheRead} cached` : ""; console.log( chalk.gray(