@coze-arch/cli 0.0.18 → 0.0.19-alpha.502ddf

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.
Files changed (97) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/app/pages/index.vue +6 -0
  9. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  10. package/lib/__templates__/nuxt-vue/nuxt.config.ts +2 -2
  11. package/lib/__templates__/nuxt-vue/package.json +9 -2
  12. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  13. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  14. package/lib/__templates__/pi-agent/.coze +10 -0
  15. package/lib/__templates__/pi-agent/AGENTS.md +149 -0
  16. package/lib/__templates__/pi-agent/README.md +218 -0
  17. package/lib/__templates__/pi-agent/_gitignore +3 -0
  18. package/lib/__templates__/pi-agent/_npmrc +23 -0
  19. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  20. package/lib/__templates__/pi-agent/docs/project-overview.md +368 -0
  21. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  22. package/lib/__templates__/pi-agent/package.json +63 -0
  23. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  24. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +30 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +29 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +57 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +40 -0
  29. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  30. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  31. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  32. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  33. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  34. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  35. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  36. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  37. package/lib/__templates__/pi-agent/src/config.ts +749 -0
  38. package/lib/__templates__/pi-agent/src/core.ts +219 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/index.ts +74 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/server.ts +610 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +172 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +24 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +440 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +330 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +203 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  68. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  69. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  70. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  71. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  72. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  73. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  74. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  75. package/lib/__templates__/pi-agent/template.config.js +45 -0
  76. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  77. package/lib/__templates__/pi-agent/tests/config.test.ts +377 -0
  78. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  79. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  80. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  81. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  82. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  83. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  84. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  85. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  86. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  87. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  88. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  89. package/lib/__templates__/taro/.coze +1 -0
  90. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  91. package/lib/__templates__/taro/package.json +1 -1
  92. package/lib/__templates__/templates.json +24 -0
  93. package/lib/__templates__/vite/.coze +1 -0
  94. package/lib/__templates__/vite/package.json +3 -1
  95. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  96. package/lib/cli.js +13 -2
  97. package/package.json +1 -1
@@ -0,0 +1,749 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getModel, type Api, type Model } from "@mariozechner/pi-ai";
6
+ import type { BotAppConfig, ThinkingLevel } from "./core.js";
7
+
8
+ type ModelInput = "text" | "image";
9
+
10
+ interface ModelCost {
11
+ input: number;
12
+ output: number;
13
+ cacheRead: number;
14
+ cacheWrite: number;
15
+ }
16
+
17
+ interface ModelOverride {
18
+ name?: string;
19
+ reasoning?: boolean;
20
+ input?: ModelInput[];
21
+ cost?: Partial<ModelCost>;
22
+ contextWindow?: number;
23
+ maxTokens?: number;
24
+ headers?: Record<string, string>;
25
+ compat?: Model<Api>["compat"];
26
+ }
27
+
28
+ interface ProviderModelConfig {
29
+ id: string;
30
+ name?: string;
31
+ api?: Api;
32
+ baseUrl?: string;
33
+ reasoning?: boolean;
34
+ input?: ModelInput[];
35
+ cost?: ModelCost;
36
+ contextWindow?: number;
37
+ maxTokens?: number;
38
+ headers?: Record<string, string>;
39
+ compat?: Model<Api>["compat"];
40
+ }
41
+
42
+ export interface ProviderConfig {
43
+ api?: Api;
44
+ apiKey?: string;
45
+ baseUrl?: string;
46
+ headers?: Record<string, string>;
47
+ compat?: Model<Api>["compat"];
48
+ authHeader?: boolean;
49
+ modelOverrides?: Record<string, ModelOverride>;
50
+ models?: ProviderModelConfig[];
51
+ }
52
+
53
+ export interface Config {
54
+ agents?: {
55
+ defaults?: {
56
+ model?: {
57
+ primary?: string;
58
+ };
59
+ thinkingLevel?: ThinkingLevel;
60
+ workspace?: string;
61
+ };
62
+ };
63
+ models?: {
64
+ providers?: Record<string, ProviderConfig>;
65
+ };
66
+ channels?: {
67
+ feishu?: {
68
+ enabled?: boolean;
69
+ appId?: string;
70
+ appSecret?: string;
71
+ domain?: string;
72
+ encryptKey?: string;
73
+ verificationToken?: string;
74
+ requireMention?: boolean;
75
+ thinkingReaction?: {
76
+ enabled?: boolean;
77
+ emojiType?: string;
78
+ };
79
+ };
80
+ wechat?: {
81
+ enabled?: boolean;
82
+ requireMention?: boolean;
83
+ };
84
+ };
85
+ }
86
+
87
+ export interface ModelReference {
88
+ provider: string;
89
+ modelId: string;
90
+ }
91
+
92
+ export interface LoadedConfig {
93
+ path: string;
94
+ directory: string;
95
+ workspaceDir: string;
96
+ source: "file" | "fallback-mock";
97
+ thinkingLevel?: ThinkingLevel;
98
+ config: Config;
99
+ providers: Record<string, ProviderConfig>;
100
+ defaultModel?: ModelReference;
101
+ }
102
+
103
+ export interface ResolvedRuntimeModel {
104
+ model: Model<Api>;
105
+ apiKey?: string;
106
+ }
107
+
108
+ export interface ResolveRuntimeModelOptions {
109
+ provider?: string;
110
+ model?: string;
111
+ baseUrl?: string;
112
+ configPath?: string;
113
+ env?: NodeJS.ProcessEnv;
114
+ }
115
+
116
+ export interface LoadBotAppConfigOptions {
117
+ configPath?: string;
118
+ env?: NodeJS.ProcessEnv;
119
+ }
120
+
121
+ const DEFAULT_COST: ModelCost = {
122
+ input: 0,
123
+ output: 0,
124
+ cacheRead: 0,
125
+ cacheWrite: 0
126
+ };
127
+ const DEFAULT_APP_NAME = "starter-bot";
128
+ const DEFAULT_CONFIG_FILENAME = "config.json";
129
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
130
+
131
+ function createMockConfig(): Config {
132
+ return {
133
+ agents: {
134
+ defaults: {
135
+ workspace: ".",
136
+ model: {
137
+ primary: "openai/gpt-5-mini"
138
+ }
139
+ }
140
+ },
141
+ channels: {
142
+ feishu: {
143
+ enabled: false,
144
+ requireMention: true,
145
+ thinkingReaction: {
146
+ enabled: true,
147
+ emojiType: "OneSecond"
148
+ }
149
+ },
150
+ wechat: {
151
+ enabled: false,
152
+ requireMention: false
153
+ }
154
+ }
155
+ };
156
+ }
157
+
158
+ function createLoadedConfig(
159
+ configPath: string,
160
+ parsed: Config,
161
+ source: LoadedConfig["source"]
162
+ ): LoadedConfig {
163
+ const directory = dirname(configPath);
164
+ const workspaceDir = resolve(directory, parsed.agents?.defaults?.workspace ?? ".");
165
+ const defaultModelReference = parsed.agents?.defaults?.model?.primary;
166
+
167
+ return {
168
+ path: configPath,
169
+ directory,
170
+ workspaceDir,
171
+ source,
172
+ thinkingLevel: parsed.agents?.defaults?.thinkingLevel,
173
+ config: parsed,
174
+ providers: parsed.models?.providers ?? {},
175
+ defaultModel: defaultModelReference
176
+ ? parseModelReference(defaultModelReference, "agents.defaults.model.primary")
177
+ : undefined
178
+ };
179
+ }
180
+
181
+ export function parseModelReference(reference: string, description = "model reference"): ModelReference {
182
+ const normalized = reference.trim();
183
+ const splitIndex = normalized.indexOf("/");
184
+
185
+ if (splitIndex <= 0 || splitIndex === normalized.length - 1) {
186
+ throw new Error(
187
+ `${description} must use "provider/model-id" format, received "${reference}".`
188
+ );
189
+ }
190
+
191
+ return {
192
+ provider: normalized.slice(0, splitIndex),
193
+ modelId: normalized.slice(splitIndex + 1)
194
+ };
195
+ }
196
+
197
+ export function loadConfig(configPath: string): LoadedConfig {
198
+ if (!existsSync(configPath)) {
199
+ console.warn(`[pi-bot] Config not found: ${configPath}. Falling back to mock config.`);
200
+ return createLoadedConfig(configPath, createMockConfig(), "fallback-mock");
201
+ }
202
+
203
+ let parsed: Config;
204
+
205
+ try {
206
+ parsed = JSON.parse(readFileSync(configPath, "utf-8")) as Config;
207
+ } catch (error) {
208
+ throw new Error(
209
+ `Failed to read config at ${configPath}: ${
210
+ error instanceof Error ? error.message : String(error)
211
+ }`
212
+ );
213
+ }
214
+
215
+ return createLoadedConfig(configPath, parsed, "file");
216
+ }
217
+
218
+ export function loadOptionalConfig(configPath: string | undefined): LoadedConfig | undefined {
219
+ if (!configPath) {
220
+ return undefined;
221
+ }
222
+
223
+ return loadConfig(configPath);
224
+ }
225
+
226
+ export function resolveRuntimeModel(
227
+ options: ResolveRuntimeModelOptions
228
+ ): ResolvedRuntimeModel | undefined {
229
+ const loadedConfig = loadOptionalConfig(options.configPath);
230
+ const selection = resolveModelSelection(options, loadedConfig);
231
+
232
+ if (!selection) {
233
+ return undefined;
234
+ }
235
+
236
+ const providerConfig = loadedConfig?.providers[selection.provider];
237
+
238
+ if (providerConfig) {
239
+ return createConfiguredProviderModel({
240
+ provider: selection.provider,
241
+ modelId: selection.modelId,
242
+ providerConfig,
243
+ explicitBaseUrl: options.baseUrl,
244
+ configPath: loadedConfig.path,
245
+ env: options.env
246
+ });
247
+ }
248
+
249
+ const builtInModel = getModel(selection.provider as never, selection.modelId);
250
+ if (!builtInModel) {
251
+ throw new Error(
252
+ `Unknown provider/model "${selection.provider}/${selection.modelId}". ` +
253
+ `It was not found in config.json or built-in pi-ai models.`
254
+ );
255
+ }
256
+
257
+ return {
258
+ model: applyBaseUrlOverride(cloneModel(builtInModel), options.baseUrl)
259
+ };
260
+ }
261
+
262
+ function resolveModelSelection(
263
+ options: ResolveRuntimeModelOptions,
264
+ loadedConfig: LoadedConfig | undefined
265
+ ): ModelReference | undefined {
266
+ if (options.provider && options.model) {
267
+ return {
268
+ provider: options.provider,
269
+ modelId: options.model
270
+ };
271
+ }
272
+
273
+ if (!options.provider && options.model?.includes("/")) {
274
+ return parseModelReference(options.model, "agent.model");
275
+ }
276
+
277
+ return loadedConfig?.defaultModel;
278
+ }
279
+
280
+ export function getProjectRoot(): string {
281
+ return resolve(MODULE_DIR, "..");
282
+ }
283
+
284
+ export function getDefaultConfigPath(): string {
285
+ const projectRoot = getProjectRoot();
286
+ return resolve(projectRoot, "<%= workspaceDir %>", DEFAULT_CONFIG_FILENAME);
287
+ }
288
+
289
+ export function loadBotAppConfig(
290
+ options: LoadBotAppConfigOptions = {}
291
+ ): BotAppConfig {
292
+ const env = options.env ?? process.env;
293
+ const configPath = options.configPath ?? getDefaultConfigPath();
294
+ const loadedConfig = loadConfig(configPath);
295
+ const workspaceDir = loadedConfig.workspaceDir;
296
+ const defaultModel = loadedConfig.defaultModel;
297
+ const feishuCfg = loadedConfig.config.channels?.feishu;
298
+ const wechatCfg = loadedConfig.config.channels?.wechat;
299
+
300
+ return {
301
+ appName: DEFAULT_APP_NAME,
302
+ configRoot: loadedConfig.config as Record<string, unknown>,
303
+ agent: {
304
+ mode:
305
+ (env.PI_BOT_AGENT_MODE as BotAppConfig["agent"]["mode"] | undefined) ??
306
+ (loadedConfig.source === "fallback-mock" ? "mock" : "pi"),
307
+ provider: defaultModel?.provider ?? "openai",
308
+ model: defaultModel?.modelId ?? "gpt-5-mini",
309
+ configPath: loadedConfig.source === "file" ? configPath : undefined,
310
+ thinkingLevel: loadedConfig.thinkingLevel,
311
+ cwd: workspaceDir,
312
+ agentDir: workspaceDir
313
+ },
314
+ routing: {
315
+ feishuGroupRequireMention: feishuCfg?.requireMention ?? true,
316
+ wechatGroupRequireMention: wechatCfg?.requireMention ?? false
317
+ },
318
+ channels: {
319
+ feishu: {
320
+ enabled: feishuCfg?.enabled ?? false,
321
+ appId: resolveOptionalRuntimeValue(feishuCfg?.appId, "channels.feishu.appId", env),
322
+ appSecret: resolveOptionalRuntimeValue(feishuCfg?.appSecret, "channels.feishu.appSecret", env),
323
+ domain: resolveOptionalRuntimeValue(feishuCfg?.domain, "channels.feishu.domain", env),
324
+ encryptKey: resolveOptionalRuntimeValue(feishuCfg?.encryptKey, "channels.feishu.encryptKey", env),
325
+ verificationToken: resolveOptionalRuntimeValue(feishuCfg?.verificationToken, "channels.feishu.verificationToken", env),
326
+ thinkingReaction: {
327
+ enabled: feishuCfg?.thinkingReaction?.enabled ?? true,
328
+ emojiType: feishuCfg?.thinkingReaction?.emojiType
329
+ }
330
+ },
331
+ wechat: {
332
+ enabled: wechatCfg?.enabled ?? false
333
+ }
334
+ }
335
+ };
336
+ }
337
+
338
+ function createConfiguredProviderModel(options: {
339
+ provider: string;
340
+ modelId: string;
341
+ providerConfig: ProviderConfig;
342
+ explicitBaseUrl?: string;
343
+ configPath: string;
344
+ env?: NodeJS.ProcessEnv;
345
+ }): ResolvedRuntimeModel {
346
+ const explicitBaseUrl = options.explicitBaseUrl
347
+ ? resolveRuntimeValue(
348
+ options.explicitBaseUrl,
349
+ `${options.provider}/${options.modelId} baseUrl override`,
350
+ options.env
351
+ )
352
+ : undefined;
353
+ const apiKey = options.providerConfig.apiKey
354
+ ? resolveRuntimeValue(
355
+ options.providerConfig.apiKey,
356
+ `${options.provider} apiKey in ${options.configPath}`,
357
+ options.env
358
+ )
359
+ : undefined;
360
+ const providerHeaders = resolveHeaders(
361
+ options.providerConfig.headers,
362
+ `${options.provider} headers in ${options.configPath}`,
363
+ options.env
364
+ );
365
+ const override = options.providerConfig.modelOverrides?.[options.modelId];
366
+ const customModel = options.providerConfig.models?.find((model) => model.id === options.modelId);
367
+
368
+ let model: Model<Api>;
369
+
370
+ if (customModel) {
371
+ const api = customModel.api ?? options.providerConfig.api;
372
+ const baseUrl =
373
+ explicitBaseUrl ??
374
+ resolveOptionalRuntimeValue(
375
+ customModel.baseUrl ?? options.providerConfig.baseUrl,
376
+ `${options.provider}/${options.modelId} baseUrl in ${options.configPath}`,
377
+ options.env
378
+ );
379
+
380
+ if (!api) {
381
+ throw new Error(
382
+ `Provider "${options.provider}" model "${options.modelId}" is missing an api field in ${options.configPath}.`
383
+ );
384
+ }
385
+
386
+ if (!baseUrl) {
387
+ throw new Error(
388
+ `Provider "${options.provider}" model "${options.modelId}" is missing a baseUrl in ${options.configPath}.`
389
+ );
390
+ }
391
+
392
+ model = {
393
+ id: customModel.id,
394
+ name: customModel.name ?? customModel.id,
395
+ api,
396
+ provider: options.provider,
397
+ baseUrl,
398
+ reasoning: customModel.reasoning ?? false,
399
+ input: [...(customModel.input ?? ["text"])],
400
+ cost: { ...(customModel.cost ?? DEFAULT_COST) },
401
+ contextWindow: customModel.contextWindow ?? 128000,
402
+ maxTokens: customModel.maxTokens ?? 16384,
403
+ headers: mergeHeaders(
404
+ providerHeaders,
405
+ resolveHeaders(
406
+ customModel.headers,
407
+ `${options.provider}/${options.modelId} headers in ${options.configPath}`,
408
+ options.env
409
+ )
410
+ ),
411
+ compat: mergeCompat(options.providerConfig.compat, customModel.compat)
412
+ };
413
+ } else {
414
+ const builtInModel = getModel(options.provider as never, options.modelId);
415
+ if (!builtInModel) {
416
+ const availableModels = new Set<string>();
417
+
418
+ for (const configuredModel of options.providerConfig.models ?? []) {
419
+ availableModels.add(configuredModel.id);
420
+ }
421
+
422
+ throw new Error(
423
+ `Unknown model "${options.provider}/${options.modelId}" in ${options.configPath}. ` +
424
+ `Configured models: ${Array.from(availableModels).sort().join(", ") || "(none)"}`
425
+ );
426
+ }
427
+
428
+ model = cloneModel(builtInModel);
429
+ model.baseUrl =
430
+ explicitBaseUrl ??
431
+ resolveOptionalRuntimeValue(
432
+ options.providerConfig.baseUrl,
433
+ `${options.provider} baseUrl in ${options.configPath}`,
434
+ options.env
435
+ ) ??
436
+ model.baseUrl;
437
+ model.headers = mergeHeaders(model.headers, providerHeaders);
438
+ model.compat = mergeCompat(model.compat, options.providerConfig.compat);
439
+ }
440
+
441
+ model = applyModelOverride(
442
+ model,
443
+ override,
444
+ `${options.provider}/${options.modelId} override in ${options.configPath}`,
445
+ options.env
446
+ );
447
+
448
+ if (options.providerConfig.authHeader && apiKey) {
449
+ model.headers = mergeHeaders(model.headers, {
450
+ Authorization: `Bearer ${apiKey}`
451
+ });
452
+ return { model };
453
+ }
454
+
455
+ return { model, apiKey };
456
+ }
457
+
458
+ function cloneModel(model: Model<Api>): Model<Api> {
459
+ return {
460
+ ...model,
461
+ input: [...model.input],
462
+ cost: { ...model.cost },
463
+ headers: model.headers ? { ...model.headers } : undefined,
464
+ compat: model.compat ? mergeCompat(model.compat) : undefined
465
+ };
466
+ }
467
+
468
+ function applyBaseUrlOverride(model: Model<Api>, baseUrl: string | undefined): Model<Api> {
469
+ if (!baseUrl) {
470
+ return model;
471
+ }
472
+
473
+ return {
474
+ ...model,
475
+ baseUrl
476
+ };
477
+ }
478
+
479
+ function applyModelOverride(
480
+ model: Model<Api>,
481
+ override: ModelOverride | undefined,
482
+ description: string,
483
+ env: NodeJS.ProcessEnv = process.env
484
+ ): Model<Api> {
485
+ if (!override) {
486
+ return model;
487
+ }
488
+
489
+ const nextModel = cloneModel(model);
490
+
491
+ if (override.name !== undefined) nextModel.name = override.name;
492
+ if (override.reasoning !== undefined) nextModel.reasoning = override.reasoning;
493
+ if (override.input !== undefined) nextModel.input = [...override.input];
494
+ if (override.contextWindow !== undefined) nextModel.contextWindow = override.contextWindow;
495
+ if (override.maxTokens !== undefined) nextModel.maxTokens = override.maxTokens;
496
+
497
+ if (override.cost) {
498
+ nextModel.cost = {
499
+ input: override.cost.input ?? nextModel.cost.input,
500
+ output: override.cost.output ?? nextModel.cost.output,
501
+ cacheRead: override.cost.cacheRead ?? nextModel.cost.cacheRead,
502
+ cacheWrite: override.cost.cacheWrite ?? nextModel.cost.cacheWrite
503
+ };
504
+ }
505
+
506
+ nextModel.headers = mergeHeaders(
507
+ nextModel.headers,
508
+ resolveHeaders(override.headers, `${description} headers`, env)
509
+ );
510
+ nextModel.compat = mergeCompat(nextModel.compat, override.compat);
511
+
512
+ return nextModel;
513
+ }
514
+
515
+ function mergeCompat(
516
+ base?: Model<Api>["compat"],
517
+ override?: Model<Api>["compat"]
518
+ ): Model<Api>["compat"] | undefined {
519
+ if (!base && !override) {
520
+ return undefined;
521
+ }
522
+
523
+ const merged = {
524
+ ...(base ?? {}),
525
+ ...(override ?? {})
526
+ } as Record<string, unknown>;
527
+
528
+ const baseRecord = (base ?? {}) as Record<string, unknown>;
529
+ const overrideRecord = (override ?? {}) as Record<string, unknown>;
530
+
531
+ if (isPlainObject(baseRecord.openRouterRouting) || isPlainObject(overrideRecord.openRouterRouting)) {
532
+ merged.openRouterRouting = {
533
+ ...(isPlainObject(baseRecord.openRouterRouting) ? baseRecord.openRouterRouting : {}),
534
+ ...(isPlainObject(overrideRecord.openRouterRouting) ? overrideRecord.openRouterRouting : {})
535
+ };
536
+ }
537
+
538
+ if (isPlainObject(baseRecord.vercelGatewayRouting) || isPlainObject(overrideRecord.vercelGatewayRouting)) {
539
+ merged.vercelGatewayRouting = {
540
+ ...(isPlainObject(baseRecord.vercelGatewayRouting) ? baseRecord.vercelGatewayRouting : {}),
541
+ ...(isPlainObject(overrideRecord.vercelGatewayRouting)
542
+ ? overrideRecord.vercelGatewayRouting
543
+ : {})
544
+ };
545
+ }
546
+
547
+ if (isPlainObject(baseRecord.reasoningEffortMap) || isPlainObject(overrideRecord.reasoningEffortMap)) {
548
+ merged.reasoningEffortMap = {
549
+ ...(isPlainObject(baseRecord.reasoningEffortMap) ? baseRecord.reasoningEffortMap : {}),
550
+ ...(isPlainObject(overrideRecord.reasoningEffortMap) ? overrideRecord.reasoningEffortMap : {})
551
+ };
552
+ }
553
+
554
+ return merged as Model<Api>["compat"];
555
+ }
556
+
557
+ function mergeHeaders(
558
+ ...headersList: Array<Record<string, string> | undefined>
559
+ ): Record<string, string> | undefined {
560
+ const merged = headersList.reduce<Record<string, string>>((acc, headers) => {
561
+ if (!headers) {
562
+ return acc;
563
+ }
564
+
565
+ return { ...acc, ...headers };
566
+ }, {});
567
+
568
+ return Object.keys(merged).length > 0 ? merged : undefined;
569
+ }
570
+
571
+ function resolveHeaders(
572
+ headers: Record<string, string> | undefined,
573
+ description: string,
574
+ env: NodeJS.ProcessEnv = process.env
575
+ ): Record<string, string> | undefined {
576
+ if (!headers) {
577
+ return undefined;
578
+ }
579
+
580
+ const resolvedHeaders = Object.fromEntries(
581
+ Object.entries(headers).map(([key, value]) => [
582
+ key,
583
+ resolveRuntimeValue(value, `${description} header "${key}"`, env)
584
+ ])
585
+ );
586
+
587
+ return Object.keys(resolvedHeaders).length > 0 ? resolvedHeaders : undefined;
588
+ }
589
+
590
+ function resolveOptionalRuntimeValue(
591
+ value: string | undefined,
592
+ description: string,
593
+ env: NodeJS.ProcessEnv = process.env
594
+ ): string | undefined {
595
+ return value ? resolveRuntimeValue(value, description, env) : undefined;
596
+ }
597
+
598
+ function resolveRuntimeValue(
599
+ value: string,
600
+ description: string,
601
+ env: NodeJS.ProcessEnv = process.env
602
+ ): string {
603
+ if (value.startsWith("!")) {
604
+ try {
605
+ const output = execSync(value.slice(1), {
606
+ encoding: "utf-8",
607
+ stdio: ["ignore", "pipe", "ignore"]
608
+ }).trim();
609
+
610
+ if (!output) {
611
+ throw new Error("command returned empty output");
612
+ }
613
+
614
+ return output;
615
+ } catch (error) {
616
+ throw new Error(
617
+ `Failed to resolve ${description} from command "${value.slice(1)}": ${
618
+ error instanceof Error ? error.message : String(error)
619
+ }`
620
+ );
621
+ }
622
+ }
623
+
624
+ return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_, variableName: string) => {
625
+ const resolved = env[variableName];
626
+
627
+ if (!resolved) {
628
+ throw new Error(`Missing environment variable ${variableName} while resolving ${description}.`);
629
+ }
630
+
631
+ return resolved;
632
+ });
633
+ }
634
+
635
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
636
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
637
+ }
638
+
639
+ export function ensureObject(value: unknown): Record<string, unknown> {
640
+ if (value && typeof value === "object" && !Array.isArray(value)) {
641
+ return value as Record<string, unknown>;
642
+ }
643
+ return {};
644
+ }
645
+
646
+ export function readString(value: unknown): string {
647
+ return typeof value === "string" ? value : "";
648
+ }
649
+
650
+ function isArrayIndex(segment: string): boolean {
651
+ return /^\d+$/.test(segment);
652
+ }
653
+
654
+ export function getNested(root: Record<string, unknown>, path: string[]): unknown {
655
+ let current: unknown = root;
656
+ for (const segment of path) {
657
+ if (current === null || current === undefined) return undefined;
658
+ if (Array.isArray(current)) {
659
+ if (!isArrayIndex(segment)) return undefined;
660
+ current = current[Number(segment)];
661
+ } else if (typeof current === "object") {
662
+ current = (current as Record<string, unknown>)[segment];
663
+ } else {
664
+ return undefined;
665
+ }
666
+ }
667
+ return current;
668
+ }
669
+
670
+ export function setNested(root: Record<string, unknown>, path: string[], value: unknown): void {
671
+ let current: unknown = root;
672
+ for (let i = 0; i < path.length - 1; i++) {
673
+ const segment = path[i]!;
674
+ const nextSegment = path[i + 1]!;
675
+ if (Array.isArray(current)) {
676
+ if (!isArrayIndex(segment)) {
677
+ throw new Error(`Invalid array index "${segment}" in path "${path.join(".")}"`);
678
+ }
679
+ const index = Number(segment);
680
+ if (current[index] === undefined || current[index] === null || typeof current[index] !== "object") {
681
+ current[index] = isArrayIndex(nextSegment) ? [] : {};
682
+ }
683
+ current = current[index];
684
+ } else if (current && typeof current === "object") {
685
+ const obj = current as Record<string, unknown>;
686
+ if (obj[segment] === undefined || obj[segment] === null || typeof obj[segment] !== "object") {
687
+ obj[segment] = isArrayIndex(nextSegment) ? [] : {};
688
+ }
689
+ current = obj[segment];
690
+ }
691
+ }
692
+
693
+ const lastSegment = path[path.length - 1]!;
694
+ if (Array.isArray(current)) {
695
+ if (!isArrayIndex(lastSegment)) {
696
+ throw new Error(`Invalid array index "${lastSegment}" in path "${path.join(".")}"`);
697
+ }
698
+ current[Number(lastSegment)] = value;
699
+ } else if (current && typeof current === "object") {
700
+ (current as Record<string, unknown>)[lastSegment] = value;
701
+ }
702
+ }
703
+
704
+ export function deleteNested(root: Record<string, unknown>, path: string[]): boolean {
705
+ let current: unknown = root;
706
+ for (let i = 0; i < path.length - 1; i++) {
707
+ const segment = path[i]!;
708
+ if (Array.isArray(current)) {
709
+ if (!isArrayIndex(segment)) return false;
710
+ current = current[Number(segment)];
711
+ } else if (current && typeof current === "object") {
712
+ current = (current as Record<string, unknown>)[segment];
713
+ } else {
714
+ return false;
715
+ }
716
+ if (current === null || current === undefined) return false;
717
+ }
718
+
719
+ const lastSegment = path[path.length - 1]!;
720
+ if (Array.isArray(current)) {
721
+ if (!isArrayIndex(lastSegment)) return false;
722
+ const index = Number(lastSegment);
723
+ if (index >= current.length) return false;
724
+ current.splice(index, 1);
725
+ return true;
726
+ }
727
+ if (current && typeof current === "object") {
728
+ const obj = current as Record<string, unknown>;
729
+ if (lastSegment in obj) {
730
+ delete obj[lastSegment];
731
+ return true;
732
+ }
733
+ }
734
+ return false;
735
+ }
736
+
737
+ export function updateString(root: Record<string, unknown>, path: string[], value: unknown): void {
738
+ if (value === undefined) return;
739
+ if (typeof value === "string" && value.trim() === "") {
740
+ deleteNested(root, path);
741
+ return;
742
+ }
743
+ setNested(root, path, value);
744
+ }
745
+
746
+ export function updateBool(root: Record<string, unknown>, path: string[], value: unknown): void {
747
+ if (value === undefined) return;
748
+ setNested(root, path, Boolean(value));
749
+ }