@gajae-code/ai 0.6.5 → 0.7.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.
@@ -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,39 @@
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://zcode.z.ai/api/v1/zcode-plan/anthropic",
75708
+ "headers": {
75709
+ "User-Agent": "ZCode/1.0.0",
75710
+ "HTTP-Referer": "https://zcode.z.ai",
75711
+ "X-Title": "Z Code@electron",
75712
+ "X-ZCode-App-Version": "1.0.0",
75713
+ "X-ZCode-Agent": "glm"
75714
+ },
75715
+ "reasoning": true,
75716
+ "input": [
75717
+ "text"
75718
+ ],
75719
+ "cost": {
75720
+ "input": 0,
75721
+ "output": 0,
75722
+ "cacheRead": 0,
75723
+ "cacheWrite": 0
75724
+ },
75725
+ "contextWindow": 1000000,
75726
+ "maxTokens": 131072,
75727
+ "thinking": {
75728
+ "mode": "budget",
75729
+ "minLevel": "minimal",
75730
+ "maxLevel": "xhigh"
75731
+ }
75732
+ }
75733
+ },
75676
75734
  "zenmux": {
75677
75735
  "anthropic/claude-3.5-haiku": {
75678
75736
  "id": "anthropic/claude-3.5-haiku",
@@ -79707,4 +79765,4 @@
79707
79765
  }
79708
79766
  }
79709
79767
  }
79710
- }
79768
+ }
@@ -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://zcode.z.ai/api/v1/zcode-plan/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
+ }
@@ -2169,7 +2169,7 @@ function buildParams(
2169
2169
  * See: https://github.com/can1357/gajae-code/issues/814
2170
2170
  */
2171
2171
  function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2172
- if (model.provider === "zai") return true;
2172
+ if (model.provider === "zai" || model.provider === "glm-zcode") return true;
2173
2173
  const baseUrl = model.baseUrl;
2174
2174
  if (!baseUrl) return false;
2175
2175
  try {
@@ -2187,7 +2187,7 @@ function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2187
2187
  */
2188
2188
  function isNonSigningAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
2189
2189
  // Known non-signing providers
2190
- if (model.provider === "zai" || model.provider === "deepseek") return true;
2190
+ if (model.provider === "zai" || model.provider === "glm-zcode" || model.provider === "deepseek") return true;
2191
2191
  const baseUrl = model.baseUrl;
2192
2192
  if (!baseUrl) return false;
2193
2193
  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"
@@ -0,0 +1,321 @@
1
+ /**
2
+ * GLM ZCode OAuth flow (UNOFFICIAL, opt-in).
3
+ *
4
+ * Replicates the reverse-engineered ZCode desktop-app login to Z.AI/GLM. This
5
+ * is NOT an official Z.AI OAuth client: it reuses ZCode's authorize page,
6
+ * broker, and a custom-protocol redirect. It may break at any time and may
7
+ * violate ZCode/Z.AI Terms of Service. Endpoints and the client id are
8
+ * overridable via `ZCODE_OAUTH_*` environment variables.
9
+ *
10
+ * Login flow:
11
+ * 1. Authorize: GET {authorize}?redirect_uri=zcode://oauth/callback&response_type=code&client_id=...&state=...
12
+ * (custom-protocol redirect → a CLI cannot catch it, so the user pastes the code/redirect URL)
13
+ * 2. Broker: POST {broker} { provider: "zai", code, redirect_uri, state }
14
+ * → { code: 0, data: { token: <ZCode JWT>, zai: { access_token: <upstream Z.AI token> }, expires_in } }
15
+ *
16
+ * Credential mapping (verified against the ZCode host bundle):
17
+ * - `access` = the **ZCode JWT** (`data.token`). This is the GLM coding-plan
18
+ * model credential: ZCode stores it under the `zcodejwttoken`
19
+ * key and sends it as `Authorization: Bearer` to the coding-plan
20
+ * gateway `${ZCODE_PLAN_ANTHROPIC_BASE_URL}` (default
21
+ * https://zcode.z.ai/api/v1/zcode-plan/anthropic), which
22
+ * validates the ZCode session and injects the upstream GLM key
23
+ * server-side. Model traffic does NOT go to api.z.ai directly.
24
+ * - `refresh` = the upstream Z.AI OAuth access token (`data.zai.access_token`),
25
+ * kept for identity/userinfo only. ZCode's separate z/login
26
+ * "business token" is used by ZCode for billing/userinfo, NOT
27
+ * model calls, so it is intentionally never minted here.
28
+ *
29
+ * The ZCode JWT reaches the Anthropic-messages request as a plain bearer
30
+ * automatically (the gateway base is not api.anthropic.com). This provider must
31
+ * NEVER force `isOAuth=true`, which would route GLM into the Claude-Code OAuth
32
+ * header branch (claude-cli UA, `claude_` tool prefixes, Claude system prompt).
33
+ */
34
+ import { OAuthCallbackFlow, type OAuthCallbackFlowOptions, parseCallbackInput } from "./callback-server";
35
+ import type { OAuthController, OAuthCredentials } from "./types";
36
+
37
+ const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
38
+ export const GLM_ZCODE_REFRESH_SKEW_MS = 2 * 60 * 1000;
39
+
40
+ /** Default endpoints / client id. Override via the matching `ZCODE_OAUTH_*` env vars. */
41
+ export const GLM_ZCODE_OAUTH_AUTHORIZE_URL = "https://chat.z.ai/api/oauth/authorize";
42
+ export const GLM_ZCODE_OAUTH_CLIENT_ID = "client_P8X5CMWmlaRO9gyO-KSqtg";
43
+ export const GLM_ZCODE_OAUTH_REDIRECT_URI = "zcode://oauth/callback";
44
+ export const GLM_ZCODE_OAUTH_BROKER_TOKEN_URL = "https://zcode.z.ai/api/v1/oauth/token";
45
+ export const GLM_ZCODE_USERINFO_URL = "https://chat.z.ai/api/oauth/userinfo";
46
+
47
+ /**
48
+ * Default coding-plan ("start plan") Anthropic gateway base. ZCode derives this
49
+ * as `${zcodeBackend}/api/v1/zcode-plan/anthropic`. Model requests go here
50
+ * (NOT api.z.ai), authenticated with the ZCode JWT. Override via
51
+ * `ZCODE_PLAN_ANTHROPIC_BASE_URL`. Exported for the model descriptor / catalog.
52
+ */
53
+ export const GLM_ZCODE_PLAN_ANTHROPIC_BASE_URL = "https://zcode.z.ai/api/v1/zcode-plan/anthropic";
54
+
55
+ type FetchImpl = typeof globalThis.fetch;
56
+
57
+ function envOr(name: string, fallback: string): string {
58
+ const value = process.env[name];
59
+ return value && value.trim().length > 0 ? value.trim() : fallback;
60
+ }
61
+
62
+ function resolveAuthorizeUrl(): string {
63
+ return envOr("ZCODE_OAUTH_AUTHORIZE_URL", GLM_ZCODE_OAUTH_AUTHORIZE_URL);
64
+ }
65
+ function resolveClientId(): string {
66
+ return envOr("ZCODE_OAUTH_CLIENT_ID", GLM_ZCODE_OAUTH_CLIENT_ID);
67
+ }
68
+ function resolveRedirectUri(): string {
69
+ return envOr("ZCODE_OAUTH_REDIRECT_URI", GLM_ZCODE_OAUTH_REDIRECT_URI);
70
+ }
71
+ function resolveBrokerTokenUrl(): string {
72
+ return envOr("ZCODE_OAUTH_BROKER_TOKEN_URL", GLM_ZCODE_OAUTH_BROKER_TOKEN_URL);
73
+ }
74
+ function resolveUserinfoUrl(): string {
75
+ return envOr("ZCODE_OAUTH_USERINFO_URL", GLM_ZCODE_USERINFO_URL);
76
+ }
77
+
78
+ /**
79
+ * The provider is configured whenever a client id is available. The real ZCode
80
+ * client id ships as the default, so this is true unless explicitly cleared.
81
+ */
82
+ export function isGlmZcodeOAuthConfigured(): boolean {
83
+ return resolveClientId().length > 0;
84
+ }
85
+
86
+ /** Mask token-like substrings so broker/upstream/JWT tokens never leak into errors or logs. */
87
+ function redactSecrets(text: string): string {
88
+ return text
89
+ .replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "[redacted-jwt]")
90
+ .replace(/[A-Za-z0-9_-]{40,}/g, "[redacted]");
91
+ }
92
+
93
+ function validateHttpsEndpoint(rawUrl: string, label: string): string {
94
+ let parsed: URL;
95
+ try {
96
+ parsed = new URL(rawUrl);
97
+ } catch {
98
+ throw new Error(`GLM ZCode ${label} endpoint is not a valid URL`);
99
+ }
100
+ if (parsed.protocol !== "https:") {
101
+ throw new Error(`GLM ZCode ${label} endpoint must use https`);
102
+ }
103
+ return parsed.toString();
104
+ }
105
+
106
+ function requestSignal(signal: AbortSignal | undefined): AbortSignal {
107
+ const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
108
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
109
+ }
110
+
111
+ function isRecord(value: unknown): value is Record<string, unknown> {
112
+ return typeof value === "object" && value !== null;
113
+ }
114
+
115
+ async function postJson(
116
+ fetchImpl: FetchImpl,
117
+ url: string,
118
+ body: Record<string, unknown>,
119
+ label: string,
120
+ signal: AbortSignal | undefined,
121
+ ): Promise<unknown> {
122
+ const response = await fetchImpl(url, {
123
+ method: "POST",
124
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
125
+ body: JSON.stringify(body),
126
+ signal: requestSignal(signal),
127
+ });
128
+ if (!response.ok) {
129
+ throw new Error(`GLM ZCode ${label} request failed: ${response.status} ${redactSecrets(await response.text())}`);
130
+ }
131
+ return response.json();
132
+ }
133
+
134
+ interface JwtPayload {
135
+ sub?: unknown;
136
+ email?: unknown;
137
+ account_id?: unknown;
138
+ uid?: unknown;
139
+ [key: string]: unknown;
140
+ }
141
+
142
+ function decodeJwtPayload(token: string): JwtPayload | undefined {
143
+ const parts = token.split(".");
144
+ const payload = parts[1];
145
+ if (parts.length !== 3 || !payload) return undefined;
146
+ try {
147
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as JwtPayload;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ interface Identity {
154
+ email?: string;
155
+ accountId?: string;
156
+ }
157
+
158
+ function identityFromJwts(tokens: readonly string[]): Identity {
159
+ for (const token of tokens) {
160
+ const payload = decodeJwtPayload(token);
161
+ if (!payload) continue;
162
+ const accountId =
163
+ (typeof payload.sub === "string" && payload.sub) ||
164
+ (typeof payload.account_id === "string" && payload.account_id) ||
165
+ (typeof payload.uid === "string" && payload.uid) ||
166
+ undefined;
167
+ const email =
168
+ typeof payload.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
169
+ if (accountId || email) {
170
+ return { accountId: accountId || undefined, email };
171
+ }
172
+ }
173
+ return {};
174
+ }
175
+
176
+ async function resolveIdentity(
177
+ fetchImpl: FetchImpl,
178
+ upstreamZaiAccess: string,
179
+ jwtCandidates: readonly string[],
180
+ signal: AbortSignal | undefined,
181
+ ): Promise<Identity> {
182
+ // Best-effort userinfo; never fail login if identity lookup fails.
183
+ try {
184
+ const userinfoUrl = validateHttpsEndpoint(resolveUserinfoUrl(), "userinfo");
185
+ const response = await fetchImpl(userinfoUrl, {
186
+ headers: { Accept: "application/json", Authorization: `Bearer ${upstreamZaiAccess}` },
187
+ signal: requestSignal(signal),
188
+ });
189
+ if (response.ok) {
190
+ const payload = (await response.json()) as unknown;
191
+ const data = isRecord(payload) && isRecord(payload.data) ? payload.data : isRecord(payload) ? payload : {};
192
+ const email = typeof data.email === "string" && data.email.length > 0 ? data.email.toLowerCase() : undefined;
193
+ const accountId =
194
+ (typeof data.id === "string" && data.id) ||
195
+ (typeof data.account_id === "string" && data.account_id) ||
196
+ (typeof data.sub === "string" && data.sub) ||
197
+ undefined;
198
+ if (email || accountId) {
199
+ return { email, accountId: accountId || undefined };
200
+ }
201
+ }
202
+ } catch {
203
+ // fall through to JWT decode
204
+ }
205
+ return identityFromJwts(jwtCandidates);
206
+ }
207
+
208
+ interface BrokerResult {
209
+ zcodeToken: string;
210
+ upstreamZaiAccess: string;
211
+ expiresIn: number;
212
+ }
213
+
214
+ function parseBrokerResponse(payload: unknown): BrokerResult {
215
+ const data = isRecord(payload) && isRecord(payload.data) ? payload.data : undefined;
216
+ const zcodeToken = data && typeof data.token === "string" ? data.token : undefined;
217
+ const zai = data && isRecord(data.zai) ? data.zai : undefined;
218
+ const upstreamZaiAccess = zai && typeof zai.access_token === "string" ? zai.access_token : undefined;
219
+ if (!zcodeToken || !upstreamZaiAccess) {
220
+ throw new Error("GLM ZCode broker response missing data.token or data.zai.access_token");
221
+ }
222
+ const expiresIn =
223
+ data && typeof data.expires_in === "number" && Number.isFinite(data.expires_in) ? data.expires_in : 3600;
224
+ return { zcodeToken, upstreamZaiAccess, expiresIn };
225
+ }
226
+
227
+ async function exchangeGlmZcodeCode(
228
+ fetchImpl: FetchImpl,
229
+ input: { code: string; state: string; redirectUri: string },
230
+ signal: AbortSignal | undefined,
231
+ ): Promise<OAuthCredentials> {
232
+ // Defensive: a pasted value may still be a full redirect URL or `code#state`.
233
+ const parsed = parseCallbackInput(input.code);
234
+ const code = parsed.code ?? input.code;
235
+ const brokerUrl = validateHttpsEndpoint(resolveBrokerTokenUrl(), "broker");
236
+ const brokerPayload = await postJson(
237
+ fetchImpl,
238
+ brokerUrl,
239
+ { provider: "zai", code, redirect_uri: input.redirectUri, state: input.state },
240
+ "broker",
241
+ signal,
242
+ );
243
+ const { zcodeToken, upstreamZaiAccess, expiresIn } = parseBrokerResponse(brokerPayload);
244
+ const identity = await resolveIdentity(fetchImpl, upstreamZaiAccess, [zcodeToken, upstreamZaiAccess], signal);
245
+ // access = ZCode JWT (the coding-plan model credential); refresh = upstream
246
+ // Z.AI token (identity only — there is no documented JWT refresh grant).
247
+ return {
248
+ access: zcodeToken,
249
+ refresh: upstreamZaiAccess,
250
+ expires: Date.now() + expiresIn * 1000 - GLM_ZCODE_REFRESH_SKEW_MS,
251
+ email: identity.email,
252
+ accountId: identity.accountId,
253
+ };
254
+ }
255
+
256
+ export interface GlmZcodeOAuthFlowOptions {
257
+ fetch?: FetchImpl;
258
+ }
259
+
260
+ export class GlmZcodeOAuthFlow extends OAuthCallbackFlow {
261
+ #fetch: FetchImpl;
262
+
263
+ constructor(ctrl: OAuthController, options: GlmZcodeOAuthFlowOptions = {}) {
264
+ super(ctrl, {
265
+ // Port 0 → a free random local port. The custom-protocol redirect
266
+ // never reaches it; login completes via manual code/redirect paste.
267
+ preferredPort: 0,
268
+ callbackPath: "/callback",
269
+ callbackHostname: "127.0.0.1",
270
+ callbackBindHostname: "127.0.0.1",
271
+ redirectUri: resolveRedirectUri(),
272
+ } satisfies OAuthCallbackFlowOptions);
273
+ this.#fetch = options.fetch ?? ctrl.fetch ?? globalThis.fetch;
274
+ }
275
+
276
+ async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
277
+ const authorizeUrl = validateHttpsEndpoint(resolveAuthorizeUrl(), "authorize");
278
+ const params = new URLSearchParams({
279
+ redirect_uri: redirectUri,
280
+ response_type: "code",
281
+ client_id: resolveClientId(),
282
+ state,
283
+ });
284
+ return {
285
+ url: `${authorizeUrl}?${params.toString()}`,
286
+ instructions:
287
+ "Complete Z.AI login in your browser. This is an UNOFFICIAL ZCode-based login — use at your own risk; it may stop working or violate ZCode/Z.AI Terms of Service. Because this CLI cannot receive the zcode:// redirect, paste the final redirect URL or authorization code when prompted.",
288
+ };
289
+ }
290
+
291
+ async exchangeToken(code: string, state: string, redirectUri: string): Promise<OAuthCredentials> {
292
+ return exchangeGlmZcodeCode(this.#fetch, { code, state, redirectUri }, this.ctrl.signal);
293
+ }
294
+ }
295
+
296
+ export async function loginGlmZcode(
297
+ ctrl: OAuthController,
298
+ options?: GlmZcodeOAuthFlowOptions,
299
+ ): Promise<OAuthCredentials> {
300
+ return new GlmZcodeOAuthFlow(ctrl, options).login();
301
+ }
302
+
303
+ export interface GlmZcodeRefreshOptions {
304
+ signal?: AbortSignal;
305
+ fetch?: FetchImpl;
306
+ }
307
+
308
+ /**
309
+ * The ZCode session JWT is the model credential. ZCode mints it from a one-time
310
+ * authorization code via the broker and exposes no documented refresh grant, so
311
+ * there is no autonomous refresh: an expired credential requires re-login
312
+ * (`/login glm-zcode`). Never return an expired credential as valid.
313
+ */
314
+ export async function refreshGlmZcodeToken(
315
+ _credentials: OAuthCredentials,
316
+ _options: AbortSignal | GlmZcodeRefreshOptions = {},
317
+ ): Promise<OAuthCredentials> {
318
+ throw new Error(
319
+ "glm-zcode session expired; re-login required (`/login glm-zcode`). The ZCode coding-plan token has no documented refresh endpoint.",
320
+ );
321
+ }
@@ -170,6 +170,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
170
170
  name: "Z.AI (GLM Coding Plan)",
171
171
  available: true,
172
172
  },
173
+ {
174
+ id: "glm-zcode",
175
+ name: "GLM ZCode OAuth (unofficial, opt-in)",
176
+ available: true,
177
+ },
173
178
  {
174
179
  id: "minimax-code",
175
180
  name: "MiniMax Coding Plan (International)",
@@ -335,6 +340,11 @@ export async function refreshOAuthToken(
335
340
  newCredentials = await refreshXaiToken(credentials.refresh);
336
341
  break;
337
342
  }
343
+ case "glm-zcode": {
344
+ const { refreshGlmZcodeToken } = await import("./glm-zcode");
345
+ newCredentials = await refreshGlmZcodeToken(credentials);
346
+ break;
347
+ }
338
348
  case "kilo":
339
349
  case "perplexity":
340
350
  case "huggingface":
@@ -49,6 +49,7 @@ export type OAuthProvider =
49
49
  | "vercel-ai-gateway"
50
50
  | "vllm"
51
51
  | "xai"
52
+ | "glm-zcode"
52
53
  | "xiaomi"
53
54
  | "xiaomi-token-plan-sgp"
54
55
  | "xiaomi-token-plan-ams"
package/src/utils.ts CHANGED
@@ -98,8 +98,10 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
98
98
  ): OpenAIResponsesReplayItem | undefined {
99
99
  if (item.type === "item_reference") return undefined;
100
100
 
101
- // providerPayload stores raw output items; replay strips item ids and keeps only normalized call_id.
102
- const { id: _id, ...sanitizedItem } = item;
101
+ // providerPayload stores raw output items; replay strips fields that are output-only.
102
+ const { id: _id, ...itemWithoutId } = item;
103
+ const sanitizedItem =
104
+ item.type === "computer_call" ? sanitizeComputerCallForResponsesInput(itemWithoutId) : itemWithoutId;
103
105
  if (typeof item.call_id === "string") {
104
106
  sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
105
107
  }
@@ -107,6 +109,13 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
107
109
  return sanitizedItem as unknown as OpenAIResponsesReplayItem;
108
110
  }
109
111
 
112
+ function sanitizeComputerCallForResponsesInput(item: Record<string, unknown>): Record<string, unknown> {
113
+ // The Responses stream includes the performed computer action on output items,
114
+ // but the create input accepts only the call identity/status fields on replay.
115
+ const { action: _action, actions: _actions, ...inputSafeItem } = item;
116
+ return inputSafeItem;
117
+ }
118
+
110
119
  function normalizeReplayedResponsesHistoryCallId(value: string, normalizedValues: Map<string, string>): string {
111
120
  const normalized = normalizedValues.get(value);
112
121
  if (normalized) return normalized;