mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-28 15:45:22 -04:00
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:
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user