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:
2026-07-17 09:23:44 -04:00
parent 021b82443d
commit fe1f885bb3
25 changed files with 2148 additions and 430 deletions
@@ -655,7 +655,7 @@ impl SessionActor {
let request = ConversationRequest {
items,
tools: vec![],
model: Some("grok-build".to_owned()),
model: Some(crate::models::default_model().to_owned()),
temperature: Some(0.3),
max_output_tokens: Some(1024),
..Default::default()
@@ -507,7 +507,7 @@ impl SessionActor {
let model = match model_override {
Some(m) => m.to_owned(),
None => "grok-build".to_owned(),
None => crate::models::default_model().to_owned(),
};
let request = ConversationRequest {
@@ -569,10 +569,10 @@ impl SessionActor {
/// (`KIGI_PROMPT_SUGGESTIONS_MODEL`) > `[models] prompt_suggestion`
/// (config.toml) > remote `prompt_suggestion_model` (remote settings) >
/// (config.toml) > remote `prompt_suggestion_model` (remote settings) >
/// [`prompt_suggest::DEFAULT_SUGGEST_MODEL`] (`grok-build-0.1`). Every
/// [`prompt_suggest::default_suggest_model`]. Every
/// tier except env is catalog-guarded against this shell's own model
/// catalog — when the effective model is not sampleable here (e.g.
/// `grok-build-0.1` for OAuth users) the request is **skipped
/// a model the catalog does not offer) the request is **skipped
/// entirely** instead of fired doomed. The session model is never used:
/// a per-turn background call must stay on the small model.
/// Temperature, max_output_tokens, and
@@ -521,7 +521,8 @@ pub struct SessionInfoData {
/// Whether this model slug supports showing checkpoint identity (resolved model ID, fingerprint).
pub fn is_coding_model_slug(model: &str) -> bool {
matches!(model, "grok-build" | "grok-4.5")
model == kigi_models::PlatformId::KimiCode.managed_model_key(crate::models::default_model())
|| model == crate::models::default_model()
}
/// Display gate for the model fingerprint: server/catalog opt-in OR the built-in coding-slug default.
@@ -626,9 +627,13 @@ mod tests {
fn should_show_model_fingerprint_truth_table() {
// Catalog opt-in shows the fingerprint even for a non-coding slug.
assert!(should_show_model_fingerprint(true, "non-coding"));
// Coding slugs always show, even without the catalog flag.
assert!(should_show_model_fingerprint(false, "grok-build"));
assert!(should_show_model_fingerprint(false, "grok-4.5"));
// The default coding model always shows, by slug or managed key,
// even without the catalog flag.
assert!(should_show_model_fingerprint(false, "kimi-for-coding"));
assert!(should_show_model_fingerprint(
false,
"kimi-code/kimi-for-coding"
));
// Non-coding slug without the flag stays hidden.
assert!(!should_show_model_fingerprint(false, "some-other"));
}
@@ -5,7 +5,7 @@
//! the empty prompt input; Tab accepts it. Modelled on common coding-agent
//! prompt suggestion features, but instead of replaying the full conversation prefix
//! it sends a *compact text-only transcript* — the call always routes to a
//! small dedicated model (configurable, [`DEFAULT_SUGGEST_MODEL`] by
//! dedicated model (configurable, [`default_suggest_model`] by
//! default, never the session model — see [`effective_suggest_model`]),
//! where the parent session's prompt cache would not apply anyway, so a
//! small request wins on both cost and latency.
@@ -20,20 +20,20 @@ use crate::session::helpers::chat::floor_char_boundary;
/// Model used for suggestion calls when nothing pins one (no env /
/// `[models] prompt_suggestion` / remote setting / client hint — see
/// [`effective_suggest_model`]). Suggestion requests must stay on a small,
/// fast model: falling back to the session model would multiply the per-turn
/// cost of the feature and add reasoning-model latency for a throwaway
/// prediction.
pub(crate) const DEFAULT_SUGGEST_MODEL: &str = "grok-build-0.1";
/// [`effective_suggest_model`]). The Kimi catalog has no dedicated small
/// suggestion model, so this is the bundled default coding model; the
/// catalog guard still controls whether the request fires at all.
pub(crate) fn default_suggest_model() -> &'static str {
crate::models::default_model()
}
/// Resolve the model for one suggestion request, or `None` to skip the
/// request entirely (controlled disable).
///
/// Precedence: env pin > config.toml/remote pin > client hint (the request's
/// `model` param) > [`DEFAULT_SUGGEST_MODEL`]. Every tier except the env pin
/// is catalog-guarded via `in_catalog`: [`DEFAULT_SUGGEST_MODEL`]
/// (`grok-build-0.1`) is API-key-only and excluded from OAuth catalogs, so
/// firing it (or any unavailable pin) would send a doomed per-turn request
/// `model` param) > [`default_suggest_model`]. Every tier except the env pin
/// is catalog-guarded via `in_catalog`: firing an unavailable pin or default
/// would send a doomed per-turn request
/// that can never render ghost text. Skipping keeps the per-turn cost at
/// zero; deliberately NOT a session-model fallback — a per-turn background
/// call must stay on a small cheap model. The env pin bypasses the guard so
@@ -48,7 +48,7 @@ pub(crate) fn effective_suggest_model(
let (model, catalog_guarded) = match pin {
PromptSuggestModelPin::Env(m) => (m.as_str(), false),
PromptSuggestModelPin::Pinned(m) => (m.as_str(), true),
PromptSuggestModelPin::Unpinned => (client_hint.unwrap_or(DEFAULT_SUGGEST_MODEL), true),
PromptSuggestModelPin::Unpinned => (client_hint.unwrap_or(default_suggest_model()), true),
};
if catalog_guarded && !in_catalog(model) {
return None;
@@ -324,9 +324,9 @@ mod tests {
// No pin, no hint: the built-in default fires only when this shell's
// catalog can sample it.
assert_eq!(
effective_suggest_model(&Pin::Unpinned, None, |m| m == DEFAULT_SUGGEST_MODEL)
effective_suggest_model(&Pin::Unpinned, None, |m| m == default_suggest_model())
.as_deref(),
Some(DEFAULT_SUGGEST_MODEL)
Some(default_suggest_model())
);
// OAuth catalogs exclude grok-build-0.1 → skip the request entirely,
// never a doomed call (and never the session model).
@@ -349,9 +349,9 @@ mod tests {
);
// Blank hints are ignored: the default tier applies.
assert_eq!(
effective_suggest_model(&Pin::Unpinned, Some(" "), |m| m == DEFAULT_SUGGEST_MODEL)
effective_suggest_model(&Pin::Unpinned, Some(" "), |m| m == default_suggest_model())
.as_deref(),
Some(DEFAULT_SUGGEST_MODEL)
Some(default_suggest_model())
);
}