From 5b27f1b418470bd5fa7eb68a2a71af5b26e6cf36 Mon Sep 17 00:00:00 2001 From: guanbear <123guan@gmail.com> Date: Fri, 14 Aug 2026 14:53:32 +0800 Subject: [PATCH] fix: explain unsupported custom TUI providers --- docs/CONFIGURATION.md | 2 ++ src/launcher.ts | 32 +++++++++++++++++++++++++++----- src/model-access.ts | 14 +++++++++++++- test/launcher.test.ts | 17 +++++++++++++++++ test/model-access.test.ts | 26 +++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 14a9df9..3d0df4f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -90,6 +90,8 @@ considers a direct API key configured only when it is stored under provider ID `zai` or `bigmodel`. An arbitrary provider ID is valid model configuration, but as the only provider it still triggers the upstream login gate. The display name, API format, endpoint, headers and models remain fully custom. +The launcher detects this mismatch before opening the TUI and prints the +required provider IDs instead of reporting the configuration as fully ready. For an Anthropic-compatible endpoint: diff --git a/src/launcher.ts b/src/launcher.ts index 2d9d35d..8b0ff27 100644 --- a/src/launcher.ts +++ b/src/launcher.ts @@ -13,7 +13,11 @@ import { constants as osConstants, homedir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { ensureUserConfig, readConfiguredModelAccess } from "./model-access.ts"; +import { + ensureUserConfig, + readConfiguredModelAccess, + type ConfiguredModelAccess +} from "./model-access.ts"; import { classifyZaiOAuthInvocation, runZaiOAuthLogin, @@ -108,6 +112,15 @@ export function normalizeLoginArgs(args: string[]): { args: string[]; checkConfi return { args, checkConfiguredAccess: false }; } +export function formatUnsupportedTuiProviderMessage(access: ConfiguredModelAccess): string { + return [ + `Model access is configured for headless use as ${access.model}, but the upstream TUI`, + 'requires a direct API key under provider ID "zai" or "bigmodel".', + `Rename the provider key and update model.main/model.lite in ${access.configPath}.`, + "See docs/CONFIGURATION.md for a compatible custom-provider example." + ].join("\n"); +} + function longOptionName(argument: string): string { const separator = argument.indexOf("="); return separator < 0 ? argument : argument.slice(0, separator); @@ -433,16 +446,25 @@ export async function main(args: string[]): Promise { const login = normalizeLoginArgs(args); const zaiOAuth = classifyZaiOAuthInvocation(args); + const configuredAccess = await readConfiguredModelAccess(); if (login.checkConfiguredAccess) { - const access = await readConfiguredModelAccess(); - if (access) { + if (configuredAccess?.tuiCompatible) { console.log( - `Model access is already configured for ${access.model}; OAuth login is not required.\n` - + `Config: ${access.configPath}\n` + `Model access is already configured for ${configuredAccess.model}; OAuth login is not required.\n` + + `Config: ${configuredAccess.configPath}\n` + "Run `zcode login --oauth` to force Z.AI OAuth." ); return 0; } + if (configuredAccess) { + console.error(formatUnsupportedTuiProviderMessage(configuredAccess)); + return 1; + } + } + + if (isTuiRuntimeInvocation(login.args) && configuredAccess && !configuredAccess.tuiCompatible) { + console.error(formatUnsupportedTuiProviderMessage(configuredAccess)); + return 1; } if (zaiOAuth) { diff --git a/src/model-access.ts b/src/model-access.ts index 3b43108..751c8be 100644 --- a/src/model-access.ts +++ b/src/model-access.ts @@ -23,6 +23,7 @@ export interface ConfiguredModelAccess { configPath: string; model: string; providerId: string; + tuiCompatible: boolean; } export interface UserConfigBootstrapResult { @@ -32,6 +33,12 @@ export interface UserConfigBootstrapResult { export type UserConfigRecord = Record; +const tuiDirectApiKeyProviderIds = new Set(["zai", "bigmodel"]); + +export function isTuiCompatibleProviderId(providerId: string): boolean { + return tuiDirectApiKeyProviderIds.has(providerId); +} + function isNodeError(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && "code" in error; } @@ -183,5 +190,10 @@ export async function readConfiguredModelAccess( const provider = config.provider?.[providerId]; const apiKey = provider?.options?.apiKey; if (!provider?.models?.[modelId] || typeof apiKey !== "string" || !apiKey.trim()) return null; - return { configPath, model, providerId }; + return { + configPath, + model, + providerId, + tuiCompatible: isTuiCompatibleProviderId(providerId) + }; } diff --git a/test/launcher.test.ts b/test/launcher.test.ts index 51ee26e..4385b75 100644 --- a/test/launcher.test.ts +++ b/test/launcher.test.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { formatVersionOutput, + formatUnsupportedTuiProviderMessage, isTuiRuntimeInvocation, isVersionInvocation, normalizeLoginArgs, @@ -14,6 +15,7 @@ import { resolveModelRetryMaxRetries, withDefaultBrowserUse } from "../src/launcher.ts"; +import type { ConfiguredModelAccess } from "../src/model-access.ts"; import { classifyZaiOAuthInvocation } from "../src/zai-oauth.ts"; describe("launcher routing", () => { @@ -72,6 +74,21 @@ describe("launcher routing", () => { }); }); + test("explains when configured headless access cannot pass the upstream TUI gate", () => { + const access: ConfiguredModelAccess = { + configPath: "/home/user/.zcode/cli/config.json", + model: "custom/GLM-5.3", + providerId: "custom", + tuiCompatible: false + }; + + expect(formatUnsupportedTuiProviderMessage(access)).toContain( + 'provider ID "zai" or "bigmodel"' + ); + expect(formatUnsupportedTuiProviderMessage(access)).toContain(access.configPath); + expect(formatUnsupportedTuiProviderMessage(access)).not.toContain("configured-key"); + }); + test("enables Browser Use only for agent-producing runtime invocations", () => { expect(withDefaultBrowserUse([])).toEqual(["--browser-use=headless"]); expect(withDefaultBrowserUse(["tui"])).toEqual(["--browser-use=headless", "tui"]); diff --git a/test/model-access.test.ts b/test/model-access.test.ts index 1ba8412..d73b3e6 100644 --- a/test/model-access.test.ts +++ b/test/model-access.test.ts @@ -38,7 +38,31 @@ describe("configured model access", () => { expect(await readConfiguredModelAccess(env)).toEqual({ configPath: path, model: "zai/custom/model", - providerId: "zai" + providerId: "zai", + tuiCompatible: true + }); + }); + + test("reports custom provider ids as headless-only for the upstream TUI gate", async () => { + const home = await temporaryHome(); + const env = { HOME: home, USERPROFILE: home }; + const path = userConfigPath(env); + await mkdir(join(home, ".zcode", "cli"), { recursive: true }); + await writeFile(path, JSON.stringify({ + provider: { + "bigmodel-coding-plan": { + options: { apiKey: "configured-key" }, + models: { "GLM-5.3": { name: "GLM-5.3" } } + } + }, + model: { main: "bigmodel-coding-plan/GLM-5.3" } + })); + + expect(await readConfiguredModelAccess(env)).toEqual({ + configPath: path, + model: "bigmodel-coding-plan/GLM-5.3", + providerId: "bigmodel-coding-plan", + tuiCompatible: false }); });