fix(providers): apply Kimi Coding default headers
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user