@ottocode/sdk 0.1.315 → 0.1.316

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/sdk",
3
- "version": "0.1.315",
3
+ "version": "0.1.316",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -211,6 +211,7 @@ export async function resolveModel(
211
211
  return createZaiModel(model, {
212
212
  apiKey: config.apiKey,
213
213
  baseURL: config.baseURL,
214
+ fetch: config.customFetch,
214
215
  });
215
216
  }
216
217
 
@@ -218,6 +219,7 @@ export async function resolveModel(
218
219
  return createZaiCodingModel(model, {
219
220
  apiKey: config.apiKey,
220
221
  baseURL: config.baseURL,
222
+ fetch: config.customFetch,
221
223
  });
222
224
  }
223
225
 
@@ -15,6 +15,16 @@ let cachedLoginPath: {
15
15
  path: string | null;
16
16
  } | null = null;
17
17
 
18
+ let cachedLoginEnv: {
19
+ key: string;
20
+ env: NodeJS.ProcessEnv | null;
21
+ } | null = null;
22
+
23
+ export type ShellEnvMode = 'fast' | 'login-cache' | 'login-fresh';
24
+
25
+ const ENV_JSON_START = '___OTTO_ENV_JSON_START___';
26
+ const ENV_JSON_END = '___OTTO_ENV_JSON_END___';
27
+
18
28
  export { getAgiBinDir } from './bin-manager/paths.ts';
19
29
 
20
30
  async function whichBinary(name: string): Promise<string | null> {
@@ -72,29 +82,35 @@ export function getUserShell(): string {
72
82
  return process.env.SHELL || '/bin/bash';
73
83
  }
74
84
 
75
- function getShellRcBootstrap(shell: string): string {
76
- const shellName = shell.split('/').pop() || '';
77
- if (shellName.includes('zsh')) {
78
- return 'if [ -f "$HOME/.zshrc" ]; then . "$HOME/.zshrc"; fi';
79
- }
80
- if (shellName.includes('bash')) {
81
- return 'if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc"; fi';
82
- }
83
- return '';
84
- }
85
-
86
- function getInteractiveShellFlag(shell: string): string {
87
- const shellName = shell.split('/').pop() || '';
88
- if (shellName.includes('bash')) return '-ic';
89
- return '-ilc';
90
- }
91
-
92
- export function getShellExecutionConfig(cmd: string): {
85
+ export function getShellExecutionConfig(
86
+ cmd: string,
87
+ options?: { envMode?: ShellEnvMode },
88
+ ): {
89
+ command: string;
90
+ args: string[];
91
+ env: NodeJS.ProcessEnv;
92
+ };
93
+ export function getShellExecutionConfig(
94
+ cmd: string,
95
+ options: { envMode?: ShellEnvMode } = {},
96
+ ): {
93
97
  command: string;
94
98
  args: string[];
95
99
  env: NodeJS.ProcessEnv;
96
100
  } {
97
- const env = { ...process.env, PATH: getAugmentedPath() };
101
+ const envMode = options.envMode ?? 'fast';
102
+ const loginEnv =
103
+ envMode === 'fast' ? null : getLoginShellEnv(envMode === 'login-fresh');
104
+ const env = {
105
+ ...process.env,
106
+ ...(loginEnv ?? {}),
107
+ PATH: mergePaths([
108
+ getAgiBinDir(),
109
+ loginEnv?.PATH,
110
+ getLoginShellPath(),
111
+ process.env.PATH,
112
+ ]),
113
+ };
98
114
  if (process.platform === 'win32') {
99
115
  return {
100
116
  command: getUserShell(),
@@ -106,11 +122,56 @@ export function getShellExecutionConfig(cmd: string): {
106
122
  const command = getUserShell();
107
123
  return {
108
124
  command,
109
- args: [getInteractiveShellFlag(command), 'eval "$OTTO_SHELL_COMMAND"'],
125
+ args: ['-c', 'eval "$OTTO_SHELL_COMMAND"'],
110
126
  env: { ...env, OTTO_SHELL_COMMAND: cmd },
111
127
  };
112
128
  }
113
129
 
130
+ function getLoginShellEnv(refresh: boolean): NodeJS.ProcessEnv | null {
131
+ const home = process.env.HOME || homedir();
132
+ const userShell = getUserShell();
133
+ const cacheKey = [home, userShell, process.env.PATH || ''].join('\0');
134
+ if (!refresh && cachedLoginEnv?.key === cacheKey) return cachedLoginEnv.env;
135
+
136
+ if (process.platform === 'win32') {
137
+ cachedLoginEnv = { key: cacheKey, env: { ...process.env } };
138
+ return cachedLoginEnv.env;
139
+ }
140
+ try {
141
+ const output = execFileSync(
142
+ userShell,
143
+ [
144
+ '-ic',
145
+ `printf '%s\n' ${JSON.stringify(ENV_JSON_START)}; env; printf '%s\n' ${JSON.stringify(ENV_JSON_END)}`,
146
+ ],
147
+ {
148
+ timeout: 10000,
149
+ stdio: ['ignore', 'pipe', 'ignore'],
150
+ env: {
151
+ ...process.env,
152
+ HOME: home,
153
+ USER: process.env.USER || '',
154
+ SHELL: userShell,
155
+ },
156
+ },
157
+ ).toString();
158
+ const start = output.indexOf(ENV_JSON_START);
159
+ const end = output.indexOf(ENV_JSON_END, start + ENV_JSON_START.length);
160
+ if (start >= 0 && end > start) {
161
+ const env: NodeJS.ProcessEnv = {};
162
+ const body = output.slice(start + ENV_JSON_START.length, end).trim();
163
+ for (const line of body.split('\n')) {
164
+ const separator = line.indexOf('=');
165
+ if (separator <= 0) continue;
166
+ env[line.slice(0, separator)] = line.slice(separator + 1);
167
+ }
168
+ cachedLoginEnv = { key: cacheKey, env };
169
+ return env;
170
+ }
171
+ } catch {}
172
+ return null;
173
+ }
174
+
114
175
  function getLoginShellPath(): string | null {
115
176
  const home = process.env.HOME || homedir();
116
177
  const userShell = getUserShell();
@@ -131,10 +192,8 @@ function getLoginShellPath(): string | null {
131
192
 
132
193
  for (const shell of shellCandidates) {
133
194
  try {
134
- const rcBootstrap = getShellRcBootstrap(shell);
135
- const pathCommand = `${rcBootstrap ? `${rcBootstrap}\n` : ''}echo "___PATH___:$PATH"`;
136
- const result = execFileSync(shell, ['-ilc', pathCommand], {
137
- timeout: 5000,
195
+ const result = execFileSync(shell, ['-lc', 'echo "___PATH___:$PATH"'], {
196
+ timeout: 1500,
138
197
  stdio: ['ignore', 'pipe', 'ignore'],
139
198
  env: {
140
199
  ...process.env,
@@ -157,19 +216,15 @@ function getLoginShellPath(): string | null {
157
216
  }
158
217
 
159
218
  export function getAugmentedPath(): string {
160
- const sep = process.platform === 'win32' ? ';' : ':';
161
- const binDir = getAgiBinDir();
162
- const current = process.env.PATH || '';
163
- const loginPath = getLoginShellPath();
219
+ return mergePaths([getAgiBinDir(), getLoginShellPath(), process.env.PATH]);
220
+ }
164
221
 
222
+ function mergePaths(paths: Array<string | null | undefined>): string {
223
+ const sep = process.platform === 'win32' ? ';' : ':';
165
224
  const seen = new Set<string>();
166
225
  const parts: string[] = [];
167
226
 
168
- for (const p of [
169
- binDir,
170
- ...(loginPath ? loginPath.split(sep) : []),
171
- ...current.split(sep),
172
- ]) {
227
+ for (const p of paths.flatMap((path) => (path ? path.split(sep) : []))) {
173
228
  if (p && !seen.has(p)) {
174
229
  seen.add(p);
175
230
  parts.push(p);
@@ -3,7 +3,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { spawn } from 'node:child_process';
4
4
  import { z } from 'zod/v3';
5
5
  import DESCRIPTION from './shell.txt' with { type: 'text' };
6
- import { getShellExecutionConfig } from '../bin-manager.ts';
6
+ import { getShellExecutionConfig, type ShellEnvMode } from '../bin-manager.ts';
7
7
  import { createToolError, type ToolResponse } from '../error.ts';
8
8
  import {
9
9
  injectCoAuthorIntoGitCommit,
@@ -45,6 +45,16 @@ function killProcessTree(pid: number) {
45
45
  }
46
46
  }
47
47
 
48
+ function forceKillProcessTree(pid: number) {
49
+ try {
50
+ process.kill(-pid, 'SIGKILL');
51
+ } catch {
52
+ try {
53
+ process.kill(pid, 'SIGKILL');
54
+ } catch {}
55
+ }
56
+ }
57
+
48
58
  const REDIRECTED_SEARCH_COMMANDS = new Set(['grep', 'egrep', 'fgrep', 'rg']);
49
59
  const REDIRECTED_GLOB_COMMANDS = new Set(['find', 'fd']);
50
60
 
@@ -103,6 +113,29 @@ function repositoryDiscoveryHint(cmd: string): string | undefined {
103
113
  : `Tip: For repository file discovery, prefer the glob tool instead of shelling out to ${discovery.command}. It returns structured paths and skips common build/cache folders.`;
104
114
  }
105
115
 
116
+ const SHELL_ENV_HINT =
117
+ 'This command may require environment from your login/interactive shell. If appropriate, retry with envMode: "login-cache" (or "login-fresh" after changing shell config).';
118
+
119
+ export function detectShellEnvHint(args: {
120
+ stdout: string;
121
+ stderr: string;
122
+ exitCode: number;
123
+ envMode?: ShellEnvMode;
124
+ }): string | undefined {
125
+ if (args.envMode && args.envMode !== 'fast') return undefined;
126
+ if (args.exitCode === 0) return undefined;
127
+ const text = `${args.stderr}\n${args.stdout}`;
128
+ const patterns = [
129
+ /\b[A-Z][A-Z0-9_]{2,}\b[^\n]*(?:not set|not defined|required|missing|must be set)/i,
130
+ /(?:missing|required|could not find)[^\n]*(?:api key|token|credential|credentials|secret|environment variable|env var)/i,
131
+ /(?:no credentials|credentials[^\n]*not found|not authenticated|authentication required|please log in|please login)/i,
132
+ /(?:asdf|nvm|mise|direnv|op|doppler)[^\n]*(?:not found|not loaded|command not found)/i,
133
+ ];
134
+ return patterns.some((pattern) => pattern.test(text))
135
+ ? SHELL_ENV_HINT
136
+ : undefined;
137
+ }
138
+
106
139
  export type ShellOutputMode = 'auto' | 'full' | 'tail';
107
140
 
108
141
  const DEFAULT_TAIL_LINES = 100;
@@ -170,6 +203,8 @@ type ShellResult = ToolResponse<{
170
203
  stderrOriginalBytes?: number;
171
204
  stderrShownBytes?: number;
172
205
  discoveryHint?: string;
206
+ envMode?: ShellEnvMode;
207
+ envHint?: string;
173
208
  }>;
174
209
 
175
210
  type ShellStreamChunk =
@@ -199,7 +234,7 @@ const shellInputSchema = z
199
234
  cmd: z
200
235
  .string()
201
236
  .describe(
202
- 'Non-interactive shell command to run using the user shell with login/interactive startup loaded',
237
+ 'Non-interactive shell command to run using the user shell. Login PATH is loaded, but interactive startup files are not sourced per command.',
203
238
  ),
204
239
  cwd: z
205
240
  .string()
@@ -215,6 +250,13 @@ const shellInputSchema = z
215
250
  .optional()
216
251
  .default(300000)
217
252
  .describe('Timeout in milliseconds (default: 300000 = 5 minutes)'),
253
+ envMode: z
254
+ .enum(['fast', 'login-cache', 'login-fresh'])
255
+ .optional()
256
+ .default('fast')
257
+ .describe(
258
+ 'Environment loading mode. "fast" is the default one-off shell env. "login-cache" reuses a cached environment captured from the user login/interactive shell for commands that need shell-managed credentials. "login-fresh" refreshes that cache.',
259
+ ),
218
260
  outputMode: z
219
261
  .enum(['auto', 'full', 'tail'])
220
262
  .optional()
@@ -270,6 +312,7 @@ export function buildShellTool(projectRoot: string): {
270
312
  cwd,
271
313
  allowNonZeroExit,
272
314
  timeout = 300000,
315
+ envMode = 'fast',
273
316
  outputMode = 'auto',
274
317
  tailLines = DEFAULT_TAIL_LINES,
275
318
  maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES,
@@ -297,6 +340,7 @@ export function buildShellTool(projectRoot: string): {
297
340
  cwd: absCwd,
298
341
  allowNonZeroExit,
299
342
  timeout,
343
+ envMode,
300
344
  outputMode,
301
345
  tailLines,
302
346
  maxOutputBytes,
@@ -305,7 +349,7 @@ export function buildShellTool(projectRoot: string): {
305
349
  ) as AsyncIterable<ShellStreamChunk> | ShellResult;
306
350
  }
307
351
 
308
- const shellConfig = getShellExecutionConfig(finalCmd);
352
+ const shellConfig = getShellExecutionConfig(finalCmd, { envMode });
309
353
  const proc = spawn(shellConfig.command, shellConfig.args, {
310
354
  cwd: absCwd,
311
355
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -318,8 +362,11 @@ export function buildShellTool(projectRoot: string): {
318
362
  let didTimeout = false;
319
363
  let didAbort = false;
320
364
  let settled = false;
365
+ let terminating = false;
321
366
  let done = false;
322
367
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
368
+ let killEscalationId: ReturnType<typeof setTimeout> | null = null;
369
+ let fallbackSettleId: ReturnType<typeof setTimeout> | null = null;
323
370
  const queue: ShellStreamChunk[] = [];
324
371
  let notify: (() => void) | null = null;
325
372
 
@@ -382,6 +429,8 @@ export function buildShellTool(projectRoot: string): {
382
429
  details.maxOutputBytes = maxOutputBytes;
383
430
  }
384
431
  if (timeoutId) clearTimeout(timeoutId);
432
+ if (killEscalationId) clearTimeout(killEscalationId);
433
+ if (fallbackSettleId) clearTimeout(fallbackSettleId);
385
434
  if (abortSignal) {
386
435
  abortSignal.removeEventListener('abort', onAbort);
387
436
  }
@@ -390,11 +439,54 @@ export function buildShellTool(projectRoot: string): {
390
439
  wake();
391
440
  };
392
441
 
442
+ const abortResult = () =>
443
+ createToolError(`Command aborted by user: ${cmd}`, 'abort', {
444
+ cmd,
445
+ stdout,
446
+ stderr,
447
+ envMode,
448
+ ...(outputMode === 'tail' || outputMode === 'auto'
449
+ ? { outputMode, tailLines, maxOutputBytes }
450
+ : { outputMode, maxOutputBytes }),
451
+ });
452
+
453
+ const timeoutResult = () =>
454
+ createToolError(
455
+ `Command timed out after ${timeout}ms: ${cmd}`,
456
+ 'timeout',
457
+ {
458
+ parameter: 'timeout',
459
+ value: timeout,
460
+ stdout,
461
+ stderr,
462
+ envMode,
463
+ ...(outputMode === 'tail' || outputMode === 'auto'
464
+ ? { outputMode, tailLines, maxOutputBytes }
465
+ : { outputMode, maxOutputBytes }),
466
+ suggestion: 'Increase timeout or optimize the command',
467
+ },
468
+ );
469
+
470
+ const terminate = (fallbackResult: () => ShellResult) => {
471
+ if (terminating) return;
472
+ terminating = true;
473
+ if (proc.pid) {
474
+ killProcessTree(proc.pid);
475
+ killEscalationId = setTimeout(() => {
476
+ if (proc.pid) forceKillProcessTree(proc.pid);
477
+ }, 1000);
478
+ } else {
479
+ proc.kill('SIGTERM');
480
+ }
481
+ fallbackSettleId = setTimeout(() => {
482
+ settle(fallbackResult());
483
+ }, 2000);
484
+ };
485
+
393
486
  const onAbort = () => {
394
487
  if (settled) return;
395
488
  didAbort = true;
396
- if (proc.pid) killProcessTree(proc.pid);
397
- else proc.kill('SIGTERM');
489
+ terminate(abortResult);
398
490
  };
399
491
 
400
492
  if (abortSignal) {
@@ -404,8 +496,7 @@ export function buildShellTool(projectRoot: string): {
404
496
  if (timeout > 0) {
405
497
  timeoutId = setTimeout(() => {
406
498
  didTimeout = true;
407
- if (proc.pid) killProcessTree(proc.pid);
408
- else proc.kill();
499
+ terminate(timeoutResult);
409
500
  }, timeout);
410
501
  }
411
502
 
@@ -428,12 +519,20 @@ export function buildShellTool(projectRoot: string): {
428
519
  });
429
520
 
430
521
  proc.on('close', (exitCode) => {
522
+ const resolvedExitCode = exitCode ?? 0;
523
+ const envHint = detectShellEnvHint({
524
+ stdout,
525
+ stderr,
526
+ exitCode: resolvedExitCode,
527
+ envMode,
528
+ });
431
529
  if (didAbort) {
432
530
  settle(
433
531
  createToolError(`Command aborted by user: ${cmd}`, 'abort', {
434
532
  cmd,
435
533
  stdout,
436
534
  stderr,
535
+ envMode,
437
536
  ...(outputMode === 'tail' || outputMode === 'auto'
438
537
  ? { outputMode, tailLines, maxOutputBytes }
439
538
  : { outputMode, maxOutputBytes }),
@@ -452,6 +551,7 @@ export function buildShellTool(projectRoot: string): {
452
551
  value: timeout,
453
552
  stdout,
454
553
  stderr,
554
+ envMode,
455
555
  ...(outputMode === 'tail' || outputMode === 'auto'
456
556
  ? { outputMode, tailLines, maxOutputBytes }
457
557
  : { outputMode, maxOutputBytes }),
@@ -462,15 +562,17 @@ export function buildShellTool(projectRoot: string): {
462
562
  return;
463
563
  }
464
564
 
465
- if (exitCode !== 0 && !allowNonZeroExit) {
565
+ if (resolvedExitCode !== 0 && !allowNonZeroExit) {
466
566
  const errorDetail = stderr.trim() || stdout.trim() || '';
467
- const errorMsg = `Command failed with exit code ${exitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
567
+ const errorMsg = `Command failed with exit code ${resolvedExitCode}${errorDetail ? `\n\n${errorDetail}` : ''}`;
468
568
  settle(
469
569
  createToolError(errorMsg, 'execution', {
470
- exitCode,
570
+ exitCode: resolvedExitCode,
471
571
  stdout,
472
572
  stderr,
473
573
  cmd,
574
+ envMode,
575
+ ...(envHint ? { envHint } : {}),
474
576
  ...(outputMode === 'tail' || outputMode === 'auto'
475
577
  ? { outputMode, tailLines, maxOutputBytes }
476
578
  : { outputMode, maxOutputBytes }),
@@ -483,16 +585,17 @@ export function buildShellTool(projectRoot: string): {
483
585
  const discoveryHint = repositoryDiscoveryHint(finalCmd);
484
586
  settle({
485
587
  ok: true,
486
- exitCode: exitCode ?? 0,
588
+ exitCode: resolvedExitCode,
487
589
  stdout,
488
590
  stderr,
591
+ envMode,
489
592
  ...(discoveryHint ? { discoveryHint } : {}),
593
+ ...(envHint ? { envHint } : {}),
490
594
  ...(outputMode === 'tail' || outputMode === 'auto'
491
595
  ? { outputMode, tailLines, maxOutputBytes }
492
596
  : { outputMode, maxOutputBytes }),
493
597
  });
494
598
  });
495
-
496
599
  proc.on('error', (err) => {
497
600
  settle(
498
601
  createToolError(
@@ -1,7 +1,16 @@
1
- - Execute a non-interactive shell command using the user's shell with login/interactive startup loaded
1
+ - Execute a non-interactive shell command using the user's shell
2
2
  - Returns `stdout`, `stderr`, and `exitCode`
3
3
  - `cwd` is relative to the project root and sandboxed within it
4
4
 
5
+ The shell tool loads the user's login PATH once for command lookup, but it does not source interactive startup files for every command. Use `terminal` when you need an interactive shell, a TTY, or a persistent process.
6
+
7
+ `envMode` controls extra environment loading:
8
+ - `fast` (default): fastest one-off shell environment plus cached login PATH.
9
+ - `login-cache`: reuses a cached environment captured from the user's login/interactive shell. Use this only when a prior fast command reports missing credentials/environment, or when the user explicitly asks to load shell startup environment.
10
+ - `login-fresh`: refreshes that login/interactive shell environment cache before running the command.
11
+
12
+ If a fast command fails with an `envHint`, retry once with `envMode: "login-cache"` when appropriate. Do not use login env modes for normal repository discovery, simple build/test commands, or as a blanket default.
13
+
5
14
  **Use `shell` for one-off, non-interactive commands.** These may be short-lived checks or long-running commands that finish on their own and do not require stdin. For commands that need interactive input, a TTY, or persistence across turns, use the `terminal` tool instead.
6
15
 
7
16
  For repository discovery, use `search` for content/code search and `glob` for filename/path discovery. Reserve `shell` for execution, builds, tests, diagnostics, and other command-line tasks.
@@ -13,6 +13,35 @@ type CatalogMap = Partial<Record<BuiltInProviderId, ProviderCatalogEntry>>;
13
13
 
14
14
  const OLLAMA_CLOUD_ID: BuiltInProviderId = 'ollama-cloud';
15
15
  const OTTOROUTER_ID: BuiltInProviderId = 'ottorouter';
16
+ const ZAI_CODING_ID: BuiltInProviderId = 'zai-coding';
17
+
18
+ const ZAI_CODING_MODEL_ORDER = [
19
+ 'glm-5.2',
20
+ 'glm-5.1',
21
+ 'glm-5-turbo',
22
+ 'glm-5',
23
+ 'glm-4.7',
24
+ 'glm-4.5-air',
25
+ 'glm-5v-turbo',
26
+ ];
27
+
28
+ const ZAI_CODING_MANUAL_MODELS: ModelInfo[] = [
29
+ {
30
+ id: 'glm-5',
31
+ ownedBy: 'zai',
32
+ label: 'GLM-5',
33
+ modalities: { input: ['text'], output: ['text'] },
34
+ toolCall: true,
35
+ reasoningText: true,
36
+ attachment: false,
37
+ temperature: true,
38
+ releaseDate: '2026-02-11',
39
+ lastUpdated: '2026-02-11',
40
+ openWeights: true,
41
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
42
+ limit: { context: 204_800, output: 131_072 },
43
+ },
44
+ ];
16
45
 
17
46
  const XAI_GROK_CLI_MODELS: ModelInfo[] = [
18
47
  {
@@ -214,6 +243,33 @@ export function applyOfficialKimiCatalogMetadata<
214
243
  };
215
244
  }
216
245
 
246
+ export function applyZaiCodingCatalogMetadata<T extends ProviderCatalogEntry>(
247
+ entry: T | undefined,
248
+ ): T | undefined {
249
+ if (!entry) return undefined;
250
+ const order = new Map(
251
+ ZAI_CODING_MODEL_ORDER.map((modelId, index) => [modelId, index]),
252
+ );
253
+ const modelById = new Map(entry.models.map((model) => [model.id, model]));
254
+ for (const model of ZAI_CODING_MANUAL_MODELS) {
255
+ if (!modelById.has(model.id)) modelById.set(model.id, model);
256
+ }
257
+ const models = Array.from(modelById.values()).sort((a, b) => {
258
+ const orderA = order.get(a.id) ?? Number.MAX_SAFE_INTEGER;
259
+ const orderB = order.get(b.id) ?? Number.MAX_SAFE_INTEGER;
260
+ if (orderA !== orderB) return orderA - orderB;
261
+ return a.id.localeCompare(b.id);
262
+ });
263
+ return {
264
+ ...entry,
265
+ models,
266
+ label: 'Z.AI Coding Plan',
267
+ env: ['ZAI_CODING_API_KEY'],
268
+ api: 'https://api.z.ai/api/coding/paas/v4',
269
+ doc: 'https://docs.z.ai/devpack/overview',
270
+ };
271
+ }
272
+
217
273
  export function mergeManualCatalog(
218
274
  base: CatalogMap,
219
275
  ): Record<BuiltInProviderId, ProviderCatalogEntry> {
@@ -231,6 +287,10 @@ export function mergeManualCatalog(
231
287
  if (kimiEntry) {
232
288
  merged.kimi = kimiEntry;
233
289
  }
290
+ const zaiCodingEntry = applyZaiCodingCatalogMetadata(merged[ZAI_CODING_ID]);
291
+ if (zaiCodingEntry) {
292
+ merged[ZAI_CODING_ID] = zaiCodingEntry;
293
+ }
234
294
  if (manualEntry) {
235
295
  merged[OTTOROUTER_ID] = manualEntry;
236
296
  }
@@ -4,6 +4,7 @@ import { catalog } from './catalog-merged.ts';
4
4
  export type ZaiProviderConfig = {
5
5
  apiKey?: string;
6
6
  baseURL?: string;
7
+ fetch?: typeof fetch;
7
8
  };
8
9
 
9
10
  export function createZaiModel(model: string, config?: ZaiProviderConfig) {
@@ -21,6 +22,7 @@ export function createZaiModel(model: string, config?: ZaiProviderConfig) {
21
22
  name: entry?.label ?? 'Z.AI',
22
23
  baseURL,
23
24
  headers,
25
+ fetch: config?.fetch,
24
26
  });
25
27
 
26
28
  return instance(model);
@@ -44,6 +46,7 @@ export function createZaiCodingModel(
44
46
  name: entry?.label ?? 'Z.AI Coding',
45
47
  baseURL,
46
48
  headers,
49
+ fetch: config?.fetch,
47
50
  });
48
51
 
49
52
  return instance(model);