Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ Give it a model and a key and it goes. It speaks the OpenAI-compatible API by de
|---|---|
| OpenAI (default `gpt-5.5`) | `OPENAI_API_KEY=sk-...` |
| DeepSeek | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com CORECODER_MODEL=deepseek-chat` |
| MiniMax (global, OpenAI) | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.minimax.io/v1 CORECODER_MODEL=MiniMax-M3 CORECODER_MAX_CONTEXT=1000000` |
| MiniMax (China, OpenAI) | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.minimaxi.com/v1 CORECODER_MODEL=MiniMax-M3 CORECODER_MAX_CONTEXT=1000000` |
| MiniMax (global, Anthropic via LiteLLM) | `CORECODER_API_KEY=sk-... CORECODER_BASE_URL=https://api.minimax.io/anthropic CORECODER_MODEL=anthropic/MiniMax-M3 CORECODER_MAX_CONTEXT=1000000 CORECODER_PROVIDER=litellm` |
| MiniMax (China, Anthropic via LiteLLM) | `CORECODER_API_KEY=sk-... CORECODER_BASE_URL=https://api.minimaxi.com/anthropic CORECODER_MODEL=anthropic/MiniMax-M3 CORECODER_MAX_CONTEXT=1000000 CORECODER_PROVIDER=litellm` |
| Local Ollama | `OPENAI_API_KEY=ollama OPENAI_BASE_URL=http://localhost:11434/v1 CORECODER_MODEL=qwen2.5-coder` |

MiniMax-M2.7 is available through the same four endpoints. Replace `MiniMax-M3` with `MiniMax-M2.7` and set `CORECODER_MAX_CONTEXT=204800`; keep the `anthropic/` prefix when using LiteLLM. The Anthropic-compatible examples require `pip install "corecoder[litellm]"`.

Kimi, Qwen and the like are the same two variables; for providers that don't even offer an OpenAI-compatible endpoint, the optional LiteLLM backend (`pip install "corecoder[litellm]"`) routes to a hundred-plus of them. The third essay goes into this in detail. The key can be `export`ed directly or dropped into a `.env` at the project root, which is loaded on startup. Then:

```bash
Expand Down
6 changes: 6 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ pip install -e .
|---|---|
| OpenAI(默认 `gpt-5.5`) | `OPENAI_API_KEY=sk-...` |
| DeepSeek | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.deepseek.com CORECODER_MODEL=deepseek-chat` |
| MiniMax (global, OpenAI) | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.minimax.io/v1 CORECODER_MODEL=MiniMax-M3 CORECODER_MAX_CONTEXT=1000000` |
| MiniMax (China, OpenAI) | `OPENAI_API_KEY=sk-... OPENAI_BASE_URL=https://api.minimaxi.com/v1 CORECODER_MODEL=MiniMax-M3 CORECODER_MAX_CONTEXT=1000000` |
| MiniMax (global, Anthropic via LiteLLM) | `CORECODER_API_KEY=sk-... CORECODER_BASE_URL=https://api.minimax.io/anthropic CORECODER_MODEL=anthropic/MiniMax-M3 CORECODER_MAX_CONTEXT=1000000 CORECODER_PROVIDER=litellm` |
| MiniMax (China, Anthropic via LiteLLM) | `CORECODER_API_KEY=sk-... CORECODER_BASE_URL=https://api.minimaxi.com/anthropic CORECODER_MODEL=anthropic/MiniMax-M3 CORECODER_MAX_CONTEXT=1000000 CORECODER_PROVIDER=litellm` |
| 本地 Ollama | `OPENAI_API_KEY=ollama OPENAI_BASE_URL=http://localhost:11434/v1 CORECODER_MODEL=qwen2.5-coder` |

MiniMax-M2.7 is available through the same four endpoints. Replace `MiniMax-M3` with `MiniMax-M2.7` and set `CORECODER_MAX_CONTEXT=204800`; keep the `anthropic/` prefix when using LiteLLM. The Anthropic-compatible examples require `pip install "corecoder[litellm]"`.

Kimi、Qwen 这些同样是改这两个变量;连 OpenAI 兼容接口都不给的 provider,装上可选的 LiteLLM 后端(`pip install "corecoder[litellm]"`)能路由一百多家。第三篇文章把这块讲得更细。key 可以直接 `export`,也可以在项目根目录扔个 `.env`,启动时自动加载。然后:

```bash
Expand Down
88 changes: 75 additions & 13 deletions corecoder/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ def message(self) -> dict:
return msg


# pricing per million tokens: (input, output)
# pricing per million tokens: (input, output, optional cache read, optional cache write)
# sources: openai.com/api/pricing, api-docs.deepseek.com, platform.claude.com,
# platform.moonshot.ai, alibabacloud.com/help/en/model-studio
# platform.moonshot.ai, alibabacloud.com/help/en/model-studio, platform.minimax.io
_PRICING = {
# OpenAI - current flagships
"gpt-5.5": (5, 30),
Expand All @@ -78,9 +78,55 @@ def message(self) -> dict:
"qwen-max": (0.78, 3.9),
# Moonshot Kimi
"kimi-k2.5": (0.6, 3),
# MiniMax
"MiniMax-M2.7": (0.3, 1.2, 0.06, 0.375),
}


# tier entries: (maximum input tokens, input, output, cache read, cache write)
_TIERED_PRICING = {
"MiniMax-M3": {
"standard": (
(512_000, 0.3, 1.2, 0.06, None),
(None, 0.6, 2.4, 0.12, None),
),
"priority": (
(512_000, 0.45, 1.8, 0.09, None),
(None, 0.9, 3.6, 0.18, None),
),
},
}


def _pricing_key(model: str) -> str:
"""Strip a LiteLLM provider prefix before looking up model pricing."""
return model.rsplit("/", 1)[-1]


def _cost_for_usage(
model: str,
prompt_tokens: int,
completion_tokens: int,
service_tier: str = "standard",
) -> float | None:
key = _pricing_key(model)
pricing = _PRICING.get(key)
if pricing:
input_rate, output_rate = pricing[:2]
else:
model_tiers = _TIERED_PRICING.get(key)
if not model_tiers:
return None
tiers = model_tiers.get(service_tier, model_tiers["standard"])
_, input_rate, output_rate, _, _ = next(
tier for tier in tiers if tier[0] is None or prompt_tokens <= tier[0]
)
return (
prompt_tokens * input_rate / 1_000_000
+ completion_tokens * output_rate / 1_000_000
)


class LLM:
def __init__(
self,
Expand All @@ -94,19 +140,34 @@ def __init__(
self.extra = kwargs # temperature, max_tokens, etc.
self.total_prompt_tokens = 0
self.total_completion_tokens = 0
self._estimated_cost = _cost_for_usage(
model, 0, 0, kwargs.get("service_tier", "standard")
)

@property
def estimated_cost(self) -> float | None:
"""Rough cost estimate in USD. Returns None if model not in pricing table."""
pricing = _PRICING.get(self.model)
if not pricing:
return None
input_rate, output_rate = pricing
return (
self.total_prompt_tokens * input_rate / 1_000_000
+ self.total_completion_tokens * output_rate / 1_000_000
if "_estimated_cost" in self.__dict__:
return self._estimated_cost
return _cost_for_usage(
self.model,
self.total_prompt_tokens,
self.total_completion_tokens,
getattr(self, "extra", {}).get("service_tier", "standard"),
)

def _record_usage(self, prompt_tokens: int, completion_tokens: int):
self.total_prompt_tokens += prompt_tokens
self.total_completion_tokens += completion_tokens
request_cost = _cost_for_usage(
self.model,
prompt_tokens,
completion_tokens,
self.extra.get("service_tier", "standard"),
)
if request_cost is not None:
self._estimated_cost = (self._estimated_cost or 0.0) + request_cost

def chat(
self,
messages: list[dict],
Expand Down Expand Up @@ -180,8 +241,7 @@ def chat(
args = {}
parsed.append(ToolCall(id=raw["id"], name=raw["name"], arguments=args))

self.total_prompt_tokens += prompt_tok
self.total_completion_tokens += completion_tok
self._record_usage(prompt_tok, completion_tok)

return LLMResponse(
content="".join(content_parts),
Expand Down Expand Up @@ -236,6 +296,9 @@ def __init__(
self.extra = kwargs
self.total_prompt_tokens = 0
self.total_completion_tokens = 0
self._estimated_cost = _cost_for_usage(
model, 0, 0, kwargs.get("service_tier", "standard")
)

def chat(
self,
Expand Down Expand Up @@ -300,8 +363,7 @@ def chat(
args = {}
parsed.append(ToolCall(id=raw["id"], name=raw["name"], arguments=args))

self.total_prompt_tokens += prompt_tok
self.total_completion_tokens += completion_tok
self._record_usage(prompt_tok, completion_tok)

return LLMResponse(
content="".join(content_parts),
Expand Down
57 changes: 57 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for core modules: config, context, session, imports."""

import pytest

from corecoder import Agent, LLM, Config, ALL_TOOLS, __version__
from corecoder import session as session_module
from corecoder.context import ContextManager, estimate_tokens
Expand Down Expand Up @@ -150,6 +152,61 @@ def test_cost_estimation_unknown_model():
assert llm.estimated_cost is None


def test_minimax_pricing_registry_preserves_official_tiers():
from corecoder.llm import _PRICING, _TIERED_PRICING

assert _PRICING["MiniMax-M2.7"] == (0.3, 1.2, 0.06, 0.375)
assert _TIERED_PRICING["MiniMax-M3"] == {
"standard": (
(512_000, 0.3, 1.2, 0.06, None),
(None, 0.6, 2.4, 0.12, None),
),
"priority": (
(512_000, 0.45, 1.8, 0.09, None),
(None, 0.9, 3.6, 0.18, None),
),
}


@pytest.mark.parametrize(
("model", "prompt_tokens", "service_tier", "expected"),
[
("MiniMax-M3", 500_000, "standard", 0.27),
("MiniMax-M3", 600_000, "standard", 0.60),
("MiniMax-M3", 500_000, "priority", 0.405),
("anthropic/MiniMax-M3", 600_000, "priority", 0.90),
],
)
def test_minimax_m3_cost_estimation_uses_request_tier(
model, prompt_tokens, service_tier, expected
):
from corecoder.llm import LLM

llm = LLM.__new__(LLM)
llm.model = model
llm.total_prompt_tokens = prompt_tokens
llm.total_completion_tokens = 100_000
llm.extra = {"service_tier": service_tier}
assert llm.estimated_cost == pytest.approx(expected)


def test_minimax_m3_cost_tracks_each_request_separately():
from corecoder.llm import LLM

llm = LLM.__new__(LLM)
llm.model = "MiniMax-M3"
llm.extra = {}
llm.total_prompt_tokens = 0
llm.total_completion_tokens = 0
llm._estimated_cost = 0.0

llm._record_usage(400_000, 100_000)
llm._record_usage(400_000, 100_000)

assert llm.total_prompt_tokens == 800_000
assert llm.estimated_cost == pytest.approx(0.48)


# --- Changed files tracking ---

def test_edit_tracks_changed_files(tmp_path):
Expand Down