feat: support Kimi K3
This commit is contained in:
committed by
Xubin Ren
parent
b76d54aae1
commit
8c68c6fe1e
@@ -59,6 +59,7 @@ _DEFAULT_OPENROUTER_HEADERS = {
|
||||
"X-OpenRouter-Title": "nanobot",
|
||||
"X-OpenRouter-Categories": "cli-agent,personal-agent",
|
||||
}
|
||||
_KIMI_K3_MODEL = "kimi-k3"
|
||||
_KIMI_THINKING_MODELS: frozenset[str] = frozenset({
|
||||
"kimi-k2.5",
|
||||
"kimi-k2.6",
|
||||
@@ -113,9 +114,9 @@ def _provider_prefix_key(name: str) -> str:
|
||||
|
||||
|
||||
def _requires_max_completion_tokens(model_name: str) -> bool:
|
||||
"""Return True for models that reject ``max_tokens`` (GPT-5 family, o-series)."""
|
||||
"""Return True for models that require ``max_completion_tokens``."""
|
||||
slug = _model_slug(model_name)
|
||||
return "gpt-5" in slug or any(
|
||||
return slug == _KIMI_K3_MODEL or "gpt-5" in slug or any(
|
||||
slug == p or slug.startswith((p + "-", p + ".")) for p in ("o1", "o3", "o4")
|
||||
)
|
||||
|
||||
@@ -716,9 +717,12 @@ class OpenAICompatProvider(LLMProvider):
|
||||
) -> bool:
|
||||
"""Return True when the model accepts a temperature parameter.
|
||||
|
||||
GPT-5 family and reasoning models (o1/o3/o4) reject temperature
|
||||
when reasoning_effort is set to anything other than ``"none"``.
|
||||
Kimi K3 uses a fixed temperature that should be omitted. GPT-5 family
|
||||
and reasoning models (o1/o3/o4) reject temperature when
|
||||
reasoning_effort is set to anything other than ``"none"``.
|
||||
"""
|
||||
if _model_slug(model_name) == _KIMI_K3_MODEL:
|
||||
return False
|
||||
if reasoning_effort and reasoning_effort.lower() != "none":
|
||||
return False
|
||||
name = model_name.lower()
|
||||
@@ -788,6 +792,17 @@ class OpenAICompatProvider(LLMProvider):
|
||||
semantic_effort = "minimal"
|
||||
|
||||
wire_effort = reasoning_effort
|
||||
slug = _model_slug(model_name)
|
||||
if slug == _KIMI_K3_MODEL and semantic_effort is not None:
|
||||
# K3 always reasons and currently accepts only the top-level
|
||||
# reasoning_effort="max". Preserve disabled/default semantics by
|
||||
# omitting the field; normalize older enabled presets to "max" so
|
||||
# switching from a K2.x model does not send an unsupported value.
|
||||
if semantic_effort in ("none", "minimal"):
|
||||
wire_effort = None
|
||||
else:
|
||||
semantic_effort = "max"
|
||||
wire_effort = "max"
|
||||
if spec and spec.name == "dashscope" and semantic_effort == "minimal":
|
||||
# DashScope accepts none/minimum/low/medium/high/xhigh; "minimal" 400s.
|
||||
wire_effort = "minimum"
|
||||
@@ -825,7 +840,6 @@ class OpenAICompatProvider(LLMProvider):
|
||||
# Only send thinking controls when reasoning_effort is explicit so
|
||||
# omitting the config preserves each provider's default.
|
||||
if reasoning_effort is not None:
|
||||
slug = _model_slug(model_name)
|
||||
thinking_enabled = semantic_effort not in ("none", "minimal")
|
||||
for thinking_style in _thinking_styles_for(spec, model_name):
|
||||
if not thinking_enabled and slug in _KIMI_ALWAYS_THINKING_MODELS:
|
||||
|
||||
@@ -121,7 +121,7 @@ _IMAGE_GENERATION_ASPECT_RATIOS = {
|
||||
"2:3",
|
||||
"21:9",
|
||||
}
|
||||
_CONTEXT_WINDOW_TOKEN_OPTIONS = {65_536, 200_000, 262_144}
|
||||
_CONTEXT_WINDOW_TOKEN_OPTIONS = {65_536, 200_000, 262_144, 1_048_576}
|
||||
_MODEL_CONFIGURATION_SLUG_RE = re.compile(r"[^a-z0-9_-]+")
|
||||
_ENV_REF_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
||||
|
||||
@@ -634,7 +634,9 @@ def _parse_context_window_tokens(value: str | None) -> int | None:
|
||||
except ValueError:
|
||||
raise WebUISettingsError("context_window_tokens must be an integer") from None
|
||||
if parsed not in _CONTEXT_WINDOW_TOKEN_OPTIONS:
|
||||
raise WebUISettingsError("context_window_tokens must be 65536, 200000, or 262144")
|
||||
raise WebUISettingsError(
|
||||
"context_window_tokens must be 65536, 200000, 262144, or 1048576"
|
||||
)
|
||||
return parsed
|
||||
|
||||
|
||||
@@ -706,6 +708,10 @@ def _reasoning_effort_values_for(provider_name: str, model: str) -> list[str]:
|
||||
return list(_DEFAULT_REASONING_EFFORT_VALUES)
|
||||
|
||||
model_lower = (model or "").lower()
|
||||
if model_lower.rsplit("/", 1)[-1] == "kimi-k3":
|
||||
# K3 always reasons and currently exposes only its default/max effort.
|
||||
return ["", "max"]
|
||||
|
||||
implicit = getattr(spec, "implicit_reasoning_models", ())
|
||||
if implicit and any(pat in model_lower for pat in implicit):
|
||||
# Reasoning is always on; only "Default" makes sense.
|
||||
|
||||
Reference in New Issue
Block a user