@sayknow-cli/ai 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -151,6 +151,29 @@ export interface AuthCredentialSnapshotEntry {
151
151
  identityKey: string | null;
152
152
  }
153
153
 
154
+ export type AuthCredentialIfAbsentReason =
155
+ | "inserted"
156
+ | "skipped-existing"
157
+ | "skipped-existing-runtime"
158
+ | "skipped-existing-config"
159
+ | "skipped-existing-env"
160
+ | "skipped-existing-fallback"
161
+ | "skipped-invalid";
162
+
163
+ export interface AuthCredentialIfAbsentResult {
164
+ inserted: boolean;
165
+ reason: AuthCredentialIfAbsentReason;
166
+ provider: string;
167
+ entries: StoredAuthCredential[];
168
+ }
169
+
170
+ export interface AuthCredentialIfAbsentSnapshotResult {
171
+ inserted: boolean;
172
+ reason: AuthCredentialIfAbsentReason;
173
+ provider: string;
174
+ entries: AuthCredentialSnapshotEntry[];
175
+ }
176
+
154
177
  /**
155
178
  * Wire-shaped snapshot exported by {@link AuthStorage.exportSnapshot} and
156
179
  * served by the auth-broker server on `GET /v1/snapshot`.
@@ -182,6 +205,7 @@ export interface AuthCredentialStore {
182
205
  tryDisableAuthCredentialIfMatches(id: number, expectedData: string, disabledCause: string): boolean;
183
206
  replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
184
207
  upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
208
+ upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
185
209
  deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
186
210
  getCache(key: string, options?: { includeExpired?: boolean }): string | null;
187
211
  setCache(key: string, value: string, expiresAtSec: number): void;
@@ -254,6 +278,10 @@ export interface AuthCredentialStore {
254
278
  * post-write read path is consistent.
255
279
  */
256
280
  upsertAuthCredentialRemote?(provider: string, credential: AuthCredential): Promise<StoredAuthCredential[]>;
281
+ upsertAuthCredentialRemoteIfAbsent?(
282
+ provider: string,
283
+ credential: AuthCredential,
284
+ ): Promise<AuthCredentialIfAbsentResult>;
257
285
  /**
258
286
  * Optional async write hook for replace-all semantics (e.g. API-key login
259
287
  * overwriting any previous keys for the same provider). When present,
@@ -1234,6 +1262,56 @@ export class AuthStorage {
1234
1262
  this.#resetProviderAssignments(provider);
1235
1263
  }
1236
1264
 
1265
+ #toSnapshotEntries(provider: string, stored: StoredAuthCredential[]): AuthCredentialSnapshotEntry[] {
1266
+ return stored.map(entry => {
1267
+ const persisted = entry.credential;
1268
+ const redacted: SnapshotCredential =
1269
+ persisted.type === "api_key" ? persisted : { ...persisted, refresh: REMOTE_REFRESH_SENTINEL };
1270
+ return {
1271
+ id: entry.id,
1272
+ provider: entry.provider,
1273
+ credential: redacted,
1274
+ identityKey: resolveCredentialIdentityKey(provider, persisted),
1275
+ };
1276
+ });
1277
+ }
1278
+
1279
+ #snapshotSkipResult(provider: string, reason: AuthCredentialIfAbsentReason): AuthCredentialIfAbsentSnapshotResult {
1280
+ return {
1281
+ inserted: false,
1282
+ reason,
1283
+ provider,
1284
+ entries: this.exportSnapshot().credentials.filter(entry => entry.provider === provider),
1285
+ };
1286
+ }
1287
+
1288
+ async importCredentialIfAbsent(
1289
+ provider: string,
1290
+ credential: AuthCredential,
1291
+ ): Promise<AuthCredentialIfAbsentSnapshotResult> {
1292
+ if (this.#runtimeOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-runtime");
1293
+ if (this.#configOverrides.has(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-config");
1294
+ if (this.#getCredentialsForProvider(provider).length > 0)
1295
+ return this.#snapshotSkipResult(provider, "skipped-existing");
1296
+ if (getEnvApiKey(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-env");
1297
+ if (this.#fallbackResolver?.(provider)) return this.#snapshotSkipResult(provider, "skipped-existing-fallback");
1298
+
1299
+ const result = this.#store.upsertAuthCredentialRemoteIfAbsent
1300
+ ? await this.#store.upsertAuthCredentialRemoteIfAbsent(provider, credential)
1301
+ : this.#store.upsertAuthCredentialForProviderIfAbsent(provider, credential);
1302
+ this.#setStoredCredentials(
1303
+ provider,
1304
+ result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
1305
+ );
1306
+ this.#resetProviderAssignments(provider);
1307
+ return {
1308
+ inserted: result.inserted,
1309
+ reason: result.reason,
1310
+ provider: result.provider,
1311
+ entries: this.#toSnapshotEntries(provider, result.entries),
1312
+ };
1313
+ }
1314
+
1237
1315
  async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
1238
1316
  const stored = this.#store.upsertAuthCredentialRemote
1239
1317
  ? await this.#store.upsertAuthCredentialRemote(provider, credential)
@@ -1515,6 +1593,14 @@ export class AuthStorage {
1515
1593
  });
1516
1594
  break;
1517
1595
  }
1596
+ case "glm-zcode": {
1597
+ const { loginGlmZcode } = await import("./utils/oauth/glm-zcode");
1598
+ credentials = await loginGlmZcode({
1599
+ ...ctrl,
1600
+ onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
1601
+ });
1602
+ break;
1603
+ }
1518
1604
  case "fireworks": {
1519
1605
  const { loginFireworks } = await import("./utils/oauth/fireworks");
1520
1606
  const apiKey = await loginFireworks(ctrl);
@@ -3343,17 +3429,7 @@ export class AuthStorage {
3343
3429
  stored.map(entry => ({ id: entry.id, credential: entry.credential })),
3344
3430
  );
3345
3431
  this.#resetProviderAssignments(provider);
3346
- return stored.map(entry => {
3347
- const persisted = entry.credential;
3348
- const redacted: SnapshotCredential =
3349
- persisted.type === "api_key" ? persisted : { ...persisted, refresh: REMOTE_REFRESH_SENTINEL };
3350
- return {
3351
- id: entry.id,
3352
- provider: entry.provider,
3353
- credential: redacted,
3354
- identityKey: resolveCredentialIdentityKey(provider, persisted),
3355
- };
3356
- });
3432
+ return this.#toSnapshotEntries(provider, stored);
3357
3433
  }
3358
3434
 
3359
3435
  /**
@@ -3989,6 +4065,48 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
3989
4065
  return result;
3990
4066
  }
3991
4067
 
4068
+ upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult {
4069
+ let serialized: SerializedCredentialRecord | null;
4070
+ try {
4071
+ serialized = serializeCredential(provider, credential);
4072
+ } catch {
4073
+ serialized = null;
4074
+ }
4075
+ if (!serialized) {
4076
+ return {
4077
+ inserted: false,
4078
+ reason: "skipped-invalid",
4079
+ provider,
4080
+ entries: this.listAuthCredentials(provider),
4081
+ };
4082
+ }
4083
+
4084
+ const writeIfAbsent = this.#db.transaction(
4085
+ (providerName: string, record: SerializedCredentialRecord): AuthCredentialIfAbsentResult => {
4086
+ const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
4087
+ const existing: StoredAuthCredential[] = [];
4088
+ for (const row of existingRows) {
4089
+ const activeCredential = deserializeCredential(row);
4090
+ if (!activeCredential) continue;
4091
+ existing.push(toStoredAuthCredential(row, activeCredential));
4092
+ }
4093
+ if (existing.length > 0) {
4094
+ return { inserted: false, reason: "skipped-existing", provider: providerName, entries: existing };
4095
+ }
4096
+
4097
+ this.#insertStmt.get(providerName, record.credentialType, record.data, record.identityKey);
4098
+ return {
4099
+ inserted: true,
4100
+ reason: "inserted",
4101
+ provider: providerName,
4102
+ entries: this.listAuthCredentials(providerName),
4103
+ };
4104
+ },
4105
+ );
4106
+
4107
+ return writeIfAbsent.immediate(provider, serialized);
4108
+ }
4109
+
3992
4110
  /**
3993
4111
  * Hard-deletes disabled rows for a provider when an active row with the same identity exists.
3994
4112
  * This prevents unbounded accumulation of soft-deleted credentials while preserving
package/src/cli.ts CHANGED
@@ -109,6 +109,7 @@ Providers:
109
109
  kagi Kagi
110
110
  tavily Tavily
111
111
  zai Z.AI (GLM Coding Plan)
112
+ glm-zcode GLM ZCode OAuth (unofficial, opt-in; at your own risk)
112
113
  deepseek DeepSeek
113
114
  xai xAI
114
115
  nanogpt NanoGPT
@@ -404,13 +404,11 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
404
404
  if (model.provider === "zai" && model.id === "glm-5.2") {
405
405
  model.contextWindow = 1_000_000;
406
406
  }
407
- // MiniMax-M3: official MiniMax docs (platform.minimax.io/docs/guides/models-intro)
408
- // document a 1M context window, but models.dev and the bundled catalog both report
409
- // 512K. The stale 512K survives generate-models (provider-scoped models bypass the
410
- // models.dev refresh in applyGlobalModelsDevFallback), tripping auto-compaction /
411
- // context-cap thresholds 2x early on MiniMax sessions. Pin to the true 1M.
407
+ // MiniMax-M3: MiniMax exposes a 1M context tier, but usage beyond 512K is
408
+ // billed separately. Keep bundled/default metadata at the billing-safe 512K
409
+ // unless an explicit paid-tier contract is added.
412
410
  if (model.provider !== "opencode-go" && model.id === "minimax-m3") {
413
- model.contextWindow = 1_000_000;
411
+ model.contextWindow = 512_000;
414
412
  }
415
413
  }
416
414
 
@@ -455,10 +453,13 @@ function inferGeneratedApplyPatchToolType(
455
453
  }
456
454
 
457
455
  function applyGpt55ContextWindow(model: ApiModel<Api>, parsedModel: OpenAIModel): boolean {
458
- // gpt-5.5 is a 400K-context model. OpenAI code backend discovery can omit the
459
- // context window, falling back to the 272K default, which incorrectly trips
460
- // context-cap / auto-promote thresholds (a ~272K session would look over-cap
461
- // and demote to gpt-5.4). Pin gpt-5.5 to its true 400K window.
456
+ // OpenAI Codex reports GPT-5.5 with a 272K prompt budget. Keep the generated
457
+ // bundle aligned with the backend limit so compaction fires before the prompt
458
+ // crosses the usable Codex window instead of trusting stale 400K snapshots.
459
+ if (model.provider === "openai-codex" && parsedModel.variant === "base" && semverEqual(parsedModel.version, "5.5")) {
460
+ model.contextWindow = 272000;
461
+ return true;
462
+ }
462
463
  if (parsedModel.variant === "base" && semverEqual(parsedModel.version, "5.5")) {
463
464
  model.contextWindow = 400000;
464
465
  return true;
package/src/models.json CHANGED
@@ -10486,6 +10486,31 @@
10486
10486
  "maxLevel": "high"
10487
10487
  }
10488
10488
  },
10489
+ "gemini-3.5-flash": {
10490
+ "id": "gemini-3.5-flash",
10491
+ "name": "Gemini 3.5 Flash",
10492
+ "api": "google-gemini-cli",
10493
+ "provider": "google-gemini-cli",
10494
+ "baseUrl": "https://cloudcode-pa.googleapis.com",
10495
+ "reasoning": true,
10496
+ "input": [
10497
+ "text",
10498
+ "image"
10499
+ ],
10500
+ "cost": {
10501
+ "input": 1.5,
10502
+ "output": 9,
10503
+ "cacheRead": 0.15,
10504
+ "cacheWrite": 0
10505
+ },
10506
+ "contextWindow": 1048576,
10507
+ "maxTokens": 65536,
10508
+ "thinking": {
10509
+ "mode": "google-level",
10510
+ "minLevel": "minimal",
10511
+ "maxLevel": "high"
10512
+ }
10513
+ },
10489
10514
  "gemini-3-flash-preview": {
10490
10515
  "id": "gemini-3-flash-preview",
10491
10516
  "name": "Gemini 3 Flash Preview",
@@ -37453,7 +37478,7 @@
37453
37478
  "cacheRead": 0.12,
37454
37479
  "cacheWrite": 0
37455
37480
  },
37456
- "contextWindow": 1000000,
37481
+ "contextWindow": 512000,
37457
37482
  "maxTokens": 128000,
37458
37483
  "thinking": {
37459
37484
  "mode": "budget",
@@ -37673,7 +37698,7 @@
37673
37698
  "cacheRead": 0.12,
37674
37699
  "cacheWrite": 0
37675
37700
  },
37676
- "contextWindow": 1000000,
37701
+ "contextWindow": 512000,
37677
37702
  "maxTokens": 128000,
37678
37703
  "thinking": {
37679
37704
  "mode": "budget",
@@ -37965,7 +37990,7 @@
37965
37990
  "cacheRead": 0,
37966
37991
  "cacheWrite": 0
37967
37992
  },
37968
- "contextWindow": 1000000,
37993
+ "contextWindow": 512000,
37969
37994
  "maxTokens": 128000,
37970
37995
  "compat": {
37971
37996
  "supportsStore": false,
@@ -38300,7 +38325,7 @@
38300
38325
  "cacheRead": 0,
38301
38326
  "cacheWrite": 0
38302
38327
  },
38303
- "contextWindow": 1000000,
38328
+ "contextWindow": 512000,
38304
38329
  "maxTokens": 128000,
38305
38330
  "compat": {
38306
38331
  "supportsStore": false,
@@ -56285,7 +56310,7 @@
56285
56310
  "cacheRead": 0.5,
56286
56311
  "cacheWrite": 0
56287
56312
  },
56288
- "contextWindow": 400000,
56313
+ "contextWindow": 272000,
56289
56314
  "maxTokens": 128000,
56290
56315
  "preferWebsockets": true,
56291
56316
  "priority": 9,
@@ -68507,7 +68532,7 @@
68507
68532
  "cacheRead": 0,
68508
68533
  "cacheWrite": 0
68509
68534
  },
68510
- "contextWindow": 1000000,
68535
+ "contextWindow": 512000,
68511
68536
  "maxTokens": 128000,
68512
68537
  "compat": {
68513
68538
  "supportsUsageInStreaming": false
@@ -75673,6 +75698,32 @@
75673
75698
  }
75674
75699
  }
75675
75700
  },
75701
+ "glm-zcode": {
75702
+ "glm-5.2": {
75703
+ "id": "glm-5.2",
75704
+ "name": "GLM-5.2 (ZCode)",
75705
+ "api": "anthropic-messages",
75706
+ "provider": "glm-zcode",
75707
+ "baseUrl": "https://api.z.ai/api/anthropic",
75708
+ "reasoning": true,
75709
+ "input": [
75710
+ "text"
75711
+ ],
75712
+ "cost": {
75713
+ "input": 0,
75714
+ "output": 0,
75715
+ "cacheRead": 0,
75716
+ "cacheWrite": 0
75717
+ },
75718
+ "contextWindow": 1000000,
75719
+ "maxTokens": 131072,
75720
+ "thinking": {
75721
+ "mode": "budget",
75722
+ "minLevel": "minimal",
75723
+ "maxLevel": "xhigh"
75724
+ }
75725
+ }
75726
+ },
75676
75727
  "zenmux": {
75677
75728
  "anthropic/claude-3.5-haiku": {
75678
75729
  "id": "anthropic/claude-3.5-haiku",
@@ -79707,4 +79758,4 @@
79707
79758
  }
79708
79759
  }
79709
79760
  }
79710
- }
79761
+ }
@@ -43,7 +43,7 @@ import {
43
43
  xiaomiModelManagerOptions,
44
44
  zenmuxModelManagerOptions,
45
45
  } from "./openai-compat";
46
- import { cursorModelManagerOptions, zaiModelManagerOptions } from "./special";
46
+ import { cursorModelManagerOptions, glmZcodeModelManagerOptions, zaiModelManagerOptions } from "./special";
47
47
 
48
48
  /** Catalog discovery configuration for providers that support endpoint-based model listing. */
49
49
  export interface CatalogDiscoveryConfig {
@@ -299,6 +299,12 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
299
299
  catalog("ZenMux", ["ZENMUX_API_KEY"]),
300
300
  ),
301
301
  catalogDescriptor("zai", "glm-5.2", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
302
+ catalogDescriptor(
303
+ "glm-zcode",
304
+ "glm-5.2",
305
+ config => glmZcodeModelManagerOptions(config),
306
+ catalog("GLM ZCode (unofficial)", ["GLM_ZCODE_API_KEY"], { oauthProvider: "glm-zcode" }),
307
+ ),
302
308
  descriptor("github-copilot", "gpt-4o", config => githubCopilotModelManagerOptions(config)),
303
309
  descriptor("google", "gemini-2.5-pro", config => googleModelManagerOptions(config)),
304
310
  catalogDescriptor(
@@ -2278,6 +2278,12 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
2278
2278
  const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDescriptor[] = [
2279
2279
  // --- zAI ---
2280
2280
  anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
2281
+ // --- GLM ZCode (unofficial Z.AI OAuth) ---
2282
+ anthropicMessagesDescriptor(
2283
+ "glm-zcode-coding-plan",
2284
+ "glm-zcode",
2285
+ process.env.ZCODE_PLAN_ANTHROPIC_BASE_URL ?? "https://api.z.ai/api/anthropic",
2286
+ ),
2281
2287
  // --- Xiaomi ---
2282
2288
  openAiCompletionsDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/v1", {
2283
2289
  defaultContextWindow: 262144,
@@ -65,3 +65,15 @@ export interface ZaiModelManagerConfig {}
65
65
  export function zaiModelManagerOptions(_config: ZaiModelManagerConfig = {}): ModelManagerOptions<"anthropic-messages"> {
66
66
  return { providerId: "zai" };
67
67
  }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // GLM ZCode (unofficial Z.AI OAuth)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export interface GlmZcodeModelManagerConfig {}
74
+
75
+ export function glmZcodeModelManagerOptions(
76
+ _config: GlmZcodeModelManagerConfig = {},
77
+ ): ModelManagerOptions<"anthropic-messages"> {
78
+ return { providerId: "glm-zcode" };
79
+ }
@@ -1,5 +1,6 @@
1
1
  import * as nodeCrypto from "node:crypto";
2
2
  import * as fs from "node:fs";
3
+ import * as os from "node:os";
3
4
  import { scheduler } from "node:timers/promises";
4
5
  import * as tls from "node:tls";
5
6
  import Anthropic, { type ClientOptions as AnthropicSdkClientOptions } from "@anthropic-ai/sdk";
@@ -93,6 +94,13 @@ export type AnthropicHeaderOptions = {
93
94
  stream?: boolean;
94
95
  modelHeaders?: Record<string, string>;
95
96
  isCloudflareAiGateway?: boolean;
97
+ /**
98
+ * Attach ZCode client "source" headers (User-Agent: ZCode/<ver>, X-Title,
99
+ * X-ZCode-Agent: glm, X-Platform, etc.) so api.z.ai recognizes the caller as
100
+ * the ZCode client, exactly like ZCode's `buildZCodeSourceHeaders` does for
101
+ * GLM providers. glm-zcode only.
102
+ */
103
+ zcodeSourceHeaders?: boolean;
96
104
  };
97
105
 
98
106
  export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
@@ -161,6 +169,67 @@ const sharedHeaders = {
161
169
  "X-App": "cli",
162
170
  };
163
171
 
172
+ // ZCode bakes its app version and runtime env at build time. Mirror the values
173
+ // from the analyzed ZCode 3.1.2 desktop bundle (`resolveRuntimeZCodeEnv` returns
174
+ // "production" for non-test builds). Both are overridable for forward-compat.
175
+ const ZCODE_APP_VERSION = process.env.ZCODE_APP_VERSION?.trim() || "3.1.2";
176
+ const ZCODE_RELEASE_CHANNEL = process.env.ZCODE_RELEASE_CHANNEL?.trim() || "production";
177
+
178
+ // Mirrors ZCode's `normalizePrintableHeaderValue`: only printable ASCII passes.
179
+ function normalizePrintableHeaderValue(value: string | undefined): string | undefined {
180
+ const trimmed = value?.trim();
181
+ if (trimmed && /^[\x20-\x7e]+$/.test(trimmed)) return trimmed;
182
+ return undefined;
183
+ }
184
+
185
+ // Mirrors ZCode's `normalizeOsCategory`.
186
+ function normalizeOsCategory(platform: NodeJS.Platform): string {
187
+ switch (platform) {
188
+ case "darwin":
189
+ return "macos";
190
+ case "win32":
191
+ return "windows";
192
+ default:
193
+ return "linux";
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Replicates ZCode's `buildZCodeSourceHeaders()` + GLM `X-ZCode-Agent` tag
199
+ * (host bundle `Bl` / `buildConnectivitySourceHeaders` for GLM providers), so
200
+ * api.z.ai sees skc's glm-zcode requests as the ZCode client. Dynamic values
201
+ * (platform/arch, locale, timezone, OS version) are resolved at runtime exactly
202
+ * as ZCode does; printable-ASCII-only and conditionally omitted when empty.
203
+ */
204
+ export function buildZCodeSourceHeaders(): Record<string, string> {
205
+ const platform = process.platform;
206
+ const arch = process.arch;
207
+ const appVersion = normalizePrintableHeaderValue(ZCODE_APP_VERSION);
208
+ const releaseChannel = normalizePrintableHeaderValue(ZCODE_RELEASE_CHANNEL);
209
+ let locale: string | undefined;
210
+ let timezone: string | undefined;
211
+ try {
212
+ const resolved = Intl.DateTimeFormat().resolvedOptions();
213
+ locale = normalizePrintableHeaderValue(resolved.locale);
214
+ timezone = normalizePrintableHeaderValue(resolved.timeZone);
215
+ } catch {}
216
+ const osVersion = normalizePrintableHeaderValue(os.version());
217
+ const headers: Record<string, string> = {
218
+ "User-Agent": `ZCode/${appVersion ?? "unknown"}`,
219
+ "HTTP-Referer": "https://zcode.z.ai",
220
+ "X-Title": "Z Code@electron",
221
+ "X-Platform": `${platform}-${arch}`,
222
+ "X-Client-Language": locale ?? "unknown",
223
+ "X-Client-Timezone": timezone ?? "unknown",
224
+ "X-Os-Category": normalizeOsCategory(platform),
225
+ "X-ZCode-Agent": "glm",
226
+ };
227
+ if (appVersion) headers["X-ZCode-App-Version"] = appVersion;
228
+ if (releaseChannel) headers["X-Release-Channel"] = releaseChannel;
229
+ if (osVersion) headers["X-Os-Version"] = osVersion;
230
+ return headers;
231
+ }
232
+
164
233
  export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<string, string> {
165
234
  const oauthToken = options.isOAuth ?? isAnthropicOAuthToken(options.apiKey);
166
235
  const extraBetas = options.extraBetas ?? [];
@@ -197,6 +266,9 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
197
266
  };
198
267
  } else if (!isAnthropicApiBaseUrl(options.baseUrl)) {
199
268
  const incomingUserAgent = getHeaderCaseInsensitive(options.modelHeaders, "User-Agent");
269
+ // ZCode merges its source headers LAST for GLM providers (`withZCodeSourceHeaders`
270
+ // → `{ ...base, ...extra, ...source }`), so they win over any incoming User-Agent.
271
+ const zcodeSourceHeaders = options.zcodeSourceHeaders ? buildZCodeSourceHeaders() : undefined;
200
272
  return {
201
273
  ...modelHeaders,
202
274
  Accept: acceptHeader,
@@ -204,6 +276,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
204
276
  ...sharedHeaders,
205
277
  "Anthropic-Beta": betaHeader,
206
278
  ...(incomingUserAgent ? { "User-Agent": incomingUserAgent } : {}),
279
+ ...(zcodeSourceHeaders ?? {}),
207
280
  };
208
281
  } else {
209
282
  return {
@@ -678,6 +751,12 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
678
751
  if (model.provider === "github-copilot") {
679
752
  return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
680
753
  }
754
+ // glm-zcode logs in via ZCode's OAuth but auto-provisions a real Z.AI API key and
755
+ // calls api.z.ai directly (no zcode.z.ai gateway, no captcha). Pin the base so dynamic
756
+ // discovery / stale bundled catalogs / model cache can't redirect it elsewhere.
757
+ if (model.provider === "glm-zcode") {
758
+ return normalizeAnthropicBaseUrl(process.env.ZCODE_PLAN_ANTHROPIC_BASE_URL) ?? "https://api.z.ai/api/anthropic";
759
+ }
681
760
  if (model.provider === "anthropic" && isFoundryEnabled()) {
682
761
  const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
683
762
  if (foundryBaseUrl) {
@@ -1697,6 +1776,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
1697
1776
  stream,
1698
1777
  modelHeaders: mergeHeaders(model.headers, foundryCustomHeaders, headers, dynamicHeaders),
1699
1778
  isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
1779
+ zcodeSourceHeaders: model.provider === "glm-zcode",
1700
1780
  });
1701
1781
 
1702
1782
  if (model.provider === "cloudflare-ai-gateway") {
@@ -2169,7 +2249,7 @@ function buildParams(
2169
2249
  * See: https://github.com/jaybeyond/Sayknow_CLI/issues/814
2170
2250
  */
2171
2251
  function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2172
- if (model.provider === "zai") return true;
2252
+ if (model.provider === "zai" || model.provider === "glm-zcode") return true;
2173
2253
  const baseUrl = model.baseUrl;
2174
2254
  if (!baseUrl) return false;
2175
2255
  try {
@@ -2187,7 +2267,7 @@ function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2187
2267
  */
2188
2268
  function isNonSigningAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2189
2269
  // Known non-signing providers
2190
- if (model.provider === "zai" || model.provider === "deepseek") return true;
2270
+ if (model.provider === "zai" || model.provider === "glm-zcode" || model.provider === "deepseek") return true;
2191
2271
  const baseUrl = model.baseUrl;
2192
2272
  if (!baseUrl) return false;
2193
2273
  try {
package/src/stream.ts CHANGED
@@ -88,6 +88,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
88
88
  kilo: "KILO_API_KEY",
89
89
  "vercel-ai-gateway": "AI_GATEWAY_API_KEY",
90
90
  zai: "ZAI_API_KEY",
91
+ "glm-zcode": "GLM_ZCODE_API_KEY",
91
92
  mistral: "MISTRAL_API_KEY",
92
93
  minimax: "MINIMAX_API_KEY",
93
94
  "minimax-code": "MINIMAX_CODE_API_KEY",
package/src/types.ts CHANGED
@@ -122,6 +122,7 @@ export type KnownProvider =
122
122
  | "kilo"
123
123
  | "vercel-ai-gateway"
124
124
  | "zai"
125
+ | "glm-zcode"
125
126
  | "mistral"
126
127
  | "minimax"
127
128
  | "opencode-go"