fix(providers): apply Kimi Coding default headers

This commit is contained in:
Xubin Ren 2026-06-24 10:34:42 +08:00
parent 9d6c606cc9
commit f9afc9389b
5 changed files with 77 additions and 16 deletions

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 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 (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`. > - **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. > - **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. > - **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. > - **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.

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 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 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 | | 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 | | 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 | | 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 | | 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": { "providers": {
"kimiCoding": { "kimiCoding": {
"apiKey": "${KIMI_CODING_API_KEY}", "apiKey": "${KIMI_CODING_API_KEY}"
"extraHeaders": {
"User-Agent": "claude-code/0.1.0"
}
} }
}, },
"modelPresets": { "modelPresets": {
@ -311,7 +308,7 @@ nanobot status
nanobot agent -m "Hello!" 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 ## Recipe: Custom OpenAI-Compatible Provider

View File

@ -5,10 +5,10 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path 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.base import LLMProvider
from nanobot.providers.fallback_provider import FallbackProvider 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) @dataclass(frozen=True)
@ -28,6 +28,16 @@ def _resolve_model_preset(
return preset if preset is not None else config.resolve_preset(preset_name) 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( def _make_provider_core(
config: Config, config: Config,
*, *,
@ -89,7 +99,7 @@ def _make_provider_core(
api_key=p.api_key if p else None, api_key=p.api_key if p else None,
api_base=config.get_api_base(model, preset=resolved), api_base=config.get_api_base(model, preset=resolved),
default_model=model, default_model=model,
extra_headers=p.extra_headers if p else None, extra_headers=_provider_extra_headers(spec, p),
) )
elif backend == "bedrock": elif backend == "bedrock":
from nanobot.providers.bedrock_provider import BedrockProvider 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_key=p.api_key if p else None,
api_base=config.get_api_base(model, preset=resolved), api_base=config.get_api_base(model, preset=resolved),
default_model=model, default_model=model,
extra_headers=p.extra_headers if p else None, extra_headers=_provider_extra_headers(spec, p),
spec=spec, spec=spec,
extra_body=p.extra_body if p else None, extra_body=p.extra_body if p else None,
api_type=p.api_type if p and provider_name == "openai" else "auto", 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, ...]: def _fallback_signature(fallback: ModelPresetConfig) -> tuple[object, ...]:
fp = config.get_provider(fallback.model, preset=fallback) fp = config.get_provider(fallback.model, preset=fallback)
provider_name = config.get_provider_name(fallback.model, preset=fallback)
return ( return (
fallback.model, fallback.model,
fallback.provider, fallback.provider,
config.get_provider_name(fallback.model, preset=fallback), provider_name,
config.get_api_key(fallback.model, preset=fallback), config.get_api_key(fallback.model, preset=fallback),
config.get_api_base(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.extra_body if fp else None,
fp.api_type if fp else "auto", fp.api_type if fp else "auto",
fp.extra_query if fp else None, fp.extra_query if fp else None,
@ -209,13 +220,14 @@ def provider_signature(
fallback.context_window_tokens, fallback.context_window_tokens,
) )
provider_name = config.get_provider_name(resolved.model, preset=resolved)
return ( return (
resolved.model, resolved.model,
resolved.provider, resolved.provider,
config.get_provider_name(resolved.model, preset=resolved), provider_name,
config.get_api_key(resolved.model, preset=resolved), config.get_api_key(resolved.model, preset=resolved),
config.get_api_base(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.extra_body if p else None,
p.api_type if p else "auto", p.api_type if p else "auto",
p.extra_query if p else None, p.extra_query if p else None,

View File

@ -37,8 +37,9 @@ class ProviderSpec:
# "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot" | "bedrock" # "openai_compat" | "anthropic" | "azure_openai" | "openai_codex" | "github_copilot" | "bedrock"
backend: str = "openai_compat" 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], ...] = () env_extras: tuple[tuple[str, str], ...] = ()
default_extra_headers: tuple[tuple[str, str], ...] = ()
# gateway / local detection # gateway / local detection
is_gateway: bool = False # routes any model (OpenRouter, AiHubMix) is_gateway: bool = False # routes any model (OpenRouter, AiHubMix)
@ -426,6 +427,7 @@ PROVIDERS: tuple[ProviderSpec, ...] = (
display_name="Kimi Coding", display_name="Kimi Coding",
backend="anthropic", backend="anthropic",
default_api_base="https://api.kimi.com/coding/v1", default_api_base="https://api.kimi.com/coding/v1",
default_extra_headers=(("User-Agent", "claude-code/0.1.0"),),
), ),
# MiniMax: OpenAI-compatible API # MiniMax: OpenAI-compatible API
ProviderSpec( ProviderSpec(

View File

@ -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)