@sayknow-cli/ai 0.2.6 → 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.
- package/CHANGELOG.md +12 -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/providers/anthropic.d.ts +15 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/oauth/glm-zcode.d.ts +71 -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 +58 -7
- 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 +82 -2
- package/src/stream.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/oauth/glm-zcode.ts +433 -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/auth-storage.ts
CHANGED
|
@@ -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
|
|
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
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,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",
|