fix(kimi): resolve Kimi Code API 403 errors and User-Agent restrictions (#3549)

* fix(kimi): resolve Kimi Code API 403 errors and User-Agent restrictions

Kimi Code subscription keys require a whitelisted coding-agent User-Agent to avoid access_terminated_error 403s. This adds User-Agent probing and caching for Kimi Code endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kimi): omit temperature for kimi-for-coding API calls

Kimi Code rejects any non-default temperature with HTTP 400, which broke deep research probes and low-temp LLM rounds.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
KYDNO
2026-06-15 02:56:54 -04:00
committed by GitHub
parent 674457384a
commit 955455b797
10 changed files with 289 additions and 9 deletions
+32
View File
@@ -0,0 +1,32 @@
"""Kimi Code host-allowlist behavior (follow-up to provider support).
Kimi Code (https://api.kimi.com/coding/v1) is a subscription, OpenAI-compatible
cloud API with native tool-calling. These tests pin the three host-list integrations:
- agent loop sends native tool schemas to Kimi Code (not fenced-block parsing),
- teacher escalation treats Kimi Code as SOTA (loop OFF, no added latency).
"""
from src import agent_loop, teacher_escalation
class TestAgentToolHosts:
def test_kimi_code_in_api_hosts(self):
assert "api.kimi.com" in agent_loop._API_HOSTS
def test_kimi_code_url_matches_api_host(self):
url = "https://api.kimi.com/coding/v1/chat/completions"
assert any(h in url for h in agent_loop._API_HOSTS)
def test_unknown_host_not_matched(self):
url = "https://example.invalid/v1/chat/completions"
assert not any(h in url for h in agent_loop._API_HOSTS)
class TestTeacherEscalationSota:
def test_kimi_code_is_sota_not_self_hosted(self):
assert teacher_escalation.is_self_hosted("https://api.kimi.com/coding/v1/chat/completions") is False
def test_known_cloud_still_sota(self):
assert teacher_escalation.is_self_hosted("https://api.openai.com/v1") is False
def test_local_endpoint_still_self_hosted(self):
assert teacher_escalation.is_self_hosted("http://localhost:8000/v1") is True
+69
View File
@@ -0,0 +1,69 @@
"""Kimi Code User-Agent fallback list and 403 detection."""
from src.llm_core import (
KIMI_CODE_USER_AGENTS,
KIMI_CODE_USER_AGENT,
_is_kimi_code_access_denied,
_is_kimi_code_url,
_kimi_code_base_key,
_kimi_code_ua_cache,
_kimi_code_ua_candidates,
_remember_kimi_code_user_agent,
httpx_post_kimi_aware,
)
class TestKimiCodeUserAgents:
def test_default_is_first_fallback(self):
assert KIMI_CODE_USER_AGENT == KIMI_CODE_USER_AGENTS[0]
def test_multiple_fallbacks_configured(self):
assert len(KIMI_CODE_USER_AGENTS) >= 3
assert "KimiCLI/1.0" in KIMI_CODE_USER_AGENTS
def test_detects_coding_agent_403(self):
body = '{"error":{"message":"only available for Coding Agents","type":"access_terminated_error"}}'
assert _is_kimi_code_access_denied(403, body) is True
def test_non_403_not_access_denied(self):
assert _is_kimi_code_access_denied(401, "unauthorized") is False
def test_ua_candidates_prefers_cache(self):
_kimi_code_ua_cache.clear()
url = "https://api.kimi.com/coding/v1/chat/completions"
_remember_kimi_code_user_agent(url, "Kilo-Code/1.0")
candidates = _kimi_code_ua_candidates(url)
assert candidates[0] == "Kilo-Code/1.0"
assert len(candidates) == len(KIMI_CODE_USER_AGENTS)
_kimi_code_ua_cache.clear()
def test_non_kimi_url_has_no_candidates(self):
assert _kimi_code_ua_candidates("https://api.openai.com/v1") == []
def test_base_key_normalizes_chat_url(self):
assert _kimi_code_base_key("https://api.kimi.com/coding/v1/chat/completions") == (
"https://api.kimi.com/coding/v1"
)
def test_post_retries_next_user_agent_on_403(self, monkeypatch):
_kimi_code_ua_cache.clear()
calls = []
class _Resp:
def __init__(self, status, text=""):
self.status_code = status
self.content = text.encode()
self.text = text
def fake_post(url, headers=None, **kwargs):
calls.append(headers.get("User-Agent"))
if headers.get("User-Agent") == KIMI_CODE_USER_AGENTS[0]:
return _Resp(403, '{"error":{"type":"access_terminated_error"}}')
return _Resp(200, "{}")
monkeypatch.setattr("src.llm_core.httpx.post", fake_post)
url = "https://api.kimi.com/coding/v1/chat/completions"
r = httpx_post_kimi_aware(url, {"Authorization": "Bearer x"}, json={})
assert r.status_code == 200
assert calls[0] == KIMI_CODE_USER_AGENTS[0]
assert calls[1] == KIMI_CODE_USER_AGENTS[1]
_kimi_code_ua_cache.clear()
+7 -1
View File
@@ -14,7 +14,7 @@ from src import llm_core
@pytest.mark.parametrize(
"model",
["o1", "o1-mini", "o3", "o3-mini", "o4-mini", "gpt-5", "gpt-5-mini",
"openrouter/openai/o3-mini", "OpenAI/GPT-5"],
"openrouter/openai/o3-mini", "OpenAI/GPT-5", "kimi-for-coding"],
)
def test_reasoning_models_restrict_temperature(model):
assert llm_core._restricts_temperature(model) is True
@@ -62,6 +62,12 @@ def test_reasoning_model_payload_omits_temperature(monkeypatch):
assert payload["max_completion_tokens"] == 5
def test_kimi_for_coding_payload_omits_temperature(monkeypatch):
payload = _capture_openai_payload(monkeypatch, "kimi-for-coding", 0.1)
assert "temperature" not in payload
assert payload["max_tokens"] == 5
def test_normal_model_payload_keeps_temperature(monkeypatch):
payload = _capture_openai_payload(monkeypatch, "gpt-4o", 0.2)
assert payload["temperature"] == 0.2
+9
View File
@@ -205,6 +205,9 @@ class TestMatchProviderCurated:
def test_ollama_url(self):
assert _match_provider_curated("https://ollama.com/api", "openai") == "ollama"
def test_kimi_code_url(self):
assert _match_provider_curated("https://api.kimi.com/coding/v1", "openai") == "kimi-code"
def test_no_url_match_returns_provider(self):
assert _match_provider_curated("https://localhost:1234", "openai") == "openai"
@@ -312,6 +315,12 @@ class TestCurateModels:
assert curated == models
assert extra == []
def test_kimi_code_partitions(self):
models = ["kimi-for-coding", "other-model"]
curated, extra = _curate_models(models, "kimi-code")
assert "kimi-for-coding" in curated
assert "other-model" in extra
def test_curated_sorted_by_priority(self):
models = ["gpt-4o-mini", "gpt-4o", "o3"]
curated, _ = _curate_models(models, "openai")