@gajae-code/ai 0.6.4 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/types/auth-broker/client.d.ts +2 -1
- package/dist/types/auth-broker/remote-store.d.ts +3 -1
- package/dist/types/auth-broker/types.d.ts +6 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +17 -0
- package/dist/types/provider-models/special.d.ts +3 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/oauth/glm-zcode.d.ts +80 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/auth-broker/client.ts +15 -0
- package/src/auth-broker/remote-store.ts +20 -0
- package/src/auth-broker/server.ts +27 -0
- package/src/auth-broker/types.ts +12 -1
- package/src/auth-broker/wire-schemas.ts +17 -0
- package/src/auth-storage.ts +129 -11
- package/src/cli.ts +1 -0
- package/src/model-thinking.ts +11 -10
- package/src/models.json +65 -7
- package/src/models.ts +12 -5
- package/src/provider-models/descriptors.ts +7 -1
- package/src/provider-models/openai-compat.ts +6 -0
- package/src/provider-models/special.ts +12 -0
- package/src/providers/anthropic.ts +2 -2
- package/src/stream.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/oauth/glm-zcode.ts +321 -0
- package/src/utils/oauth/index.ts +10 -0
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils.ts +11 -2
package/src/model-thinking.ts
CHANGED
|
@@ -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:
|
|
408
|
-
//
|
|
409
|
-
//
|
|
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 =
|
|
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
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -33,14 +33,21 @@ function getProviderModels(provider: GeneratedProvider): Map<string, Model<Api>>
|
|
|
33
33
|
* Mythos rejecting forced tool use) without a full regeneration.
|
|
34
34
|
*/
|
|
35
35
|
function applyBundledCompatDefaults(model: Model<Api>): Model<Api> {
|
|
36
|
+
let normalized = model;
|
|
37
|
+
if (normalized.id === "minimax-m3" && normalized.name === "MiniMax M3") {
|
|
38
|
+
normalized = { ...normalized, name: "MiniMax-M3" };
|
|
39
|
+
}
|
|
36
40
|
if (
|
|
37
|
-
(
|
|
38
|
-
isClaudeForcedToolChoiceIncapableModelId(
|
|
39
|
-
(
|
|
41
|
+
(normalized.api === "anthropic-messages" || normalized.api === "bedrock-converse-stream") &&
|
|
42
|
+
isClaudeForcedToolChoiceIncapableModelId(normalized.id) &&
|
|
43
|
+
(normalized.compat as { toolChoiceSupport?: string } | undefined)?.toolChoiceSupport === undefined
|
|
40
44
|
) {
|
|
41
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
...normalized,
|
|
47
|
+
compat: { ...(normalized.compat ?? {}), toolChoiceSupport: "auto" } as Model<Api>["compat"],
|
|
48
|
+
};
|
|
42
49
|
}
|
|
43
|
-
return
|
|
50
|
+
return normalized;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export type GeneratedProvider = keyof typeof MODELS;
|
|
@@ -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
|
@@ -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
|
+
}
|
package/src/utils/oauth/index.ts
CHANGED
|
@@ -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":
|
package/src/utils/oauth/types.ts
CHANGED
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
|
|
102
|
-
const { id: _id, ...
|
|
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;
|