@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.
- package/lib/__templates__/expo/AGENTS.md +15 -7
- package/lib/__templates__/expo/README.md +15 -7
- package/lib/__templates__/expo/client/eslint.config.mjs +3 -0
- package/lib/__templates__/expo/eslint-plugins/expo/index.js +9 -0
- package/lib/__templates__/expo/eslint-plugins/expo/rule.js +105 -0
- package/lib/__templates__/expo/eslint-plugins/expo/tech.md +108 -0
- package/lib/__templates__/nextjs/AGENTS.md +9 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +15 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +150 -0
- package/lib/__templates__/pi-agent/README.md +155 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +273 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
- package/lib/__templates__/pi-agent/package.json +52 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +7840 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +2 -0
- package/lib/__templates__/pi-agent/src/agent.ts +367 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
- package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
- package/lib/__templates__/pi-agent/src/config.ts +596 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +148 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +141 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
- package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
- package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
- package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
- package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
- package/lib/__templates__/pi-agent/src/index.ts +123 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +292 -0
- package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
- package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
- package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
- package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
- package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
- package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
- package/lib/__templates__/pi-agent/tsconfig.json +20 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/pnpm-lock.yaml +24 -14
- package/lib/__templates__/taro/server/package.json +0 -2
- package/lib/__templates__/taro/src/presets/dev-debug.ts +2 -2
- package/lib/__templates__/templates.json +24 -0
- package/lib/__templates__/vite/AGENTS.md +5 -0
- package/lib/cli.js +1 -1
- 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
|
+
}
|