@coze-arch/cli 0.0.18 → 0.0.19-beta.1

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