M1/F2+F4: platform registry, Moonshot API-key channel, dynamic model sync
F2 — fixed three-platform registry in kigi-models: kimi-code (subscription, OAuth bearer, base kigi_env::coding_api_base_url()), moonshot-cn (https://api.moonshot.cn/v1), moonshot-ai (https://api.moonshot.ai/v1) with kimi-k model-prefix filtering. Moonshot API keys via KIGI_MOONSHOT_CN_API_KEY / KIGI_MOONSHOT_AI_API_KEY (+ KIGI_MOONSHOT_API_KEY shared fallback) or ~/.kigi/config.toml; values redacted from logs/display. F4 — model catalog now syncs from GET {base}/models (Bearer auth, wire shape per official kimi-cli: id/context_length/supports_reasoning/ supports_image_in/supports_video_in/display_name) with the official capability-derivation rules (thinking / always_thinking-in-name / kimi-k2 implicit set). Managed keys {platform_id}/{model_id}; default model = first list entry; default thinking iff capabilities contain thinking/always_thinking. Sync failure → last cache; no cache → built-in fallback table seeded from ids sourced in official kimi-cli (kimi-for-coding, kimi-k2-turbo-preview, kimi-k2-thinking-turbo). 401 during sync forces one token refresh and retries. Model resolution priority preserved: CLI > env > config > server > fallback. Grok model artifacts (grok-4*/grok-build catalog, tier gating remnants) removed from non-test code. All first-party endpoints re-verified live: device_authorization mints real codes; /models on all three platforms answers with real API auth errors when unauthenticated. Gates: check/clippy --all-targets 0/0, fmt clean, deny ok, kigi-shell lib 5136 green, kigi-tui lib 6819 green, kigi-models 8.
This commit is contained in:
@@ -3,9 +3,10 @@ license = "Apache-2.0"
|
||||
name = "kigi-models"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Default model IDs for the grok CLI, loaded from the embedded default_models.json."
|
||||
description = "Kimi platform registry, /models wire contract, capability derivation, and the bundled offline fallback catalog (default_models.json)."
|
||||
|
||||
[dependencies]
|
||||
kigi-env = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
{
|
||||
"default": "grok-build",
|
||||
"web_search": "grok-4.20-multi-agent",
|
||||
"image_description": "grok-build",
|
||||
"session_summary": "grok-build",
|
||||
"default": "kimi-for-coding",
|
||||
"models": [
|
||||
{
|
||||
"model": "grok-build",
|
||||
"name": "Grok Build",
|
||||
"description": "Best for advanced coding tasks",
|
||||
"context_window": 500000,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.95,
|
||||
"api_backend": "responses",
|
||||
"id": "kimi-code/kimi-for-coding",
|
||||
"model": "kimi-for-coding",
|
||||
"name": "Kimi for Coding",
|
||||
"description": "Kimi Code subscription coding model (offline fallback entry)",
|
||||
"context_window": 262144,
|
||||
"capabilities": ["thinking", "image_in", "video_in"],
|
||||
"supported_in_api": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-cn/kimi-k2-turbo-preview",
|
||||
"model": "kimi-k2-turbo-preview",
|
||||
"name": "Kimi K2 Turbo (moonshot.cn)",
|
||||
"description": "Moonshot open platform model (offline fallback entry)",
|
||||
"context_window": 262144,
|
||||
"capabilities": ["thinking", "image_in", "video_in"]
|
||||
},
|
||||
{
|
||||
"id": "moonshot-cn/kimi-k2-thinking-turbo",
|
||||
"model": "kimi-k2-thinking-turbo",
|
||||
"name": "Kimi K2 Thinking Turbo (moonshot.cn)",
|
||||
"description": "Moonshot open platform model (offline fallback entry)",
|
||||
"context_window": 262144,
|
||||
"capabilities": ["thinking", "always_thinking", "image_in", "video_in"]
|
||||
},
|
||||
{
|
||||
"id": "moonshot-ai/kimi-k2-turbo-preview",
|
||||
"model": "kimi-k2-turbo-preview",
|
||||
"name": "Kimi K2 Turbo (moonshot.ai)",
|
||||
"description": "Moonshot open platform model (offline fallback entry)",
|
||||
"context_window": 262144,
|
||||
"capabilities": ["thinking", "image_in", "video_in"]
|
||||
},
|
||||
{
|
||||
"id": "moonshot-ai/kimi-k2-thinking-turbo",
|
||||
"model": "kimi-k2-thinking-turbo",
|
||||
"name": "Kimi K2 Thinking Turbo (moonshot.ai)",
|
||||
"description": "Moonshot open platform model (offline fallback entry)",
|
||||
"context_window": 262144,
|
||||
"capabilities": ["thinking", "always_thinking", "image_in", "video_in"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,14 +1,293 @@
|
||||
//! Default model IDs loaded from `default_models.json` at runtime.
|
||||
//! Edit that JSON file to change them.
|
||||
//! Kimi model catalog primitives (PRD F2/F4).
|
||||
//!
|
||||
//! This crate owns:
|
||||
//! - the fixed three-platform registry ([`PlatformId`]): the Kimi Code
|
||||
//! subscription channel plus the two Moonshot open platforms;
|
||||
//! - the `GET {base}/models` wire contract ([`WireModel`]) and the capability
|
||||
//! derivation ported from kimi-cli `auth/platforms.py`;
|
||||
//! - the managed catalog key format `{platform_id}/{model_id}`;
|
||||
//! - the bundled OFFLINE-LAST-RESORT fallback catalog
|
||||
//! (`default_models.json`), used only when the live `/models` sync fails
|
||||
//! AND no disk cache is usable. Every id in that file is sourced from
|
||||
//! kimi-cli 1.49.0 (see the module docs on [`DEFAULT_MODELS_JSON`]).
|
||||
//!
|
||||
//! At runtime each model is resolved via:
|
||||
//! CLI flag > ENV var > config.toml > remote settings > these defaults
|
||||
//! CLI flag > ENV var > config.toml > server-delivered > these defaults
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// The raw JSON, embedded at compile time. Re-exported through the
|
||||
/// `kigi_shell::models` facade and consumed by `agent::config`, so it must
|
||||
/// be `pub` (was `pub(crate)` when this lived inside the shell crate).
|
||||
// ── Platform registry (PRD F2) ──────────────────────────────────────────────
|
||||
|
||||
/// Env var holding the moonshot-cn API key (wins over the generic name).
|
||||
pub const MOONSHOT_CN_API_KEY_ENV: &str = "KIGI_MOONSHOT_CN_API_KEY";
|
||||
/// Env var holding the moonshot-ai API key (wins over the generic name).
|
||||
pub const MOONSHOT_AI_API_KEY_ENV: &str = "KIGI_MOONSHOT_AI_API_KEY";
|
||||
/// Generic moonshot API key env var, applied to BOTH open platforms when the
|
||||
/// platform-scoped name is unset.
|
||||
pub const MOONSHOT_API_KEY_ENV: &str = "KIGI_MOONSHOT_API_KEY";
|
||||
/// Base-URL override for moonshot-cn (dev/test escape hatch mirroring
|
||||
/// `KIGI_CODE_BASE_URL`; production uses the compiled default).
|
||||
pub const MOONSHOT_CN_BASE_URL_ENV: &str = "KIGI_MOONSHOT_CN_BASE_URL";
|
||||
/// Base-URL override for moonshot-ai (dev/test escape hatch mirroring
|
||||
/// `KIGI_CODE_BASE_URL`; production uses the compiled default).
|
||||
pub const MOONSHOT_AI_BASE_URL_ENV: &str = "KIGI_MOONSHOT_AI_BASE_URL";
|
||||
|
||||
/// Env override when set and non-blank, else the compiled default.
|
||||
fn env_or(var: &str, compiled: &str) -> String {
|
||||
match std::env::var(var) {
|
||||
Ok(v) if !v.trim().is_empty() => v,
|
||||
_ => compiled.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixed platform registry. Kigi talks to exactly these three model
|
||||
/// providers; there is no dynamic provider registration (PRD F2).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub enum PlatformId {
|
||||
/// Kimi Code subscription (OAuth bearer from the F1 device flow).
|
||||
KimiCode,
|
||||
/// Moonshot AI open platform, api.moonshot.cn (API key).
|
||||
MoonshotCn,
|
||||
/// Moonshot AI open platform, api.moonshot.ai (API key).
|
||||
MoonshotAi,
|
||||
}
|
||||
|
||||
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; 3] = [Self::KimiCode, Self::MoonshotCn, Self::MoonshotAi];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::KimiCode => "kimi-code",
|
||||
Self::MoonshotCn => "moonshot-cn",
|
||||
Self::MoonshotAi => "moonshot-ai",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"kimi-code" => Some(Self::KimiCode),
|
||||
"moonshot-cn" => Some(Self::MoonshotCn),
|
||||
"moonshot-ai" => Some(Self::MoonshotAi),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::KimiCode => "Kimi Code",
|
||||
Self::MoonshotCn => "Moonshot AI Open Platform (moonshot.cn)",
|
||||
Self::MoonshotAi => "Moonshot AI Open Platform (moonshot.ai)",
|
||||
}
|
||||
}
|
||||
|
||||
/// Inference/model-listing base URL. The subscription base honors the
|
||||
/// `KIGI_CODE_BASE_URL` override via [`kigi_env::coding_api_base_url`];
|
||||
/// the open-platform bases are fixed in production, with
|
||||
/// `KIGI_MOONSHOT_{CN,AI}_BASE_URL` as dev/test overrides.
|
||||
pub fn base_url(self) -> String {
|
||||
match self {
|
||||
Self::KimiCode => kigi_env::coding_api_base_url(),
|
||||
Self::MoonshotCn => env_or(MOONSHOT_CN_BASE_URL_ENV, "https://api.moonshot.cn/v1"),
|
||||
Self::MoonshotAi => env_or(MOONSHOT_AI_BASE_URL_ENV, "https://api.moonshot.ai/v1"),
|
||||
}
|
||||
}
|
||||
|
||||
/// True for the OAuth-bearer subscription channel.
|
||||
pub fn uses_oauth(self) -> bool {
|
||||
matches!(self, Self::KimiCode)
|
||||
}
|
||||
|
||||
/// Model-id prefixes admitted from this platform's `/models` listing.
|
||||
/// `None` = no filtering (subscription listing is served pre-filtered).
|
||||
pub fn allowed_model_prefixes(self) -> Option<&'static [&'static str]> {
|
||||
match self {
|
||||
Self::KimiCode => None,
|
||||
Self::MoonshotCn | Self::MoonshotAi => Some(&["kimi-k"]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Env var names holding this platform's API key, in precedence order
|
||||
/// (first set, non-blank value wins). Empty for the OAuth channel.
|
||||
///
|
||||
/// SECURITY: the *values* behind these names must never be logged.
|
||||
pub fn api_key_env_names(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::KimiCode => &[],
|
||||
Self::MoonshotCn => &[MOONSHOT_CN_API_KEY_ENV, MOONSHOT_API_KEY_ENV],
|
||||
Self::MoonshotAi => &[MOONSHOT_AI_API_KEY_ENV, MOONSHOT_API_KEY_ENV],
|
||||
}
|
||||
}
|
||||
|
||||
/// Managed catalog key for a model served by this platform:
|
||||
/// `{platform_id}/{model_id}` (kimi-cli `managed_model_key`).
|
||||
pub fn managed_model_key(self, model_id: &str) -> String {
|
||||
format!("{}/{model_id}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Split a managed catalog key `{platform_id}/{model_id}` back into its
|
||||
/// platform and bare model id. `None` when the key carries no known platform
|
||||
/// prefix (e.g. a user-defined `[model.*]` entry).
|
||||
pub fn parse_managed_model_key(key: &str) -> Option<(PlatformId, &str)> {
|
||||
let (platform, model_id) = key.split_once('/')?;
|
||||
let platform = PlatformId::parse(platform)?;
|
||||
if model_id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((platform, model_id))
|
||||
}
|
||||
|
||||
// ── Wire contract + capability derivation (PRD F4) ──────────────────────────
|
||||
|
||||
/// Model capabilities derived from the `/models` listing
|
||||
/// (port of kimi-cli `ModelCapability` + `ModelInfo.capabilities`).
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModelCapability {
|
||||
/// Supports reasoning ("thinking" mode toggleable on/off).
|
||||
Thinking,
|
||||
/// Thinking cannot be disabled (id contains "thinking").
|
||||
AlwaysThinking,
|
||||
ImageIn,
|
||||
VideoIn,
|
||||
}
|
||||
|
||||
impl ModelCapability {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Thinking => "thinking",
|
||||
Self::AlwaysThinking => "always_thinking",
|
||||
Self::ImageIn => "image_in",
|
||||
Self::VideoIn => "video_in",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ModelCapability {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry of the `GET {base}/models` response `data` array (PRD F4).
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct WireModel {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub context_length: u64,
|
||||
#[serde(default)]
|
||||
pub supports_reasoning: bool,
|
||||
#[serde(default)]
|
||||
pub supports_image_in: bool,
|
||||
#[serde(default)]
|
||||
pub supports_video_in: bool,
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
/// `GET {base}/models` response envelope.
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct WireModelsResponse {
|
||||
pub data: Vec<WireModel>,
|
||||
}
|
||||
|
||||
impl WireModel {
|
||||
/// Capability derivation ported verbatim from kimi-cli
|
||||
/// `auth/platforms.py::ModelInfo.capabilities`:
|
||||
/// - `supports_reasoning` → thinking
|
||||
/// - `"thinking"` in id → thinking + always_thinking
|
||||
/// - `supports_image_in` → image_in; `supports_video_in` → video_in
|
||||
/// - id starts with `kimi-k2` → thinking + image_in + video_in
|
||||
///
|
||||
/// Returned sorted + deduplicated ([`ModelCapability`]'s `Ord`).
|
||||
pub fn capabilities(&self) -> Vec<ModelCapability> {
|
||||
derive_capabilities(
|
||||
&self.id,
|
||||
self.supports_reasoning,
|
||||
self.supports_image_in,
|
||||
self.supports_video_in,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// See [`WireModel::capabilities`]; split out so fallback/bundled entries can
|
||||
/// run the same derivation from an id alone.
|
||||
pub fn derive_capabilities(
|
||||
id: &str,
|
||||
supports_reasoning: bool,
|
||||
supports_image_in: bool,
|
||||
supports_video_in: bool,
|
||||
) -> Vec<ModelCapability> {
|
||||
let id_lower = id.to_lowercase();
|
||||
let mut caps = std::collections::BTreeSet::new();
|
||||
if supports_reasoning {
|
||||
caps.insert(ModelCapability::Thinking);
|
||||
}
|
||||
if id_lower.contains("thinking") {
|
||||
caps.insert(ModelCapability::Thinking);
|
||||
caps.insert(ModelCapability::AlwaysThinking);
|
||||
}
|
||||
if supports_image_in {
|
||||
caps.insert(ModelCapability::ImageIn);
|
||||
}
|
||||
if supports_video_in {
|
||||
caps.insert(ModelCapability::VideoIn);
|
||||
}
|
||||
if id_lower.starts_with("kimi-k2") {
|
||||
caps.insert(ModelCapability::Thinking);
|
||||
caps.insert(ModelCapability::ImageIn);
|
||||
caps.insert(ModelCapability::VideoIn);
|
||||
}
|
||||
caps.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Whether thinking should default ON for a model with these capabilities
|
||||
/// (PRD F4: `thinking` or `always_thinking` present).
|
||||
pub fn default_thinking_enabled(capabilities: &[ModelCapability]) -> bool {
|
||||
capabilities.iter().any(|c| {
|
||||
matches!(
|
||||
c,
|
||||
ModelCapability::Thinking | ModelCapability::AlwaysThinking
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a platform's `allowed_model_prefixes` filter to a `/models` listing
|
||||
/// (kimi-cli `list_models`). No-op for platforms without a filter.
|
||||
pub fn filter_allowed_models(platform: PlatformId, models: Vec<WireModel>) -> Vec<WireModel> {
|
||||
let Some(prefixes) = platform.allowed_model_prefixes() else {
|
||||
return models;
|
||||
};
|
||||
models
|
||||
.into_iter()
|
||||
.filter(|m| prefixes.iter().any(|p| m.id.starts_with(p)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Bundled offline fallback catalog ────────────────────────────────────────
|
||||
|
||||
/// The raw JSON, embedded at compile time. OFFLINE LAST RESORT: consulted only
|
||||
/// when the live `/models` sync fails and no disk cache is usable.
|
||||
///
|
||||
/// Sources for every id (do not add ids that cannot be sourced):
|
||||
/// - `kimi-for-coding`: kimi-cli `src/kimi_cli/llm.py` (`model_display_name`,
|
||||
/// `derive_model_capabilities`) — the Kimi Code subscription coding model.
|
||||
/// Its capabilities {thinking, image_in, video_in} come from
|
||||
/// `derive_model_capabilities` in the same file.
|
||||
/// - `kimi-k2-turbo-preview` / `kimi-k2-thinking-turbo`: kimi-cli
|
||||
/// `tests/core/test_create_llm.py` (`_make_kimi_plain_model`,
|
||||
/// `_make_kimi_thinking_model`) — Moonshot open-platform models. Their
|
||||
/// capabilities follow the `auth/platforms.py` derivation rules
|
||||
/// ([`derive_capabilities`]).
|
||||
/// - context_window 262144: the canonical Kimi context size used by kimi-cli's
|
||||
/// own budget tests (`tests/core/test_create_llm.py`).
|
||||
///
|
||||
/// Re-exported through the `kigi_shell::models` facade and consumed by
|
||||
/// `agent::config`, so it must be `pub`.
|
||||
pub const DEFAULT_MODELS_JSON: &str = include_str!("../default_models.json");
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -68,3 +347,181 @@ pub fn default_session_summary_model() -> &'static str {
|
||||
.as_deref()
|
||||
.unwrap_or(&DEFAULTS.default)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn platform_ids_round_trip() {
|
||||
for p in PlatformId::ALL {
|
||||
assert_eq!(PlatformId::parse(p.as_str()), Some(p));
|
||||
}
|
||||
assert_eq!(PlatformId::parse("openai"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_base_urls() {
|
||||
assert_eq!(
|
||||
PlatformId::MoonshotCn.base_url(),
|
||||
"https://api.moonshot.cn/v1"
|
||||
);
|
||||
assert_eq!(
|
||||
PlatformId::MoonshotAi.base_url(),
|
||||
"https://api.moonshot.ai/v1"
|
||||
);
|
||||
// Subscription base honors the env override.
|
||||
let _g = kigi_env::EnvVarGuard::set(kigi_env::CODE_BASE_URL_ENV, "https://mock.test/v1");
|
||||
assert_eq!(PlatformId::KimiCode.base_url(), "https://mock.test/v1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_model_key_format_and_parse() {
|
||||
let key = PlatformId::MoonshotCn.managed_model_key("kimi-k2-turbo-preview");
|
||||
assert_eq!(key, "moonshot-cn/kimi-k2-turbo-preview");
|
||||
assert_eq!(
|
||||
parse_managed_model_key(&key),
|
||||
Some((PlatformId::MoonshotCn, "kimi-k2-turbo-preview"))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_managed_model_key("kimi-code/kimi-for-coding"),
|
||||
Some((PlatformId::KimiCode, "kimi-for-coding"))
|
||||
);
|
||||
// No prefix / unknown platform / empty model id → None.
|
||||
assert_eq!(parse_managed_model_key("kimi-for-coding"), None);
|
||||
assert_eq!(parse_managed_model_key("openai/gpt"), None);
|
||||
assert_eq!(parse_managed_model_key("moonshot-cn/"), None);
|
||||
}
|
||||
|
||||
/// Capability derivation table ported from kimi-cli platforms.py.
|
||||
#[test]
|
||||
fn capability_derivation_table() {
|
||||
use ModelCapability::*;
|
||||
let cases: &[(&str, bool, bool, bool, &[ModelCapability])] = &[
|
||||
// supports_reasoning only → thinking
|
||||
("some-model", true, false, false, &[Thinking]),
|
||||
// no flags, no name rules → empty
|
||||
("some-model", false, false, false, &[]),
|
||||
// "thinking" in id → thinking + always_thinking
|
||||
(
|
||||
"kimi-latest-thinking",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
&[Thinking, AlwaysThinking],
|
||||
),
|
||||
// image/video flags map directly
|
||||
("some-model", false, true, true, &[ImageIn, VideoIn]),
|
||||
// kimi-k2 prefix → thinking + image_in + video_in
|
||||
(
|
||||
"kimi-k2-turbo-preview",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
&[Thinking, ImageIn, VideoIn],
|
||||
),
|
||||
// kimi-k2 prefix + "thinking" in id → all four
|
||||
(
|
||||
"kimi-k2-thinking-turbo",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
&[Thinking, AlwaysThinking, ImageIn, VideoIn],
|
||||
),
|
||||
// Case-insensitive id rules (mirrors `.lower()` in platforms.py)
|
||||
(
|
||||
"Kimi-K2-Thinking",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
&[Thinking, AlwaysThinking, ImageIn, VideoIn],
|
||||
),
|
||||
];
|
||||
for (id, reasoning, image, video, want) in cases {
|
||||
let got = derive_capabilities(id, *reasoning, *image, *video);
|
||||
assert_eq!(&got, want, "capabilities for {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_thinking_from_capabilities() {
|
||||
use ModelCapability::*;
|
||||
assert!(default_thinking_enabled(&[Thinking]));
|
||||
assert!(default_thinking_enabled(&[AlwaysThinking]));
|
||||
assert!(default_thinking_enabled(&[Thinking, ImageIn]));
|
||||
assert!(!default_thinking_enabled(&[ImageIn, VideoIn]));
|
||||
assert!(!default_thinking_enabled(&[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moonshot_prefix_filter_applies_only_to_open_platforms() {
|
||||
let listing = vec![
|
||||
WireModel {
|
||||
id: "kimi-k2-turbo-preview".into(),
|
||||
context_length: 262_144,
|
||||
supports_reasoning: false,
|
||||
supports_image_in: false,
|
||||
supports_video_in: false,
|
||||
display_name: None,
|
||||
},
|
||||
WireModel {
|
||||
id: "moonshot-v1-8k".into(),
|
||||
context_length: 8_192,
|
||||
supports_reasoning: false,
|
||||
supports_image_in: false,
|
||||
supports_video_in: false,
|
||||
display_name: None,
|
||||
},
|
||||
];
|
||||
let filtered = filter_allowed_models(PlatformId::MoonshotCn, listing.clone());
|
||||
assert_eq!(
|
||||
filtered.iter().map(|m| m.id.as_str()).collect::<Vec<_>>(),
|
||||
vec!["kimi-k2-turbo-preview"],
|
||||
"moonshot listing must be filtered to the kimi-k prefix"
|
||||
);
|
||||
let unfiltered = filter_allowed_models(PlatformId::KimiCode, listing);
|
||||
assert_eq!(unfiltered.len(), 2, "subscription listing is not filtered");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_response_parses_f4_shape() {
|
||||
let raw = r#"{
|
||||
"data": [
|
||||
{
|
||||
"id": "kimi-for-coding",
|
||||
"context_length": 262144,
|
||||
"supports_reasoning": true,
|
||||
"supports_image_in": true,
|
||||
"supports_video_in": false,
|
||||
"display_name": "k2.6-code-preview"
|
||||
},
|
||||
{ "id": "kimi-k2-turbo-preview" }
|
||||
]
|
||||
}"#;
|
||||
let resp: WireModelsResponse = serde_json::from_str(raw).expect("F4 shape must parse");
|
||||
assert_eq!(resp.data.len(), 2);
|
||||
let first = &resp.data[0];
|
||||
assert_eq!(first.id, "kimi-for-coding");
|
||||
assert_eq!(first.context_length, 262_144);
|
||||
assert_eq!(first.display_name.as_deref(), Some("k2.6-code-preview"));
|
||||
assert_eq!(
|
||||
first.capabilities(),
|
||||
vec![ModelCapability::Thinking, ModelCapability::ImageIn]
|
||||
);
|
||||
// Missing optional fields default off/0.
|
||||
let second = &resp.data[1];
|
||||
assert_eq!(second.context_length, 0);
|
||||
assert!(!second.supports_reasoning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_fallback_is_kimi_catalog() {
|
||||
assert_eq!(default_model(), "kimi-for-coding");
|
||||
// Aux models fall back to the default (no dedicated entries).
|
||||
assert_eq!(default_web_search_model(), "kimi-for-coding");
|
||||
assert_eq!(default_image_description_model(), "kimi-for-coding");
|
||||
assert_eq!(default_session_summary_model(), "kimi-for-coding");
|
||||
// No grok remnants in the embedded fallback.
|
||||
assert!(!DEFAULT_MODELS_JSON.contains("grok"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user