@coze-arch/cli 0.0.17 → 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.
- package/lib/__templates__/expo/.coze +1 -0
- package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/expo/package.json +2 -1
- package/lib/__templates__/nextjs/.coze +1 -0
- package/lib/__templates__/nextjs/package.json +3 -1
- package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
- package/lib/__templates__/nuxt-vue/.coze +1 -0
- package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
- package/lib/__templates__/nuxt-vue/package.json +9 -2
- package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
- package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +144 -0
- package/lib/__templates__/pi-agent/README.md +216 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/_npmrc +23 -0
- package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +374 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
- package/lib/__templates__/pi-agent/package.json +63 -0
- package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
- package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
- package/lib/__templates__/pi-agent/src/agent.ts +363 -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/cli.ts +117 -0
- package/lib/__templates__/pi-agent/src/config.ts +708 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -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/pi-resources.ts +125 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
- package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
- package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
- package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +315 -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/pi-resources.test.ts +73 -0
- package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -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/tests/web-fetch.test.ts +157 -0
- package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
- package/lib/__templates__/pi-agent/tsconfig.json +21 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/.coze +1 -0
- package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/taro/package.json +1 -1
- package/lib/__templates__/templates.json +18 -31
- package/lib/__templates__/vite/.coze +1 -0
- package/lib/__templates__/vite/package.json +3 -1
- package/lib/__templates__/vite/scripts/validate.sh +10 -0
- package/lib/cli.js +13 -2
- 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
|
+
}
|