feat(providers): add Kimi For Coding via static KIMI_API_KEY

Provider 16 (19th registry variant). Same endpoint + models + Kimi dialect
as the existing OAuth kimi-code platform (api.kimi.com/coding/v1 via the
KIGI_CODE_BASE_URL override), but authenticated with a static KIMI_API_KEY
instead of the device flow — for users who have a Kimi For Coding key rather
than an OAuth subscription. Bearer, OpenAI listing, ChatCompletions,
ChatCompat::Kimi, wire_serves_metadata=true (Kimi /models self-serves
context/thinking), restrict_to_enriched=false (clean 3-model catalog).
/coding/v1/models is auth-gated (401) so it doubles as the validator.

No collision: KIMI_API_KEY was previously unused (grep-verified), and the
house BYOK reads only KIGI_API_KEY/XAI_API_KEY/legacy. kimi-code (OAuth) and
kimi-coding (static key) are independently gated (OAuth-token vs key) and
their models get distinct managed keys (kimi-code/k3 vs kimi-coding/k3) — a
user with both simply sees each Kimi model twice; no dedup collision, no crash.

Review (6 areas): no blocking defects; confirmed the spec correctly mirrors
KIMI_CODE_SPEC (differing only in uses_oauth/api_key_envs/console_host/labels)
and the coexistence is benign. Strengthened the e2e's dialect assertion (Kimi
is the default ChatCompat, so it did not discriminate a parse failure) by
also asserting parse_managed_model_key attributes the key to KimiCoding.

Tests: e2e proves wire-served context (1_048_576 from the wire) with the
models.dev fetch SKIPPED (all-wire-metadata provider, .expect(0)), bare-id
round-trip under kimi-coding/, Kimi dialect; validation test rejects a 401
from /models. Registry at 19; picker 20 rows; snapshot already bundles
kimi-for-coding.
This commit is contained in:
2026-07-21 18:10:23 -04:00
parent 8245deb373
commit c02b4b1ed7
4 changed files with 161 additions and 9 deletions
+34 -2
View File
@@ -689,6 +689,33 @@ const QWEN_TOKEN_PLAN_CN_SPEC: PlatformSpec = PlatformSpec {
restrict_to_enriched: true,
};
const KIMI_CODING_SPEC: PlatformSpec = PlatformSpec {
id: "kimi-coding",
display_name: "Kimi For Coding",
// Same endpoint as the OAuth `kimi-code` platform (KIGI_CODE_BASE_URL
// override), but authenticated with a static KIMI_API_KEY instead of the
// device flow. Kimi's /coding/v1/models serves its own metadata and the
// Kimi thinking dialect applies verbatim.
base_url: BaseUrlSource::KigiEnvCoding,
uses_oauth: false,
allowed_model_prefixes: None,
api_key_envs: &["KIMI_API_KEY"],
vendor: "Kimi",
console_host: Some("www.kimi.com"),
login_label: Some("Kimi For Coding (API key)"),
models_dev_id: Some("kimi-for-coding"),
wire_serves_metadata: true,
wire_api: PlatformWireApi::ChatCompletions,
listing: ListingDialect::OpenAi,
chat_compat: PlatformChatCompat::Kimi,
key_header: PlatformKeyHeader::Bearer,
// /coding/v1/models requires auth (401 without a key), so it doubles as
// the key validator.
key_validation_path: None,
restrict_to_enriched: false,
strip_listing_id_prefix: None,
};
/// The platform registry. Platforms are compiled-in spec rows; there is no
/// dynamic provider registration (PRD F2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -729,12 +756,14 @@ pub enum PlatformId {
QwenTokenPlan,
/// Alibaba Qwen Token Plan, China (API key, DashScope compatible-mode).
QwenTokenPlanCn,
/// Kimi For Coding via a static KIMI_API_KEY (same endpoint as `KimiCode`).
KimiCoding,
}
impl PlatformId {
/// All platforms, in catalog precedence order: the subscription channel
/// first so "default model = first list item" favors it when present.
pub const ALL: [PlatformId; 18] = [
pub const ALL: [PlatformId; 19] = [
Self::KimiCode,
Self::MoonshotCn,
Self::MoonshotAi,
@@ -753,6 +782,7 @@ impl PlatformId {
Self::Xai,
Self::QwenTokenPlan,
Self::QwenTokenPlanCn,
Self::KimiCoding,
];
/// The registry row backing this platform (single source of per-platform
@@ -777,6 +807,7 @@ impl PlatformId {
Self::Xai => &XAI_SPEC,
Self::QwenTokenPlan => &QWEN_TOKEN_PLAN_SPEC,
Self::QwenTokenPlanCn => &QWEN_TOKEN_PLAN_CN_SPEC,
Self::KimiCoding => &KIMI_CODING_SPEC,
}
}
@@ -1535,9 +1566,10 @@ mod tests {
PlatformId::Xai => 15,
PlatformId::QwenTokenPlan => 16,
PlatformId::QwenTokenPlanCn => 17,
PlatformId::KimiCoding => 18,
}
}
const VARIANT_COUNT: usize = 18; // update together with `ordinal`
const VARIANT_COUNT: usize = 19; // update together with `ordinal`
let mut seen: Vec<usize> = PlatformId::ALL.iter().map(|&p| ordinal(p)).collect();
seen.sort_unstable();
seen.dedup();