mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(models): complete kimi-k3 rollout across Kimi-direct catalog surfaces
Follow-up widening for salvaged PRs #67115, #67685, #67620: - _PROVIDER_MODELS: add kimi-k3 atop kimi-coding / moonshot / opencode-go curated lists (kimi-coding-cn covered by cherry-picked #67620) - setup.py _DEFAULT_PROVIDER_MODELS: kimi-k3 for kimi-coding(-cn) + opencode-go - model_metadata: align DEFAULT_CONTEXT_LENGTHS kimi-k3 entry to 1,048,576 (matches endpoint-scoped override, models.dev, and OpenRouter live metadata) - anthropic_adapter: classify the bare Coding Plan slug 'k3' (and k3.x/k3-*) as Kimi family so adaptive thinking applies on proxied endpoints - moonshot_schema: is_moonshot_model matches bare 'k3' so tool-schema sanitization runs on the chat-completions path - contributor mappings for githubespresso407, datachainsystems, Punyko8 Tests: 582 passed across 11 targeted files; hermetic E2E verifies picker order (kimi-k3 first), no dupes, and 1M context resolution.
This commit is contained in:
parent
495d4acec5
commit
25eafd7d71
12 changed files with 82 additions and 25 deletions
|
|
@ -457,7 +457,8 @@ def _is_kimi_coding_endpoint(base_url: str | None) -> bool:
|
|||
|
||||
# Model-name prefixes that identify the Kimi / Moonshot family. Covers
|
||||
# - official slugs: ``kimi-k2.5``, ``kimi_thinking``, ``moonshot-v1-8k``
|
||||
# - common release lines: ``k1.5-...``, ``k2-thinking``, ``k25-...``, ``k2.5-...``
|
||||
# - common release lines: ``k1.5-...``, ``k2-thinking``, ``k25-...``, ``k2.5-...``,
|
||||
# and the bare Coding Plan slug ``k3`` (plus ``k3.x``/``k3-...`` variants)
|
||||
# Matched case-insensitively against the post-``normalize_model_name`` form,
|
||||
# so a caller's ``provider/vendor/model`` slug is handled the same as a
|
||||
# bare name.
|
||||
|
|
@ -467,8 +468,14 @@ _KIMI_FAMILY_MODEL_PREFIXES = (
|
|||
"k1.", "k1-",
|
||||
"k2.", "k2-",
|
||||
"k25", "k2.5",
|
||||
"k3.", "k3-",
|
||||
)
|
||||
|
||||
# Bare release slugs with no separator suffix (Kimi Coding Plan serves K3
|
||||
# as the exact slug ``k3``). Kept exact-match so unrelated model names that
|
||||
# merely start with the same characters don't get misclassified.
|
||||
_KIMI_FAMILY_EXACT_SLUGS = frozenset({"k3"})
|
||||
|
||||
|
||||
def _model_name_is_kimi_family(model: str | None) -> bool:
|
||||
if not isinstance(model, str):
|
||||
|
|
@ -479,6 +486,8 @@ def _model_name_is_kimi_family(model: str | None) -> bool:
|
|||
# Strip vendor prefix (e.g. ``moonshotai/kimi-k2.5`` → ``kimi-k2.5``)
|
||||
if "/" in m:
|
||||
m = m.rsplit("/", 1)[-1]
|
||||
if m in _KIMI_FAMILY_EXACT_SLUGS:
|
||||
return True
|
||||
return m.startswith(_KIMI_FAMILY_MODEL_PREFIXES)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -319,10 +319,12 @@ DEFAULT_CONTEXT_LENGTHS = {
|
|||
"grok-3": 131072, # grok-3, grok-3-mini, grok-3-fast, grok-3-mini-fast
|
||||
"grok-2": 131072, # grok-2, grok-2-1212, grok-2-latest
|
||||
"grok": 131072, # catch-all (grok-beta, unknown grok-*)
|
||||
# Kimi — K3 ships with a 1M context window (verified: platform.kimi.ai/docs/overview).
|
||||
# Longest-key-first substring matching ensures "kimi-k3" resolves to 1M
|
||||
# while older/unknown Kimi models still hit the generic 256K fallback.
|
||||
"kimi-k3": 1_000_000,
|
||||
# Kimi — K3 ships with a 1 Mi context window (1,048,576; verified against
|
||||
# models.dev and OpenRouter live metadata, matching the endpoint-scoped
|
||||
# override in _endpoint_scoped_context_length). Longest-key-first substring
|
||||
# matching ensures "kimi-k3" resolves to 1M while older/unknown Kimi models
|
||||
# still hit the generic 256K fallback.
|
||||
"kimi-k3": 1_048_576,
|
||||
"kimi": 262144,
|
||||
# Upstage Solar — api.upstage.ai/v1/models does not return context_length,
|
||||
# so these fallbacks keep token budgeting / compression from probing down
|
||||
|
|
|
|||
|
|
@ -232,6 +232,10 @@ def is_moonshot_model(model: str | None) -> bool:
|
|||
tail = bare.rsplit("/", 1)[-1]
|
||||
if tail.startswith("kimi-") or tail == "kimi":
|
||||
return True
|
||||
# Kimi Coding Plan serves K3 under the bare slug ``k3`` (plus dated /
|
||||
# suffixed variants like ``k3.1`` or ``k3-turbo``).
|
||||
if tail == "k3" or tail.startswith(("k3.", "k3-")):
|
||||
return True
|
||||
# Vendor-prefixed forms commonly used on aggregators
|
||||
if "moonshot" in bare or "/kimi" in bare or bare.startswith("kimi"):
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
githubespresso407
|
||||
1
contributors/emails/info@datachainsystems.com
Normal file
1
contributors/emails/info@datachainsystems.com
Normal file
|
|
@ -0,0 +1 @@
|
|||
datachainsystems
|
||||
1
contributors/emails/punyko8@users.noreply.github.com
Normal file
1
contributors/emails/punyko8@users.noreply.github.com
Normal file
|
|
@ -0,0 +1 @@
|
|||
Punyko8
|
||||
|
|
@ -319,6 +319,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||
"minimaxai/minimax-m3",
|
||||
],
|
||||
"kimi-coding": [
|
||||
"kimi-k3",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
|
|
@ -344,6 +345,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||
"step-3.5-flash-2603",
|
||||
],
|
||||
"moonshot": [
|
||||
"kimi-k3",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"kimi-k2-thinking",
|
||||
|
|
@ -466,6 +468,7 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|||
"nemotron-3-ultra-free",
|
||||
],
|
||||
"opencode-go": [
|
||||
"kimi-k3",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
|
|
|
|||
|
|
@ -100,15 +100,15 @@ _DEFAULT_PROVIDER_MODELS = {
|
|||
"google/gemini-2.5-pro", "google/gemini-2.5-flash",
|
||||
],
|
||||
"zai": ["glm-5.2", "glm-5.1", "glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"],
|
||||
"kimi-coding": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
|
||||
"kimi-coding-cn": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
|
||||
"kimi-coding": ["kimi-k3", "kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
|
||||
"kimi-coding-cn": ["kimi-k3", "kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"],
|
||||
"stepfun": ["step-3.5-flash", "step-3.5-flash-2603"],
|
||||
"arcee": ["trinity-large-thinking", "trinity-large-preview", "trinity-mini"],
|
||||
"minimax": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
|
||||
"minimax-cn": ["MiniMax-M2.7", "MiniMax-M2.5", "MiniMax-M2.1", "MiniMax-M2"],
|
||||
"kilocode": ["anthropic/claude-sonnet-5", "anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
||||
"opencode-zen": ["gpt-5.4", "gpt-5.3-codex", "claude-sonnet-5", "claude-sonnet-4-6", "gemini-3-flash", "glm-5", "kimi-k2.5", "minimax-m2.7"],
|
||||
"opencode-go": ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5", "qwen3.7-max", "qwen3.6-plus", "qwen3.5-plus"],
|
||||
"opencode-go": ["kimi-k3", "kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5", "qwen3.7-max", "qwen3.6-plus", "qwen3.5-plus"],
|
||||
"huggingface": [
|
||||
"Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct", "deepseek-ai/DeepSeek-R1-0528",
|
||||
|
|
|
|||
|
|
@ -1631,6 +1631,22 @@ class TestBuildAnthropicKwargs:
|
|||
assert _supports_xhigh_effort(m) is True, m
|
||||
assert _forbids_sampling_params(m) is False, m
|
||||
|
||||
def test_bare_k3_coding_plan_slug_is_kimi_family(self):
|
||||
"""Kimi Coding Plan serves K3 as the bare slug ``k3`` — it must be
|
||||
classified as Kimi family (adaptive thinking) even on proxied
|
||||
endpoints where only the model name is available. Lookalike
|
||||
non-Kimi names must NOT match the exact-slug rule."""
|
||||
from agent.anthropic_adapter import (
|
||||
_model_name_is_kimi_family,
|
||||
_supports_adaptive_thinking,
|
||||
)
|
||||
for m in ("k3", "K3", "moonshotai/k3", "k3.1-preview", "k3-turbo"):
|
||||
assert _model_name_is_kimi_family(m) is True, m
|
||||
assert _supports_adaptive_thinking("k3") is True
|
||||
# Prefix-lookalikes without a separator must not be swept in.
|
||||
for m in ("k30", "k3000-chat", "keras-3"):
|
||||
assert _model_name_is_kimi_family(m) is False, m
|
||||
|
||||
def test_fast_mode_omitted_for_unsupported_model(self):
|
||||
"""fast_mode=True on Opus 4.7 must NOT inject speed=fast (API 400s)."""
|
||||
kwargs = build_anthropic_kwargs(
|
||||
|
|
|
|||
|
|
@ -175,7 +175,15 @@ class TestEstimateRequestTokensRough:
|
|||
|
||||
class TestDefaultContextLengths:
|
||||
def test_k3_context_is_scoped_to_confirmed_coding_endpoint(self):
|
||||
"""K3's 1 Mi context must not leak to unverified Moonshot endpoints."""
|
||||
"""The bare ``k3`` slug's 1 Mi context must not leak to unverified endpoints.
|
||||
|
||||
The named ``kimi-k3`` / ``kimi-k3-cot`` slugs resolve to 1 Mi
|
||||
EVERYWHERE via DEFAULT_CONTEXT_LENGTHS — the window is a property of
|
||||
the model, served at 1M on api.moonshot.ai and api.moonshot.cn alike
|
||||
(verified against models.dev + OpenRouter live metadata). Only the
|
||||
bare ``k3`` slug, which exists solely on the Kimi Coding Plan
|
||||
endpoint, stays endpoint-scoped.
|
||||
"""
|
||||
with patch("agent.model_metadata.get_cached_context_length", return_value=None), \
|
||||
patch("agent.model_metadata.fetch_model_metadata", return_value={}), \
|
||||
patch("agent.model_metadata.fetch_endpoint_model_metadata", return_value={}), \
|
||||
|
|
@ -205,10 +213,16 @@ class TestDefaultContextLengths:
|
|||
) == 1_048_576
|
||||
|
||||
for base_url in rejected_urls:
|
||||
for model in ("k3", "kimi-k3", "kimi-k3-cot"):
|
||||
# Bare slug: endpoint-scoped, must NOT leak off-endpoint.
|
||||
assert get_model_context_length(
|
||||
"k3", provider="kimi-coding", base_url=base_url
|
||||
) != 1_048_576
|
||||
# Named slugs: global DEFAULT_CONTEXT_LENGTHS entry applies
|
||||
# everywhere the model is actually named kimi-k3.
|
||||
for model in ("kimi-k3", "kimi-k3-cot"):
|
||||
assert get_model_context_length(
|
||||
model, provider="kimi-coding", base_url=base_url
|
||||
) != 1_048_576
|
||||
) == 1_048_576
|
||||
|
||||
def test_grok_substring_matching(self):
|
||||
# Longest-first substring matching must resolve the real xAI model
|
||||
|
|
@ -335,24 +349,25 @@ class TestDefaultContextLengths:
|
|||
def test_kimi_k3_context_1m(self):
|
||||
"""Kimi K3 must resolve to 1M, not the generic Kimi fallback of 256K.
|
||||
|
||||
Context window verified against platform.kimi.ai/docs/overview
|
||||
(2026-07). Kimi K3 is the current flagship model with a 1M-token
|
||||
context window.
|
||||
Context window verified against models.dev and OpenRouter live
|
||||
metadata (1,048,576; 2026-07). Kimi K3 is the current flagship model
|
||||
with a 1M-token context window — the value matches the
|
||||
endpoint-scoped override in _endpoint_scoped_context_length.
|
||||
"""
|
||||
from agent.model_metadata import get_model_context_length
|
||||
from unittest.mock import patch as mock_patch
|
||||
|
||||
assert DEFAULT_CONTEXT_LENGTHS["kimi-k3"] == 1_000_000
|
||||
assert DEFAULT_CONTEXT_LENGTHS["kimi-k3"] == 1_048_576
|
||||
assert DEFAULT_CONTEXT_LENGTHS["kimi"] == 262144
|
||||
|
||||
with mock_patch("agent.model_metadata.fetch_model_metadata", return_value={}), \
|
||||
mock_patch("agent.model_metadata.fetch_endpoint_model_metadata", return_value={}), \
|
||||
mock_patch("agent.model_metadata.get_cached_context_length", return_value=None):
|
||||
# Kimi K3 (1M) must NOT fall through to the generic 256K entry
|
||||
assert get_model_context_length("kimi-k3") == 1_000_000
|
||||
assert get_model_context_length("kimi-k3") == 1_048_576
|
||||
# Vendor-prefixed forms (kimi provider, openrouter)
|
||||
assert get_model_context_length("kimi/kimi-k3") == 1_000_000
|
||||
assert get_model_context_length("moonshotai/kimi-k3") == 1_000_000
|
||||
assert get_model_context_length("kimi/kimi-k3") == 1_048_576
|
||||
assert get_model_context_length("moonshotai/kimi-k3") == 1_048_576
|
||||
# Older/unknown Kimi models still resolve to 256K fallback
|
||||
assert get_model_context_length("kimi-k2.6") == 262144
|
||||
assert get_model_context_length("kimi-k2") == 262144
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ class TestMoonshotModelDetection:
|
|||
[
|
||||
"kimi-k2.6",
|
||||
"kimi-k2-thinking",
|
||||
"k3",
|
||||
"K3",
|
||||
"moonshotai/k3",
|
||||
"k3.1-preview",
|
||||
"moonshotai/Kimi-K2.6",
|
||||
"moonshotai/kimi-k2.6",
|
||||
"nous/moonshotai/kimi-k2.6",
|
||||
|
|
|
|||
|
|
@ -97,14 +97,15 @@ class TestProviderModelIdsPreferred:
|
|||
assert "claude-opus-4-7" in out
|
||||
assert "kimi-k2.6" in out
|
||||
|
||||
def test_kimi_coding_offline_catalog_includes_k2_7_code(self):
|
||||
"""Native Kimi users must see the newest Code model without live catalog help."""
|
||||
def test_kimi_coding_offline_catalog_includes_k3(self):
|
||||
"""Native Kimi users must see the newest models without live catalog help."""
|
||||
assert "kimi-coding" not in _MODELS_DEV_PREFERRED
|
||||
with patch("agent.models_dev.list_agentic_models", return_value=[]):
|
||||
out = provider_model_ids("kimi-coding")
|
||||
assert "kimi-k3" in out
|
||||
assert "kimi-k2.7-code" in out
|
||||
|
||||
def test_kimi_coding_live_catalog_does_not_hide_curated_k2_7_code(self):
|
||||
def test_kimi_coding_live_catalog_does_not_hide_curated_k3(self):
|
||||
"""Kimi /models can lag inference; live results must not replace curated."""
|
||||
with (
|
||||
patch(
|
||||
|
|
@ -114,8 +115,8 @@ class TestProviderModelIdsPreferred:
|
|||
patch("providers.base.ProviderProfile.fetch_models", return_value=["kimi-k2.6"]),
|
||||
):
|
||||
out = provider_model_ids("kimi-coding")
|
||||
# Curated-first order; curated newest (k2.7-code) stays ahead of live.
|
||||
assert out[:2] == ["kimi-k2.7-code", "kimi-k2.6"]
|
||||
# Curated-first order; curated newest (k3) stays ahead of live.
|
||||
assert out[:3] == ["kimi-k3", "kimi-k2.7-code", "kimi-k2.6"]
|
||||
|
||||
def test_k3_live_discovery_is_scoped_to_kimi_coding_endpoint(self):
|
||||
"""Coding keys discover K3; legacy Moonshot keys must not advertise it."""
|
||||
|
|
@ -171,7 +172,7 @@ class TestProviderModelIdsPreferred:
|
|||
custom_models = provider_model_ids("kimi-coding")
|
||||
|
||||
assert "k3" in coding_models
|
||||
assert coding_models[0] == "kimi-k2.7-code"
|
||||
assert coding_models[0] == "kimi-k3"
|
||||
assert all(model.lower() != "k3" for model in legacy_models)
|
||||
assert all(model.lower() != "k3" for model in custom_models)
|
||||
|
||||
|
|
@ -194,7 +195,7 @@ class TestProviderModelIdsPreferred:
|
|||
_model_flow_kimi({}, current_model="")
|
||||
|
||||
assert captured["models"] == _PROVIDER_MODELS["kimi-coding"]
|
||||
assert captured["models"][0] == "kimi-k2.7-code"
|
||||
assert captured["models"][0] == "kimi-k3"
|
||||
|
||||
|
||||
class TestOpenRouterAndNousUnchanged:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue