Add a first-class `kimi` provider and `kimi://` DSN scheme for Moonshot AI's OpenAI-compatible Chat Completions API. Both reuse provider/openai (no new client, mirroring llama-swap's chat path). Default endpoint is the international host; the China endpoint is reachable via a kimi:// LLM_* DSN. - Credential is KIMI_API_KEY, read through the registry's injected envLookup so it stays hermetically testable. WithAPIKey is passed unconditionally so an unset KIMI_API_KEY can never fall through to the openai client's OPENAI_API_KEY default. - New openai.WithAPIKeyName option customizes the missing-key error hint (default OPENAI_API_KEY); kimi names KIMI_API_KEY. - Hermetic tests: built-in base URL + bearer, missing-key hint names KIMI_API_KEY with no OPENAI fallthrough and no network hit, kimi:// scheme round-trips against the China host. - Docs in sync: README built-in table + DSN scheme list + support matrix, .env.example, env.go DSN doc, ADR-0026 (+ index, backfilling 0024/0025), progress.md. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
3.2 KiB
3.2 KiB
ADR-0026: Kimi (Moonshot AI) built-in provider
Status: Accepted — 2026-07-18
Context
Moonshot AI's Kimi models (Kimi K2, moonshot-v1-*, and the vision variants)
are served over an OpenAI-compatible Chat Completions API at
https://api.moonshot.ai/v1 (https://api.moonshot.cn/v1 for China),
authenticated with a bearer key. mort wants Kimi as a first-class failover
tier, so kimi/kimi-k2-... should parse, chain, and alias out of the box with
a dedicated KIMI_API_KEY env var — the same ergonomics as openai,
anthropic, and google.
Two tensions:
- The wire protocol is byte-for-byte OpenAI Chat Completions, so a hand-rolled
client would duplicate
provider/openaifor zero gain (ADR-0007 forbids it), exactly as ADR-0015 found for llama-swap. - The README's current stance is that arbitrary OpenAI-compatible endpoints
(Groq, Together, …) are consumer-registered, not baked in. Blessing Kimi as
a built-in is a deliberate, narrow exception justified by the north star:
mort names Kimi directly in its tiers, and a built-in with
KIMI_API_KEYkeeps mort's config free of boilerplateopenai.New(WithName/WithBaseURL)wiring.
Decision
- No new package. The
kimibuilt-in andkimi://DSN scheme both constructprovider/openaipointed at the Moonshot base URL — the chat path inherits every openai feature/fix automatically (like llama-swap's chat). - The built-in reads its key through the registry's injected
envLookup(KIMI_API_KEYonly — noMOONSHOT_API_KEYalias, per the project owner) so it stays hermetically testable viaWithEnvLookup. WithAPIKeyis passed unconditionally, even when empty.openai.Newdefaults its key toOPENAI_API_KEY; without an explicit override an unsetKIMI_API_KEYwould silently authenticate Kimi with the OpenAI key. Passing the (possibly empty) lookup result severs that fallthrough.- New
openai.WithAPIKeyName("KIMI_API_KEY")option customizes only the synthetic-401 missing-key hint (defaultOPENAI_API_KEY), so a keyless kimi call tells the operator the right variable to set. - The default endpoint is the international host (
kimiBaseURL). The China endpoint (or any other host) is reachable with akimi://DSN, e.g.LLM_KCN=kimi://[email protected]/v1. Thekimi://scheme is an OpenAI-compatible target labeledkimiwith the same key-name hint; it is intentionally near-identical toopenai://— its value is a clear name in specs and error reporting.
Consequences
kimi/<model>is first-class in Parse, chains, aliases, and health/failover with no consumer wiring; model ids pass through verbatim (no catalog).- Chat, streaming, tools, and structured output ride the openai client. Image inputs work at the client level but only the Moonshot vision models accept them (matrix footnote ³).
WithAPIKeyNameis a small, generally useful addition toprovider/openai; the default preserves existing behavior for every other openai-compat target.- Blessing one third-party endpoint as a built-in sets a precedent; future ones
should clear the same bar (a named consumer needs it in-config), not be added
reflexively —
RegisterProvider/LLM_*remain the path for the rest.