feat(models): add Z.AI (GLM) credential integration and setup flow

Implement first-class Z.AI credential storage and authentication:

- New auth provider: src/auth/zai.ts for Z.AI API key management
- New CLI command: flynn zai-auth to store Z.AI API keys
- New TUI command: /login zai for interactive credential entry
- Modified src/auth/index.ts to register zai provider
- Modified src/cli/index.ts to register zai-auth command
- Modified src/cli/setup/providers.ts to include Z.AI in setup wizard
- Modified src/daemon/models.ts to support zhipuai use_oauth flag
- Modified src/daemon/clientFactory.test.ts to add Z.AI tests
- Modified src/frontends/tui/commands.ts to add login command
- Modified src/frontends/tui/minimal.ts to support credential prompts

This allows users to authenticate with Z.AI (GLM models) without
embedding secrets in config files. Credentials are stored securely in
~/.config/flynn/auth.json and resolved at runtime.

Updated state.json with new feature entry documenting the integration.
This commit is contained in:
William Valentin
2026-02-13 16:23:49 -08:00
parent 8a6cd7f559
commit 7df0569a39
10 changed files with 271 additions and 4 deletions
+8
View File
@@ -20,3 +20,11 @@ export {
type OpenAIOAuthInfo,
type IdTokenClaims,
} from './openai.js';
export {
loadStoredZaiAuth,
storeZaiAuth,
clearZaiAuth,
getZaiApiKey,
type ZaiAuthInfo,
} from './zai.js';
+84
View File
@@ -0,0 +1,84 @@
import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
const AUTH_DIR = resolve(homedir(), '.config/flynn');
const AUTH_FILE = resolve(AUTH_DIR, 'auth.json');
export interface ZaiAuthInfo {
/** Z.AI API key (used as Bearer token). */
api_key: string;
created_at: string;
}
interface AuthStore {
// Keep other provider entries untyped to avoid cross-module coupling.
github?: unknown;
openai?: unknown;
/** Preferred key for Z.AI credentials. */
zai?: ZaiAuthInfo;
/** Back-compat: older naming that matches the model provider id. */
zhipuai?: ZaiAuthInfo;
}
function safeJsonParse<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
function readAuthStore(): AuthStore {
try {
const raw = readFileSync(AUTH_FILE, 'utf-8');
const parsed = safeJsonParse<AuthStore>(raw);
return parsed ?? {};
} catch {
return {};
}
}
function writeAuthStore(store: AuthStore): void {
mkdirSync(AUTH_DIR, { recursive: true });
writeFileSync(AUTH_FILE, JSON.stringify(store, null, 2) + '\n', 'utf-8');
chmodSync(AUTH_FILE, 0o600);
}
export function loadStoredZaiAuth(): ZaiAuthInfo | null {
const store = readAuthStore();
return store.zai ?? store.zhipuai ?? null;
}
export function storeZaiAuth(apiKey: string): void {
const trimmed = apiKey.trim();
if (!trimmed) {
throw new Error('Z.AI API key is empty');
}
const store = readAuthStore();
store.zai = { api_key: trimmed, created_at: new Date().toISOString() };
// Also write the provider-id alias for compatibility.
store.zhipuai = store.zai;
writeAuthStore(store);
}
export function clearZaiAuth(): void {
const store = readAuthStore();
delete store.zai;
delete store.zhipuai;
writeAuthStore(store);
}
/**
* Get a Z.AI credential from any available source.
* Priority: ZAI_API_KEY → ZHIPUAI_API_KEY → ZHIPUAI_AUTH_TOKEN → stored auth.json.
*/
export function getZaiApiKey(): string | null {
const raw = process.env.ZAI_API_KEY
?? process.env.ZHIPUAI_API_KEY
?? process.env.ZHIPUAI_AUTH_TOKEN
?? loadStoredZaiAuth()?.api_key;
if (!raw) {return null;}
return raw.startsWith('Bearer ') ? raw.slice('Bearer '.length) : raw;
}