feat(providers): add Z.AI coding plan (global + China)

Providers 17-18 (20th & 21st registry variants), sourced authoritatively
from Pi (earendil-works/pi), the open-source agent whose provider list is
being mirrored. Pi's zai.ts / zai-coding-cn.ts use plain openAICompletionsApi
(NO special thinking dialect — overturns the matrix's 'thinking:{type} → new
dialect' concern), so Kigi maps them to Passthrough. Global: api.z.ai/api/
coding/paas/v4, ZAI_API_KEY, models.dev zai-coding-plan. China (Zhipu
BigModel): open.bigmodel.cn/api/coding/paas/v4, ZAI_CODING_CN_API_KEY,
models.dev zhipuai-coding-plan. Both Bearer + OpenAI listing + ChatCompletions
+ restrict_to_enriched; /models is auth-gated → validator.

id-match is PROVEN (not just assumed like Qwen): Pi's static model ids
[glm-4.5-air, glm-4.7, glm-5-turbo, glm-5.1, glm-5.2, glm-5v-turbo] are
byte-identical to the models.dev zai-coding-plan keys, all tool_call=true, so
restrict keeps every model with no silent-empty risk. Review found no defects.
GLM thinking is not lost (reasoning_content is parsed regardless of dialect).

Tests: e2e proves enrichment-supplied context + non-vacuous restrict (a
non-enriched wire model is dropped) + Passthrough; both variants' validation
tests hit /models and assert the per-variant console host (z.ai vs
open.bigmodel.cn). Registry at 21; picker 22 rows.

Also fixes the welcome login-picker test for the now-taller menu (renders at
a taller viewport to verify content coverage) and logs the real menu-overflow
UX debt: the picker clips rows past the fold with no scroll (q/l shortcuts
still work; only shown when unauthenticated) — deferred to its own cycle.
This commit is contained in:
2026-07-21 19:32:58 -04:00
parent c02b4b1ed7
commit 010f6c3be3
5 changed files with 243 additions and 11 deletions
+17 -3
View File
@@ -6894,7 +6894,7 @@ pub(crate) mod tests {
#[test]
fn pending_menu_items_lists_interactive_methods_plus_quit() {
let items = pending_menu_items(&fresh_user_auth_methods(), None);
assert_eq!(items.len(), 20, "19 login rows + Quit, got {items:?}");
assert_eq!(items.len(), 22, "21 login rows + Quit, got {items:?}");
assert!(
matches!(&items[0], PendingMenuItem::Login { label } if label == "Kimi Code (OAuth)"),
"row 0 must be the OAuth login, got {:?}",
@@ -7027,7 +7027,21 @@ pub(crate) mod tests {
label: "Kimi For Coding (API key)".into(),
}
);
assert_eq!(items[19], PendingMenuItem::Quit);
assert_eq!(
items[19],
PendingMenuItem::ApiKey {
target: PlatformLogin(kigi_shell::models::PlatformId::Zai),
label: "Z.AI (API key)".into(),
}
);
assert_eq!(
items[20],
PendingMenuItem::ApiKey {
target: PlatformLogin(kigi_shell::models::PlatformId::ZaiCodingCn),
label: "Z.AI Coding China (API key)".into(),
}
);
assert_eq!(items[21], PendingMenuItem::Quit);
// The non-interactive methods must never appear as rows.
let byok = kigi_shell::agent::auth_method::build_auth_methods(
kigi_shell::agent::auth_method::AuthMethodsBuildInputs {
@@ -7038,7 +7052,7 @@ pub(crate) mod tests {
);
assert_eq!(
pending_menu_items(&byok.methods, None).len(),
20,
22,
"xai.api_key / cached_token must not add rows"
);
}
@@ -2070,7 +2070,16 @@ mod tests {
}
fn render_done_text(params: &WelcomeRenderParams<'_>) -> String {
let area = Rect::new(0, 0, 100, 40);
render_done_text_h(params, 40)
}
/// Render at an explicit height. The login picker lists one row per
/// advertised platform; with ~20 API-key providers the menu no longer
/// fits a 40-row viewport, so the full-menu content test needs more rows
/// to verify every label renders. (On-terminal, rows past the fold are
/// clipped — the `q`/`l` shortcuts still work; see the menu-scroll debt.)
fn render_done_text_h(params: &WelcomeRenderParams<'_>, height: u16) -> String {
let area = Rect::new(0, 0, 100, height);
let mut buf = Buffer::empty(area);
let mut prompt = PromptWidget::new();
let mut picker = PickerState::default();
@@ -2093,7 +2102,9 @@ mod tests {
let trust = TrustState::Done;
let mut params = render_params(&auth, &trust, None);
params.auth_methods = &built.methods;
let text = render_done_text(&params);
// Tall viewport so every advertised login row renders (the picker now
// lists ~20 platforms); this asserts content coverage, not fit.
let text = render_done_text_h(&params, 72);
assert!(text.contains("Kimi Code (OAuth)"), "{text}");
assert!(
text.contains("Moonshot Open Platform (API key \u{b7} moonshot.cn)"),