@coze-arch/cli 0.0.13 → 0.0.14-alpha.c52ee4

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 (76) hide show
  1. package/lib/__templates__/expo/AGENTS.md +15 -7
  2. package/lib/__templates__/expo/README.md +15 -7
  3. package/lib/__templates__/expo/client/eslint.config.mjs +3 -0
  4. package/lib/__templates__/expo/eslint-plugins/expo/index.js +9 -0
  5. package/lib/__templates__/expo/eslint-plugins/expo/rule.js +105 -0
  6. package/lib/__templates__/expo/eslint-plugins/expo/tech.md +108 -0
  7. package/lib/__templates__/nextjs/AGENTS.md +9 -0
  8. package/lib/__templates__/nextjs/eslint.config.mjs +15 -0
  9. package/lib/__templates__/pi-agent/.coze +10 -0
  10. package/lib/__templates__/pi-agent/AGENTS.md +150 -0
  11. package/lib/__templates__/pi-agent/README.md +155 -0
  12. package/lib/__templates__/pi-agent/_gitignore +3 -0
  13. package/lib/__templates__/pi-agent/docs/project-overview.md +273 -0
  14. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  15. package/lib/__templates__/pi-agent/package.json +52 -0
  16. package/lib/__templates__/pi-agent/pnpm-lock.yaml +7840 -0
  17. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  18. package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
  19. package/lib/__templates__/pi-agent/src/agent.ts +367 -0
  20. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  21. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  22. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  23. package/lib/__templates__/pi-agent/src/config.ts +596 -0
  24. package/lib/__templates__/pi-agent/src/core.ts +218 -0
  25. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
  26. package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
  27. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
  28. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  29. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  30. package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
  31. package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
  32. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  33. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  34. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  35. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
  36. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  37. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  38. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  58. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  59. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  60. package/lib/__templates__/pi-agent/template.config.js +45 -0
  61. package/lib/__templates__/pi-agent/tests/config.test.ts +292 -0
  62. package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
  63. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  64. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  65. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  66. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  67. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  68. package/lib/__templates__/pi-agent/tsconfig.json +20 -0
  69. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  70. package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
  71. package/lib/__templates__/taro/server/package.json +0 -2
  72. package/lib/__templates__/taro/src/presets/dev-debug.ts +2 -2
  73. package/lib/__templates__/templates.json +24 -0
  74. package/lib/__templates__/vite/AGENTS.md +5 -0
  75. package/lib/cli.js +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,596 @@
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) ?? "mock",
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
+ }