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
@@ -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())
);
}