fix(providers): apply Kimi Coding default headers

This commit is contained in:
Xubin Ren
2026-06-24 10:43:16 +08:00
parent 9d6c606cc9
commit f9afc9389b
5 changed files with 77 additions and 16 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ Tracing covers the providers that go through nanobot's OpenAI-compatible client
> - **MiniMax Coding Plan**: Exclusive discount links for the nanobot community: [Overseas](https://platform.minimax.io/subscribe/coding-plan?code=9txpdXw04g&source=link) · [Mainland China](https://platform.minimaxi.com/subscribe/token-plan?code=GILTJpMTqZ&source=link)
> - **MiniMax (Mainland China)**: If your API key is from MiniMax's mainland China platform (minimaxi.com), set `"apiBase": "https://api.minimaxi.com/v1"` in your minimax provider config.
> - **MiniMax thinking mode**: `providers.minimaxAnthropic` is the config block for `reasoningEffort` / thinking mode. MiniMax exposes that capability through its Anthropic-compatible endpoint, so nanobot keeps it as a separate provider instead of guessing MiniMax-specific thinking parameters on the generic OpenAI-compatible `minimax` endpoint. It uses the same `MINIMAX_API_KEY`. Default Anthropic-compatible base URL: `https://api.minimax.io/anthropic`; for mainland China use `https://api.minimaxi.com/anthropic`.
> - **Kimi Coding Plan**: Use `providers.kimiCoding` with `provider: "kimi_coding"` for Kimi's dedicated Anthropic Messages API endpoint. The endpoint **requires** `extraHeaders.User-Agent` set to a Claude-compatible value such as `claude-code/0.1.0`; omitting it returns `403`.
> - **Kimi Coding Plan**: Use `providers.kimiCoding` with `provider: "kimi_coding"` for Kimi's dedicated Anthropic Messages API endpoint. The endpoint requires a Claude-compatible `User-Agent`; nanobot sends `claude-code/0.1.0` by default, and you can override it with `extraHeaders.User-Agent` if your account requires a different value.
> - **VolcEngine / BytePlus Coding Plan**: Subscription endpoints are configured through dedicated providers `volcengineCodingPlan` or `byteplusCodingPlan`, separate from the pay-per-use `volcengine` / `byteplus` providers.
> - **OpenCode Zen / Go**: `providers.opencodeZen` and `providers.opencodeGo` use the same `OPENCODE_API_KEY`, but route to different OpenCode gateways. These providers use OpenCode's OpenAI-compatible `chat/completions` endpoints; choose model IDs from that endpoint family.
> - **Zhipu Coding Plan**: If you're on Zhipu's coding plan, set `"apiBase": "https://open.bigmodel.cn/api/coding/paas/v4"` in your zhipu provider config.
+3 -6
View File
@@ -18,7 +18,7 @@ Match the recipe to the credential or endpoint you already have:
| An OpenCode Zen or Go key | [OpenCode Zen or Go](#recipe-opencode-zen-or-go) | `OPENCODE_API_KEY`, the Zen/Go provider key, and a model ID from the matching OpenCode endpoint |
| An OpenAI platform API key and OpenAI model ID | [OpenAI Direct](#recipe-openai-direct) | `OPENAI_API_KEY`, `provider: "openai"`, and an OpenAI model available to that account |
| An Anthropic API key and Anthropic model ID | [Anthropic Direct](#recipe-anthropic-direct) | `ANTHROPIC_API_KEY`, `provider: "anthropic"`, and a non-gateway model ID |
| A Kimi Coding Plan key | [Kimi Coding Plan](#recipe-kimi-coding-plan) | `KIMI_CODING_API_KEY`, `provider: "kimi_coding"`, `model: "kimi-for-coding"`, and `extraHeaders.User-Agent` |
| A Kimi Coding Plan key | [Kimi Coding Plan](#recipe-kimi-coding-plan) | `KIMI_CODING_API_KEY`, `provider: "kimi_coding"`, and `model: "kimi-for-coding"` |
| An OpenAI-compatible `/v1` endpoint that is not a named nanobot provider | [Custom OpenAI-Compatible Provider](#recipe-custom-openai-compatible-provider) | `apiBase`, optional API key, and the model ID served by that endpoint |
| Ollama already running locally | [Ollama Local Model](#recipe-ollama-local-model) | Ollama `apiBase`, pulled model name, and local server availability |
| vLLM, LM Studio, or another local OpenAI-compatible server | [vLLM or LM Studio](#recipe-vllm-or-lm-studio) | Local `/v1` base URL, any required key, and served model name |
@@ -281,10 +281,7 @@ This recipe applies when your key comes from Kimi's Coding Plan endpoint. Nanobo
{
"providers": {
"kimiCoding": {
"apiKey": "${KIMI_CODING_API_KEY}",
"extraHeaders": {
"User-Agent": "claude-code/0.1.0"
}
"apiKey": "${KIMI_CODING_API_KEY}"
}
},
"modelPresets": {
@@ -311,7 +308,7 @@ nanobot status
nanobot agent -m "Hello!"
```
The default base URL is `https://api.kimi.com/coding/v1`. This endpoint **requires** the `User-Agent` header; omitting it returns `403`. Keep the value above or use another Claude-compatible value accepted by your Kimi account.
The default base URL is `https://api.kimi.com/coding/v1`. This endpoint requires a Claude-compatible `User-Agent`; nanobot sends `claude-code/0.1.0` by default. If your account requires a different value, override it with `providers.kimiCoding.extraHeaders.User-Agent`.
## Recipe: Custom OpenAI-Compatible Provider
+20 -8
View File
@@ -5,10 +5,10 @@ from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from nanobot.config.schema import Config, InlineFallbackConfig, ModelPresetConfig
from nanobot.config.schema import Config, InlineFallbackConfig, ModelPresetConfig, ProviderConfig
from nanobot.providers.base import LLMProvider
from nanobot.providers.fallback_provider import FallbackProvider
from nanobot.providers.registry import create_dynamic_spec, find_by_name
from nanobot.providers.registry import ProviderSpec, create_dynamic_spec, find_by_name
@dataclass(frozen=True)
@@ -28,6 +28,16 @@ def _resolve_model_preset(
return preset if preset is not None else config.resolve_preset(preset_name)
def _provider_extra_headers(
spec: ProviderSpec | None,
provider_config: ProviderConfig | None,
) -> dict[str, str] | None:
headers = dict(spec.default_extra_headers) if spec else {}
if provider_config and provider_config.extra_headers:
headers.update(provider_config.extra_headers)
return headers or None
def _make_provider_core(
config: Config,
*,
@@ -89,7 +99,7 @@ def _make_provider_core(
api_key=p.api_key if p else None,
api_base=config.get_api_base(model, preset=resolved),
default_model=model,
extra_headers=p.extra_headers if p else None,
extra_headers=_provider_extra_headers(spec, p),
)
elif backend == "bedrock":
from nanobot.providers.bedrock_provider import BedrockProvider
@@ -109,7 +119,7 @@ def _make_provider_core(
api_key=p.api_key if p else None,
api_base=config.get_api_base(model, preset=resolved),
default_model=model,
extra_headers=p.extra_headers if p else None,
extra_headers=_provider_extra_headers(spec, p),
spec=spec,
extra_body=p.extra_body if p else None,
api_type=p.api_type if p and provider_name == "openai" else "auto",
@@ -191,13 +201,14 @@ def provider_signature(
def _fallback_signature(fallback: ModelPresetConfig) -> tuple[object, ...]:
fp = config.get_provider(fallback.model, preset=fallback)
provider_name = config.get_provider_name(fallback.model, preset=fallback)
return (
fallback.model,
fallback.provider,
config.get_provider_name(fallback.model, preset=fallback),
provider_name,
config.get_api_key(fallback.model, preset=fallback),
config.get_api_base(fallback.model, preset=fallback),
fp.extra_headers if fp else None,
_provider_extra_headers(find_by_name(provider_name) if provider_name else None, fp),
fp.extra_body if fp else None,
fp.api_type if fp else "auto",
fp.extra_query if fp else None,
@@ -209,13 +220,14 @@ def provider_signature(
fallback.context_window_tokens,
)
provider_name = config.get_provider_name(resolved.model, preset=resolved)
return (
resolved.model,
resolved.provider,
config.get_provider_name(resolved.model, preset=resolved),
provider_name,
config.get_api_key(resolved.model, preset=resolved),
config.get_api_base(resolved.model, preset=resolved),
p.extra_headers if p else None,
_provider_extra_headers(find_by_name(provider_name) if provider_name else None, p),
p.extra_body if p else None,
p.api_type if p else "auto",
p.extra_query if p else None,
+3 -1
View File
@@ -37,8 +37,9 @@ class ProviderSpec:
# "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot" | "bedrock"
backend: str = "openai_compat"
# extra env vars, e.g. (("ZHIPUAI_API_KEY", "{api_key}"),)
# extra env vars / request headers supplied by the provider integration.
env_extras: tuple[tuple[str, str], ...] = ()
default_extra_headers: tuple[tuple[str, str], ...] = ()
# gateway / local detection
is_gateway: bool = False # routes any model (OpenRouter, AiHubMix)
@@ -426,6 +427,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
display_name="Kimi Coding",
backend="anthropic",
default_api_base="https://api.kimi.com/coding/v1",
default_extra_headers=(("User-Agent", "claude-code/0.1.0"),),
),
# MiniMax: OpenAI-compatible API
ProviderSpec(
@@ -0,0 +1,50 @@
from nanobot.config.schema import Config, ProviderConfig
from nanobot.providers.factory import _provider_extra_headers, provider_signature
from nanobot.providers.registry import find_by_name
def test_kimi_coding_uses_default_user_agent_header() -> None:
spec = find_by_name("kimi_coding")
assert spec is not None
assert _provider_extra_headers(spec, ProviderConfig()) == {
"User-Agent": "claude-code/0.1.0",
}
def test_provider_config_extra_headers_override_defaults() -> None:
spec = find_by_name("kimi_coding")
provider = ProviderConfig.model_validate({
"extraHeaders": {
"User-Agent": "custom-client/1.0",
"X-Test": "1",
},
})
assert _provider_extra_headers(spec, provider) == {
"User-Agent": "custom-client/1.0",
"X-Test": "1",
}
def test_provider_signature_tracks_default_extra_headers() -> None:
config = Config.model_validate({
"providers": {
"kimiCoding": {
"apiKey": "sk-kimi-test",
},
},
"modelPresets": {
"primary": {
"provider": "kimi_coding",
"model": "kimi-for-coding",
},
},
"agents": {
"defaults": {
"modelPreset": "primary",
},
},
})
assert {"User-Agent": "claude-code/0.1.0"} in provider_signature(config)