oh-pi 0.1.78 → 0.1.79
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/dist/index.js +6 -6
- package/dist/locales.js +66 -12
- package/dist/tui/confirm-apply.js +24 -2
- package/dist/tui/provider-setup.d.ts +12 -4
- package/dist/tui/provider-setup.js +156 -53
- package/dist/tui/provider-setup.test.js +15 -1
- package/dist/types.d.ts +3 -0
- package/dist/utils/writers.js +50 -13
- package/dist/utils/writers.test.js +108 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -38,9 +38,9 @@ export async function run() {
|
|
|
38
38
|
* @returns 生成的配置对象
|
|
39
39
|
*/
|
|
40
40
|
async function quickFlow(env) {
|
|
41
|
-
const
|
|
41
|
+
const providerSetup = await setupProviders(env);
|
|
42
42
|
return {
|
|
43
|
-
|
|
43
|
+
...providerSetup,
|
|
44
44
|
theme: "dark",
|
|
45
45
|
keybindings: "default",
|
|
46
46
|
extensions: ["safe-guard", "git-guard", "auto-session-name", "custom-footer", "compact-header", "auto-update"],
|
|
@@ -56,8 +56,8 @@ async function quickFlow(env) {
|
|
|
56
56
|
*/
|
|
57
57
|
async function presetFlow(env) {
|
|
58
58
|
const preset = await selectPreset();
|
|
59
|
-
const
|
|
60
|
-
return { ...preset,
|
|
59
|
+
const providerSetup = await setupProviders(env);
|
|
60
|
+
return { ...preset, ...providerSetup };
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* 自定义配置流程。用户逐项选择主题、快捷键、扩展、代理等,并可配置高级选项(如自动压缩阈值)。
|
|
@@ -65,7 +65,7 @@ async function presetFlow(env) {
|
|
|
65
65
|
* @returns 生成的配置对象
|
|
66
66
|
*/
|
|
67
67
|
async function customFlow(env) {
|
|
68
|
-
const
|
|
68
|
+
const providerSetup = await setupProviders(env);
|
|
69
69
|
const theme = await selectTheme();
|
|
70
70
|
const keybindings = await selectKeybindings();
|
|
71
71
|
const extensions = await selectExtensions();
|
|
@@ -80,7 +80,7 @@ async function customFlow(env) {
|
|
|
80
80
|
process.exit(0);
|
|
81
81
|
}
|
|
82
82
|
return {
|
|
83
|
-
|
|
83
|
+
...providerSetup,
|
|
84
84
|
theme,
|
|
85
85
|
keybindings,
|
|
86
86
|
extensions,
|
package/dist/locales.js
CHANGED
|
@@ -20,6 +20,10 @@ export const messages = {
|
|
|
20
20
|
"mode.customHint": "Pick everything yourself",
|
|
21
21
|
// provider
|
|
22
22
|
"provider.select": "Select API providers",
|
|
23
|
+
"provider.selectPrimary": "Select your primary API provider",
|
|
24
|
+
"provider.selectAdditional": "Select an additional provider",
|
|
25
|
+
"provider.addFallback": "Add a fallback provider? (advanced)",
|
|
26
|
+
"provider.selectFallback": "Select a fallback provider",
|
|
23
27
|
"provider.custom": "🔧 Custom endpoint",
|
|
24
28
|
"provider.customHint": "Ollama, vLLM, LiteLLM, any OpenAI-compatible",
|
|
25
29
|
"provider.foundEnv": "Found {env} in environment. Use it?",
|
|
@@ -46,6 +50,13 @@ export const messages = {
|
|
|
46
50
|
"provider.modelNamePlaceholder": "llama3.1:8b",
|
|
47
51
|
"provider.modelNameRequired": "Model name required",
|
|
48
52
|
"provider.customConfigured": "{name} configured ({url})",
|
|
53
|
+
"provider.apiMode": "OpenAI API mode for {label}:",
|
|
54
|
+
"provider.apiModeAuto": "Auto (Recommended)",
|
|
55
|
+
"provider.apiModeAutoHint": "Use Responses for reasoning-first models (selected: {model})",
|
|
56
|
+
"provider.apiModeResponses": "Responses API",
|
|
57
|
+
"provider.apiModeResponsesHint": "Modern OpenAI API surface",
|
|
58
|
+
"provider.apiModeCompletions": "Chat Completions API",
|
|
59
|
+
"provider.apiModeCompletionsHint": "Legacy-compatible endpoint",
|
|
49
60
|
"provider.configureCaps": "Configure model capabilities? (context window, multimodal, reasoning)",
|
|
50
61
|
"provider.contextWindow": "Context window size (tokens):",
|
|
51
62
|
"provider.contextWindowValidation": "Must be a number ≥ 1024",
|
|
@@ -54,10 +65,12 @@ export const messages = {
|
|
|
54
65
|
"provider.multimodal": "Supports image input (multimodal)?",
|
|
55
66
|
"provider.reasoning": "Supports extended thinking (reasoning)?",
|
|
56
67
|
"provider.detected": "Existing providers detected: {list}",
|
|
57
|
-
"provider.
|
|
58
|
-
"provider.
|
|
59
|
-
"provider.
|
|
60
|
-
"provider.
|
|
68
|
+
"provider.detectedKeep": "⏭ Keep existing provider config",
|
|
69
|
+
"provider.detectedKeepHint": "Do not modify auth/settings/models provider entries",
|
|
70
|
+
"provider.detectedReplace": "♻️ Replace provider config",
|
|
71
|
+
"provider.detectedReplaceHint": "Reconfigure primary provider and optional fallbacks",
|
|
72
|
+
"provider.detectedAdd": "➕ Keep existing and add fallback providers",
|
|
73
|
+
"provider.detectedAddHint": "Keep current primary provider and append additional providers",
|
|
61
74
|
// preset
|
|
62
75
|
"preset.select": "Choose a preset:",
|
|
63
76
|
"preset.full": "⚫ Full Power",
|
|
@@ -97,7 +110,12 @@ export const messages = {
|
|
|
97
110
|
// confirm
|
|
98
111
|
"confirm.title": "Configuration",
|
|
99
112
|
"confirm.providers": "Providers:",
|
|
113
|
+
"confirm.providerStrategy": "Provider strategy:",
|
|
114
|
+
"confirm.providerStrategyKeep": "keep existing",
|
|
115
|
+
"confirm.providerStrategyAdd": "keep existing and add fallback providers",
|
|
116
|
+
"confirm.providerStrategyReplace": "replace with new setup",
|
|
100
117
|
"confirm.model": "Model:",
|
|
118
|
+
"confirm.fallbackProviders": "Fallback providers:",
|
|
101
119
|
"confirm.theme": "Theme:",
|
|
102
120
|
"confirm.keybindings": "Keybindings:",
|
|
103
121
|
"confirm.thinking": "Thinking:",
|
|
@@ -151,6 +169,10 @@ export const messages = {
|
|
|
151
169
|
"mode.custom": "🎛️ 自定义",
|
|
152
170
|
"mode.customHint": "逐项自选",
|
|
153
171
|
"provider.select": "选择 API 提供商",
|
|
172
|
+
"provider.selectPrimary": "选择主 Provider",
|
|
173
|
+
"provider.selectAdditional": "选择新增 Provider",
|
|
174
|
+
"provider.addFallback": "是否添加备用 Provider?(高级)",
|
|
175
|
+
"provider.selectFallback": "选择备用 Provider",
|
|
154
176
|
"provider.custom": "🔧 自定义端点",
|
|
155
177
|
"provider.customHint": "Ollama、vLLM、LiteLLM 等 OpenAI 兼容接口",
|
|
156
178
|
"provider.foundEnv": "在环境变量中找到 {env},是否使用?",
|
|
@@ -177,6 +199,13 @@ export const messages = {
|
|
|
177
199
|
"provider.modelNamePlaceholder": "llama3.1:8b",
|
|
178
200
|
"provider.modelNameRequired": "模型名称不能为空",
|
|
179
201
|
"provider.customConfigured": "{name} 配置完成 ({url})",
|
|
202
|
+
"provider.apiMode": "{label} 的 OpenAI API 模式:",
|
|
203
|
+
"provider.apiModeAuto": "自动(推荐)",
|
|
204
|
+
"provider.apiModeAutoHint": "推理模型优先用 Responses(当前模型:{model})",
|
|
205
|
+
"provider.apiModeResponses": "Responses API",
|
|
206
|
+
"provider.apiModeResponsesHint": "OpenAI 新版 API",
|
|
207
|
+
"provider.apiModeCompletions": "Chat Completions API",
|
|
208
|
+
"provider.apiModeCompletionsHint": "兼容旧接口",
|
|
180
209
|
"provider.configureCaps": "配置模型能力?(上下文窗口、多模态、推理)",
|
|
181
210
|
"provider.contextWindow": "上下文窗口大小(tokens):",
|
|
182
211
|
"provider.contextWindowValidation": "必须是 ≥ 1024 的数字",
|
|
@@ -185,10 +214,12 @@ export const messages = {
|
|
|
185
214
|
"provider.multimodal": "支持图片输入(多模态)?",
|
|
186
215
|
"provider.reasoning": "支持扩展思考(推理)?",
|
|
187
216
|
"provider.detected": "检测到已有 Provider: {list}",
|
|
188
|
-
"provider.
|
|
189
|
-
"provider.
|
|
190
|
-
"provider.
|
|
191
|
-
"provider.
|
|
217
|
+
"provider.detectedKeep": "⏭ 保留现有 Provider 配置",
|
|
218
|
+
"provider.detectedKeepHint": "不修改 auth/settings/models 中的 Provider 项",
|
|
219
|
+
"provider.detectedReplace": "♻️ 重新配置 Provider",
|
|
220
|
+
"provider.detectedReplaceHint": "重设主 Provider,并可选配置备用 Provider",
|
|
221
|
+
"provider.detectedAdd": "➕ 保留现有并新增备用 Provider",
|
|
222
|
+
"provider.detectedAddHint": "保持当前主 Provider,仅追加新的 Provider",
|
|
192
223
|
"preset.select": "选择预设方案:",
|
|
193
224
|
"preset.full": "⚫ 全功能",
|
|
194
225
|
"preset.fullHint": "所有功能,包括蚁群系统",
|
|
@@ -221,7 +252,12 @@ export const messages = {
|
|
|
221
252
|
"advanced.compactValidation": "必须是 10 到 100 之间的数字",
|
|
222
253
|
"confirm.title": "配置摘要",
|
|
223
254
|
"confirm.providers": "提供商:",
|
|
255
|
+
"confirm.providerStrategy": "Provider 策略:",
|
|
256
|
+
"confirm.providerStrategyKeep": "保留现有",
|
|
257
|
+
"confirm.providerStrategyAdd": "保留现有并新增备用提供商",
|
|
258
|
+
"confirm.providerStrategyReplace": "替换为新配置",
|
|
224
259
|
"confirm.model": "模型:",
|
|
260
|
+
"confirm.fallbackProviders": "备用提供商:",
|
|
225
261
|
"confirm.theme": "主题:",
|
|
226
262
|
"confirm.keybindings": "快捷键:",
|
|
227
263
|
"confirm.thinking": "思考:",
|
|
@@ -274,6 +310,10 @@ export const messages = {
|
|
|
274
310
|
"mode.custom": "🎛️ Personnalisé",
|
|
275
311
|
"mode.customHint": "Tout choisir soi-même",
|
|
276
312
|
"provider.select": "Sélectionner les fournisseurs API",
|
|
313
|
+
"provider.selectPrimary": "Sélectionner le fournisseur API principal",
|
|
314
|
+
"provider.selectAdditional": "Sélectionner un fournisseur supplémentaire",
|
|
315
|
+
"provider.addFallback": "Ajouter un fournisseur de secours ? (avancé)",
|
|
316
|
+
"provider.selectFallback": "Sélectionner un fournisseur de secours",
|
|
277
317
|
"provider.custom": "🔧 Point d'accès personnalisé",
|
|
278
318
|
"provider.customHint": "Ollama, vLLM, LiteLLM, tout compatible OpenAI",
|
|
279
319
|
"provider.foundEnv": "{env} trouvé dans l'environnement. L'utiliser ?",
|
|
@@ -300,6 +340,13 @@ export const messages = {
|
|
|
300
340
|
"provider.modelNamePlaceholder": "llama3.1:8b",
|
|
301
341
|
"provider.modelNameRequired": "Nom du modèle requis",
|
|
302
342
|
"provider.customConfigured": "{name} configuré ({url})",
|
|
343
|
+
"provider.apiMode": "Mode API OpenAI pour {label} :",
|
|
344
|
+
"provider.apiModeAuto": "Auto (Recommandé)",
|
|
345
|
+
"provider.apiModeAutoHint": "Utilise Responses pour les modèles orientés raisonnement (sélectionné : {model})",
|
|
346
|
+
"provider.apiModeResponses": "API Responses",
|
|
347
|
+
"provider.apiModeResponsesHint": "Surface API OpenAI moderne",
|
|
348
|
+
"provider.apiModeCompletions": "API Chat Completions",
|
|
349
|
+
"provider.apiModeCompletionsHint": "Endpoint compatible historique",
|
|
303
350
|
"provider.configureCaps": "Configurer les capacités du modèle ? (fenêtre de contexte, multimodal, raisonnement)",
|
|
304
351
|
"provider.contextWindow": "Taille de la fenêtre de contexte (tokens) :",
|
|
305
352
|
"provider.contextWindowValidation": "Doit être un nombre ≥ 1024",
|
|
@@ -308,10 +355,12 @@ export const messages = {
|
|
|
308
355
|
"provider.multimodal": "Prend en charge l'entrée d'images (multimodal) ?",
|
|
309
356
|
"provider.reasoning": "Prend en charge la réflexion étendue (raisonnement) ?",
|
|
310
357
|
"provider.detected": "Fournisseurs existants détectés : {list}",
|
|
311
|
-
"provider.
|
|
312
|
-
"provider.
|
|
313
|
-
"provider.
|
|
314
|
-
"provider.
|
|
358
|
+
"provider.detectedKeep": "⏭ Garder la config fournisseur existante",
|
|
359
|
+
"provider.detectedKeepHint": "Ne modifie pas les entrées fournisseur de auth/settings/models",
|
|
360
|
+
"provider.detectedReplace": "♻️ Remplacer la config fournisseur",
|
|
361
|
+
"provider.detectedReplaceHint": "Reconfigure le fournisseur principal et des secours optionnels",
|
|
362
|
+
"provider.detectedAdd": "➕ Garder l'existant et ajouter des secours",
|
|
363
|
+
"provider.detectedAddHint": "Conserve le fournisseur principal actuel et ajoute des fournisseurs supplémentaires",
|
|
315
364
|
"preset.select": "Choisir un préréglage :",
|
|
316
365
|
"preset.full": "⚫ Complet",
|
|
317
366
|
"preset.fullHint": "Toutes les extensions et compétences",
|
|
@@ -344,7 +393,12 @@ export const messages = {
|
|
|
344
393
|
"advanced.compactValidation": "Doit être un nombre entre 10 et 100",
|
|
345
394
|
"confirm.title": "Configuration",
|
|
346
395
|
"confirm.providers": "Fournisseurs :",
|
|
396
|
+
"confirm.providerStrategy": "Stratégie fournisseur :",
|
|
397
|
+
"confirm.providerStrategyKeep": "garder l'existant",
|
|
398
|
+
"confirm.providerStrategyAdd": "garder l'existant et ajouter des fournisseurs de secours",
|
|
399
|
+
"confirm.providerStrategyReplace": "remplacer par une nouvelle configuration",
|
|
347
400
|
"confirm.model": "Modèle :",
|
|
401
|
+
"confirm.fallbackProviders": "Fournisseurs de secours :",
|
|
348
402
|
"confirm.theme": "Thème :",
|
|
349
403
|
"confirm.keybindings": "Raccourcis :",
|
|
350
404
|
"confirm.thinking": "Réflexion :",
|
|
@@ -17,10 +17,32 @@ export function countExisting(env, dir) {
|
|
|
17
17
|
* @param env - 当前环境信息
|
|
18
18
|
*/
|
|
19
19
|
export async function confirmApply(config, env) {
|
|
20
|
+
const keepProviders = config.providerStrategy === "keep";
|
|
21
|
+
const addProviders = config.providerStrategy === "add";
|
|
22
|
+
const providerNames = keepProviders || addProviders
|
|
23
|
+
? t("confirm.skipped")
|
|
24
|
+
: (config.providers.length > 0 ? config.providers.map(p => p.name).join(", ") : t("confirm.none"));
|
|
25
|
+
const primaryModel = keepProviders
|
|
26
|
+
? t("confirm.skipped")
|
|
27
|
+
: addProviders
|
|
28
|
+
? t("confirm.skipped")
|
|
29
|
+
: (config.providers[0]?.defaultModel || t("confirm.none"));
|
|
30
|
+
const fallbackProviders = keepProviders
|
|
31
|
+
? t("confirm.skipped")
|
|
32
|
+
: addProviders
|
|
33
|
+
? (config.providers.length > 0 ? config.providers.map(p => p.name).join(", ") : t("confirm.none"))
|
|
34
|
+
: (config.providers.length > 1 ? config.providers.slice(1).map(p => p.name).join(", ") : t("confirm.none"));
|
|
35
|
+
const providerStrategy = keepProviders
|
|
36
|
+
? t("confirm.providerStrategyKeep")
|
|
37
|
+
: addProviders
|
|
38
|
+
? t("confirm.providerStrategyAdd")
|
|
39
|
+
: t("confirm.providerStrategyReplace");
|
|
20
40
|
// ═══ Summary ═══
|
|
21
41
|
const summary = [
|
|
22
|
-
`${t("confirm.
|
|
23
|
-
`${t("confirm.
|
|
42
|
+
`${t("confirm.providerStrategy")} ${chalk.cyan(providerStrategy)}`,
|
|
43
|
+
`${t("confirm.providers")} ${chalk.cyan(providerNames)}`,
|
|
44
|
+
`${t("confirm.model")} ${chalk.cyan(primaryModel)}`,
|
|
45
|
+
`${t("confirm.fallbackProviders")} ${chalk.cyan(fallbackProviders)}`,
|
|
24
46
|
`${t("confirm.theme")} ${chalk.cyan(config.theme)}`,
|
|
25
47
|
`${t("confirm.keybindings")}${chalk.cyan(config.keybindings)}`,
|
|
26
48
|
`${t("confirm.thinking")} ${chalk.cyan(config.thinking)}`,
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import type { ProviderConfig } from "../types.js";
|
|
1
|
+
import type { ProviderConfig, ProviderSetupStrategy } from "../types.js";
|
|
2
2
|
import type { EnvInfo } from "../utils/detect.js";
|
|
3
3
|
/** Block internal/private IPs to prevent SSRF */
|
|
4
4
|
export declare function isUnsafeUrl(urlStr: string): boolean;
|
|
5
|
+
type OpenAIApiMode = "auto" | "openai-responses" | "openai-completions";
|
|
6
|
+
export interface ProviderSetupResult {
|
|
7
|
+
providers: ProviderConfig[];
|
|
8
|
+
providerStrategy: ProviderSetupStrategy;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveOpenAIApiMode(mode: OpenAIApiMode, modelId: string): "openai-responses" | "openai-completions";
|
|
5
11
|
/**
|
|
6
|
-
* Interactively configure API providers
|
|
12
|
+
* Interactively configure API providers with an explicit strategy.
|
|
13
|
+
* Default flow: one primary provider. Advanced flow: optional fallback providers.
|
|
7
14
|
* @param env - Current environment info with detected providers
|
|
8
|
-
* @returns
|
|
15
|
+
* @returns Provider setup result with strategy and selected providers
|
|
9
16
|
*/
|
|
10
|
-
export declare function setupProviders(env?: EnvInfo): Promise<
|
|
17
|
+
export declare function setupProviders(env?: EnvInfo): Promise<ProviderSetupResult>;
|
|
18
|
+
export {};
|
|
@@ -42,6 +42,14 @@ export function isUnsafeUrl(urlStr) {
|
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
export function resolveOpenAIApiMode(mode, modelId) {
|
|
46
|
+
if (mode !== "auto")
|
|
47
|
+
return mode;
|
|
48
|
+
const model = modelId.toLowerCase();
|
|
49
|
+
if (/^(o\d|gpt-5|gpt-4\.1|gpt-4\.5)/.test(model))
|
|
50
|
+
return "openai-responses";
|
|
51
|
+
return "openai-completions";
|
|
52
|
+
}
|
|
45
53
|
/**
|
|
46
54
|
* 动态获取模型列表,依次尝试 Anthropic、Google、OpenAI 兼容 API 风格。
|
|
47
55
|
* @param provider - 提供商名称
|
|
@@ -128,19 +136,22 @@ async function fetchModels(provider, baseUrl, apiKey) {
|
|
|
128
136
|
return { models: [] };
|
|
129
137
|
}
|
|
130
138
|
/**
|
|
131
|
-
* Interactively configure API providers
|
|
139
|
+
* Interactively configure API providers with an explicit strategy.
|
|
140
|
+
* Default flow: one primary provider. Advanced flow: optional fallback providers.
|
|
132
141
|
* @param env - Current environment info with detected providers
|
|
133
|
-
* @returns
|
|
142
|
+
* @returns Provider setup result with strategy and selected providers
|
|
134
143
|
*/
|
|
135
144
|
export async function setupProviders(env) {
|
|
136
145
|
const entries = Object.entries(PROVIDERS);
|
|
137
|
-
|
|
146
|
+
let providerStrategy = "replace";
|
|
147
|
+
// Detect existing providers — offer keep / replace / add
|
|
138
148
|
const detected = env?.existingProviders ?? [];
|
|
139
149
|
if (detected.length > 0) {
|
|
140
150
|
const action = await p.select({
|
|
141
151
|
message: t("provider.detected", { list: detected.join(", ") }),
|
|
142
152
|
options: [
|
|
143
|
-
{ value: "
|
|
153
|
+
{ value: "keep", label: t("provider.detectedKeep"), hint: t("provider.detectedKeepHint") },
|
|
154
|
+
{ value: "replace", label: t("provider.detectedReplace"), hint: t("provider.detectedReplaceHint") },
|
|
144
155
|
{ value: "add", label: t("provider.detectedAdd"), hint: t("provider.detectedAddHint") },
|
|
145
156
|
],
|
|
146
157
|
});
|
|
@@ -148,73 +159,165 @@ export async function setupProviders(env) {
|
|
|
148
159
|
p.cancel(t("cancelled"));
|
|
149
160
|
process.exit(0);
|
|
150
161
|
}
|
|
151
|
-
if (action === "
|
|
152
|
-
return [];
|
|
162
|
+
if (action === "keep") {
|
|
163
|
+
return { providers: [], providerStrategy: "keep" };
|
|
164
|
+
}
|
|
165
|
+
providerStrategy = action;
|
|
166
|
+
}
|
|
167
|
+
const selected = new Set(providerStrategy === "add" ? detected : []);
|
|
168
|
+
const configs = [];
|
|
169
|
+
if (providerStrategy === "add") {
|
|
170
|
+
let pickedAny = false;
|
|
171
|
+
while (true) {
|
|
172
|
+
const options = [
|
|
173
|
+
...entries
|
|
174
|
+
.filter(([key]) => !selected.has(key))
|
|
175
|
+
.map(([key, info]) => ({ value: key, label: info.label, hint: info.env })),
|
|
176
|
+
...(!selected.has("_custom") ? [{ value: "_custom", label: t("provider.custom"), hint: t("provider.customHint") }] : []),
|
|
177
|
+
];
|
|
178
|
+
if (options.length === 0)
|
|
179
|
+
break;
|
|
180
|
+
if (pickedAny) {
|
|
181
|
+
const addMore = await p.confirm({
|
|
182
|
+
message: t("provider.addFallback"),
|
|
183
|
+
initialValue: false,
|
|
184
|
+
});
|
|
185
|
+
if (p.isCancel(addMore)) {
|
|
186
|
+
p.cancel(t("cancelled"));
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
if (!addMore)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const next = await p.select({
|
|
193
|
+
message: pickedAny ? t("provider.selectFallback") : t("provider.selectAdditional"),
|
|
194
|
+
options,
|
|
195
|
+
});
|
|
196
|
+
if (p.isCancel(next)) {
|
|
197
|
+
p.cancel(t("cancelled"));
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
selected.add(next);
|
|
201
|
+
const added = await setupProviderChoice(next);
|
|
202
|
+
if (added)
|
|
203
|
+
configs.push(added);
|
|
204
|
+
pickedAny = true;
|
|
205
|
+
}
|
|
206
|
+
return { providers: configs, providerStrategy: "add" };
|
|
153
207
|
}
|
|
154
|
-
const
|
|
155
|
-
message: t("provider.
|
|
208
|
+
const firstChoice = await p.select({
|
|
209
|
+
message: t("provider.selectPrimary"),
|
|
156
210
|
options: [
|
|
157
211
|
...entries.map(([key, info]) => ({ value: key, label: info.label, hint: info.env })),
|
|
158
212
|
{ value: "_custom", label: t("provider.custom"), hint: t("provider.customHint") },
|
|
159
213
|
],
|
|
160
|
-
initialValues: ["anthropic"],
|
|
161
|
-
required: true,
|
|
162
214
|
});
|
|
163
|
-
if (p.isCancel(
|
|
215
|
+
if (p.isCancel(firstChoice)) {
|
|
164
216
|
p.cancel(t("cancelled"));
|
|
165
217
|
process.exit(0);
|
|
166
218
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
219
|
+
selected.add(firstChoice);
|
|
220
|
+
const primary = await setupProviderChoice(firstChoice);
|
|
221
|
+
if (primary)
|
|
222
|
+
configs.push(primary);
|
|
223
|
+
// Advanced: optional fallback providers
|
|
224
|
+
while (true) {
|
|
225
|
+
const options = [
|
|
226
|
+
...entries
|
|
227
|
+
.filter(([key]) => !selected.has(key))
|
|
228
|
+
.map(([key, info]) => ({ value: key, label: info.label, hint: info.env })),
|
|
229
|
+
...(!selected.has("_custom") ? [{ value: "_custom", label: t("provider.custom"), hint: t("provider.customHint") }] : []),
|
|
230
|
+
];
|
|
231
|
+
if (options.length === 0)
|
|
232
|
+
break;
|
|
233
|
+
const addFallback = await p.confirm({
|
|
234
|
+
message: t("provider.addFallback"),
|
|
180
235
|
initialValue: false,
|
|
181
236
|
});
|
|
182
|
-
if (p.isCancel(
|
|
237
|
+
if (p.isCancel(addFallback)) {
|
|
183
238
|
p.cancel(t("cancelled"));
|
|
184
239
|
process.exit(0);
|
|
185
240
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
process.exit(0);
|
|
196
|
-
}
|
|
197
|
-
baseUrl = url;
|
|
241
|
+
if (!addFallback)
|
|
242
|
+
break;
|
|
243
|
+
const next = await p.select({
|
|
244
|
+
message: t("provider.selectFallback"),
|
|
245
|
+
options,
|
|
246
|
+
});
|
|
247
|
+
if (p.isCancel(next)) {
|
|
248
|
+
p.cancel(t("cancelled"));
|
|
249
|
+
process.exit(0);
|
|
198
250
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
251
|
+
selected.add(next);
|
|
252
|
+
const fallback = await setupProviderChoice(next);
|
|
253
|
+
if (fallback)
|
|
254
|
+
configs.push(fallback);
|
|
255
|
+
}
|
|
256
|
+
return { providers: configs, providerStrategy: "replace" };
|
|
257
|
+
}
|
|
258
|
+
async function setupProviderChoice(choice) {
|
|
259
|
+
if (choice === "_custom") {
|
|
260
|
+
return await setupCustomProvider();
|
|
261
|
+
}
|
|
262
|
+
const name = choice;
|
|
263
|
+
const info = PROVIDERS[name];
|
|
264
|
+
const envVal = process.env[info.env];
|
|
265
|
+
const useCustomUrl = await p.confirm({
|
|
266
|
+
message: t("provider.useCustomUrl", { label: info.label }),
|
|
267
|
+
initialValue: false,
|
|
268
|
+
});
|
|
269
|
+
if (p.isCancel(useCustomUrl)) {
|
|
270
|
+
p.cancel(t("cancelled"));
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
let baseUrl;
|
|
274
|
+
if (useCustomUrl) {
|
|
275
|
+
const url = await p.text({
|
|
276
|
+
message: t("provider.baseUrl", { label: info.label }),
|
|
277
|
+
placeholder: "https://proxy.example.com",
|
|
278
|
+
validate: (v) => (!v || !v.startsWith("http")) ? t("provider.baseUrlValidation") : isUnsafeUrl(v) ? "URL must use HTTPS for remote hosts (private IPs blocked)" : undefined,
|
|
279
|
+
});
|
|
280
|
+
if (p.isCancel(url)) {
|
|
281
|
+
p.cancel(t("cancelled"));
|
|
282
|
+
process.exit(0);
|
|
207
283
|
}
|
|
208
|
-
|
|
209
|
-
|
|
284
|
+
baseUrl = url;
|
|
285
|
+
}
|
|
286
|
+
let apiKey;
|
|
287
|
+
if (envVal && !baseUrl) {
|
|
288
|
+
const useEnv = await p.confirm({ message: t("provider.foundEnv", { env: chalk.cyan(info.env) }) });
|
|
289
|
+
if (p.isCancel(useEnv)) {
|
|
290
|
+
p.cancel(t("cancelled"));
|
|
291
|
+
process.exit(0);
|
|
210
292
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
293
|
+
apiKey = useEnv ? info.env : await promptKey(info.label);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
apiKey = await promptKey(info.label);
|
|
297
|
+
}
|
|
298
|
+
const fetchUrl = baseUrl || PROVIDER_API_URLS[name];
|
|
299
|
+
const { defaultModel, discoveredModels, api } = await selectModelWithMeta(name, info.label, info.models, fetchUrl, apiKey);
|
|
300
|
+
let finalApi = api;
|
|
301
|
+
if (name === "openai") {
|
|
302
|
+
finalApi = await selectOpenAIApiMode(info.label, defaultModel);
|
|
303
|
+
}
|
|
304
|
+
p.log.success(t("provider.configured", { label: info.label }));
|
|
305
|
+
return { name, apiKey, defaultModel, baseUrl, api: finalApi, discoveredModels };
|
|
306
|
+
}
|
|
307
|
+
async function selectOpenAIApiMode(label, defaultModel) {
|
|
308
|
+
const selected = await p.select({
|
|
309
|
+
message: t("provider.apiMode", { label }),
|
|
310
|
+
options: [
|
|
311
|
+
{ value: "auto", label: t("provider.apiModeAuto"), hint: t("provider.apiModeAutoHint", { model: defaultModel }) },
|
|
312
|
+
{ value: "openai-responses", label: t("provider.apiModeResponses"), hint: t("provider.apiModeResponsesHint") },
|
|
313
|
+
{ value: "openai-completions", label: t("provider.apiModeCompletions"), hint: t("provider.apiModeCompletionsHint") },
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
if (p.isCancel(selected)) {
|
|
317
|
+
p.cancel(t("cancelled"));
|
|
318
|
+
process.exit(0);
|
|
216
319
|
}
|
|
217
|
-
return
|
|
320
|
+
return resolveOpenAIApiMode(selected, defaultModel);
|
|
218
321
|
}
|
|
219
322
|
/**
|
|
220
323
|
* Interactively configure a custom provider (Ollama, vLLM, or other OpenAI-compatible endpoints).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { isUnsafeUrl } from "./provider-setup.js";
|
|
2
|
+
import { isUnsafeUrl, resolveOpenAIApiMode } from "./provider-setup.js";
|
|
3
3
|
describe("isUnsafeUrl", () => {
|
|
4
4
|
it("https remote is safe", () => {
|
|
5
5
|
expect(isUnsafeUrl("https://api.example.com")).toBe(false);
|
|
@@ -38,3 +38,17 @@ describe("isUnsafeUrl", () => {
|
|
|
38
38
|
expect(isUnsafeUrl("")).toBe(true);
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
describe("resolveOpenAIApiMode", () => {
|
|
42
|
+
it("keeps explicit responses mode", () => {
|
|
43
|
+
expect(resolveOpenAIApiMode("openai-responses", "gpt-4o")).toBe("openai-responses");
|
|
44
|
+
});
|
|
45
|
+
it("keeps explicit completions mode", () => {
|
|
46
|
+
expect(resolveOpenAIApiMode("openai-completions", "o3-mini")).toBe("openai-completions");
|
|
47
|
+
});
|
|
48
|
+
it("auto resolves o-series to responses", () => {
|
|
49
|
+
expect(resolveOpenAIApiMode("auto", "o3-mini")).toBe("openai-responses");
|
|
50
|
+
});
|
|
51
|
+
it("auto resolves gpt-4o to completions", () => {
|
|
52
|
+
expect(resolveOpenAIApiMode("auto", "gpt-4o")).toBe("openai-completions");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** 支持的语言区域 */
|
|
2
2
|
export type Locale = "en" | "zh" | "fr";
|
|
3
|
+
export type ProviderSetupStrategy = "keep" | "replace" | "add";
|
|
3
4
|
/** 动态发现的模型信息 */
|
|
4
5
|
export interface DiscoveredModel {
|
|
5
6
|
id: string;
|
|
@@ -35,6 +36,8 @@ export interface ProviderConfig {
|
|
|
35
36
|
export interface OhPConfig {
|
|
36
37
|
/** 已配置的提供商列表 */
|
|
37
38
|
providers: ProviderConfig[];
|
|
39
|
+
/** Provider 配置策略:保留现有、替换,或在现有基础上新增 */
|
|
40
|
+
providerStrategy?: ProviderSetupStrategy;
|
|
38
41
|
/** 主题名称 */
|
|
39
42
|
theme: string;
|
|
40
43
|
/** 快捷键方案 */
|
package/dist/utils/writers.js
CHANGED
|
@@ -12,18 +12,34 @@ If the ant_colony tool is available, automatically use it when the task is compl
|
|
|
12
12
|
For simple one-file tasks, execute directly without colony overhead.
|
|
13
13
|
After launching ant_colony, use passive mode: wait for COLONY_SIGNAL updates and do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.
|
|
14
14
|
`;
|
|
15
|
+
function readJson(file) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
/** 步骤 1-2: 生成 auth.json + settings.json */
|
|
16
24
|
export function writeProviderEnv(agentDir, config) {
|
|
25
|
+
if (config.providerStrategy === "keep")
|
|
26
|
+
return;
|
|
27
|
+
const strategy = config.providerStrategy ?? "replace";
|
|
28
|
+
const authPath = join(agentDir, "auth.json");
|
|
29
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
17
30
|
// auth.json
|
|
18
31
|
const authProviders = config.providers.filter(p => !p.baseUrl && p.apiKey !== "none");
|
|
19
32
|
if (authProviders.length > 0) {
|
|
20
|
-
const auth = {};
|
|
33
|
+
const auth = strategy === "add" ? (readJson(authPath) ?? {}) : {};
|
|
21
34
|
for (const p of authProviders) {
|
|
22
35
|
auth[p.name] = { type: "api_key", key: p.apiKey };
|
|
23
36
|
}
|
|
24
|
-
writeFileSync(
|
|
37
|
+
writeFileSync(authPath, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
25
38
|
}
|
|
26
39
|
// settings.json
|
|
40
|
+
const existingSettings = strategy === "add"
|
|
41
|
+
? (readJson(settingsPath) ?? {})
|
|
42
|
+
: {};
|
|
27
43
|
const primary = config.providers.find(p => p.baseUrl && p.defaultModel) ?? config.providers[0];
|
|
28
44
|
const providerInfo = primary ? PROVIDERS[primary.name] : undefined;
|
|
29
45
|
const primaryModelId = primary?.defaultModel ?? providerInfo?.models[0];
|
|
@@ -32,8 +48,14 @@ export function writeProviderEnv(agentDir, config) {
|
|
|
32
48
|
const reserveTokens = Math.max(16384, Math.round(ctxWindow * 0.15));
|
|
33
49
|
const keepRecentTokens = Math.max(16384, Math.round(ctxWindow * 0.15));
|
|
34
50
|
const primaryModel = primary?.defaultModel ?? providerInfo?.models[0];
|
|
51
|
+
const defaultProviderModel = strategy === "add"
|
|
52
|
+
? ((!existingSettings.defaultProvider && primary)
|
|
53
|
+
? { defaultProvider: primary.name, defaultModel: primaryModel }
|
|
54
|
+
: {})
|
|
55
|
+
: (primary ? { defaultProvider: primary.name, defaultModel: primaryModel } : {});
|
|
35
56
|
const settings = {
|
|
36
|
-
...
|
|
57
|
+
...existingSettings,
|
|
58
|
+
...defaultProviderModel,
|
|
37
59
|
defaultThinkingLevel: config.thinking,
|
|
38
60
|
theme: config.theme,
|
|
39
61
|
enableSkillCommands: true,
|
|
@@ -41,22 +63,37 @@ export function writeProviderEnv(agentDir, config) {
|
|
|
41
63
|
retry: { enabled: true, maxRetries: 3 },
|
|
42
64
|
quietStartup: true,
|
|
43
65
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
const nextEnabledModels = config.providers.flatMap((p) => {
|
|
67
|
+
if (p.discoveredModels?.length)
|
|
68
|
+
return p.discoveredModels.map(m => m.id);
|
|
69
|
+
const info = PROVIDERS[p.name];
|
|
70
|
+
return info ? info.models : [];
|
|
71
|
+
});
|
|
72
|
+
if (strategy === "add") {
|
|
73
|
+
const current = Array.isArray(existingSettings.enabledModels)
|
|
74
|
+
? existingSettings.enabledModels
|
|
75
|
+
: [];
|
|
76
|
+
const merged = [...new Set([...current, ...nextEnabledModels])];
|
|
77
|
+
if (merged.length > 0)
|
|
78
|
+
settings.enabledModels = merged;
|
|
79
|
+
}
|
|
80
|
+
else if (config.providers.length > 1) {
|
|
81
|
+
settings.enabledModels = nextEnabledModels;
|
|
51
82
|
}
|
|
52
|
-
writeFileSync(
|
|
83
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
53
84
|
}
|
|
54
85
|
/** 步骤 3: 生成 models.json(自定义端点) */
|
|
55
86
|
export function writeModelConfig(agentDir, config) {
|
|
87
|
+
if (config.providerStrategy === "keep")
|
|
88
|
+
return;
|
|
89
|
+
const strategy = config.providerStrategy ?? "replace";
|
|
90
|
+
const modelsPath = join(agentDir, "models.json");
|
|
56
91
|
const customProviders = config.providers.filter(p => p.baseUrl);
|
|
57
92
|
if (customProviders.length === 0)
|
|
58
93
|
return;
|
|
59
|
-
const providers =
|
|
94
|
+
const providers = strategy === "add"
|
|
95
|
+
? (readJson(modelsPath)?.providers ?? {})
|
|
96
|
+
: {};
|
|
60
97
|
for (const cp of customProviders) {
|
|
61
98
|
const isBuiltin = !!PROVIDERS[cp.name];
|
|
62
99
|
if (isBuiltin && !cp.discoveredModels?.length) {
|
|
@@ -93,7 +130,7 @@ export function writeModelConfig(agentDir, config) {
|
|
|
93
130
|
providers[cp.name] = entry;
|
|
94
131
|
}
|
|
95
132
|
}
|
|
96
|
-
writeFileSync(
|
|
133
|
+
writeFileSync(modelsPath, JSON.stringify({ providers }, null, 2));
|
|
97
134
|
}
|
|
98
135
|
/** 步骤 4: 生成 keybindings.json */
|
|
99
136
|
export function writeKeybindings(agentDir, config) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { writeAgents } from "./writers.js";
|
|
5
|
+
import { writeAgents, writeProviderEnv, writeModelConfig } from "./writers.js";
|
|
6
6
|
const tempDirs = [];
|
|
7
7
|
function makeTempDir() {
|
|
8
8
|
const dir = mkdtempSync(join(tmpdir(), "oh-pi-writers-"));
|
|
@@ -58,3 +58,109 @@ describe("writeAgents", () => {
|
|
|
58
58
|
expect(content).toContain("You command an autonomous ant colony");
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
describe("provider keep strategy", () => {
|
|
62
|
+
it("writeProviderEnv does not touch settings/auth when strategy is keep", () => {
|
|
63
|
+
const dir = makeTempDir();
|
|
64
|
+
const settingsPath = join(dir, "settings.json");
|
|
65
|
+
const authPath = join(dir, "auth.json");
|
|
66
|
+
const originalSettings = JSON.stringify({ defaultProvider: "openai", defaultModel: "gpt-4o" }, null, 2);
|
|
67
|
+
const originalAuth = JSON.stringify({ openai: { type: "api_key", key: "OPENAI_API_KEY" } }, null, 2);
|
|
68
|
+
writeFileSync(settingsPath, originalSettings);
|
|
69
|
+
writeFileSync(authPath, originalAuth);
|
|
70
|
+
writeProviderEnv(dir, makeConfig({
|
|
71
|
+
providerStrategy: "keep",
|
|
72
|
+
providers: [],
|
|
73
|
+
}));
|
|
74
|
+
expect(readFileSync(settingsPath, "utf8")).toBe(originalSettings);
|
|
75
|
+
expect(readFileSync(authPath, "utf8")).toBe(originalAuth);
|
|
76
|
+
});
|
|
77
|
+
it("writeModelConfig does not touch models.json when strategy is keep", () => {
|
|
78
|
+
const dir = makeTempDir();
|
|
79
|
+
const modelsPath = join(dir, "models.json");
|
|
80
|
+
const originalModels = JSON.stringify({
|
|
81
|
+
providers: {
|
|
82
|
+
openai: { baseUrl: "https://api.openai.com", api: "openai-responses" },
|
|
83
|
+
},
|
|
84
|
+
}, null, 2);
|
|
85
|
+
writeFileSync(modelsPath, originalModels);
|
|
86
|
+
writeModelConfig(dir, makeConfig({
|
|
87
|
+
providerStrategy: "keep",
|
|
88
|
+
providers: [],
|
|
89
|
+
}));
|
|
90
|
+
expect(readFileSync(modelsPath, "utf8")).toBe(originalModels);
|
|
91
|
+
});
|
|
92
|
+
it("writeModelConfig creates custom provider entries when replacing", () => {
|
|
93
|
+
const dir = makeTempDir();
|
|
94
|
+
writeModelConfig(dir, makeConfig({
|
|
95
|
+
providerStrategy: "replace",
|
|
96
|
+
providers: [{
|
|
97
|
+
name: "custom-openai",
|
|
98
|
+
apiKey: "OPENAI_API_KEY",
|
|
99
|
+
baseUrl: "https://api.openai.com",
|
|
100
|
+
defaultModel: "gpt-4o",
|
|
101
|
+
api: "openai-responses",
|
|
102
|
+
}],
|
|
103
|
+
}));
|
|
104
|
+
const modelsPath = join(dir, "models.json");
|
|
105
|
+
expect(existsSync(modelsPath)).toBe(true);
|
|
106
|
+
const text = readFileSync(modelsPath, "utf8");
|
|
107
|
+
expect(text).toContain("\"custom-openai\"");
|
|
108
|
+
expect(text).toContain("\"openai-responses\"");
|
|
109
|
+
});
|
|
110
|
+
it("writeProviderEnv merges auth/settings when strategy is add", () => {
|
|
111
|
+
const dir = makeTempDir();
|
|
112
|
+
const settingsPath = join(dir, "settings.json");
|
|
113
|
+
const authPath = join(dir, "auth.json");
|
|
114
|
+
writeFileSync(settingsPath, JSON.stringify({
|
|
115
|
+
defaultProvider: "anthropic",
|
|
116
|
+
defaultModel: "claude-sonnet-4-20250514",
|
|
117
|
+
enabledModels: ["claude-sonnet-4-20250514"],
|
|
118
|
+
theme: "dark",
|
|
119
|
+
}, null, 2));
|
|
120
|
+
writeFileSync(authPath, JSON.stringify({
|
|
121
|
+
anthropic: { type: "api_key", key: "ANTHROPIC_API_KEY" },
|
|
122
|
+
}, null, 2));
|
|
123
|
+
writeProviderEnv(dir, makeConfig({
|
|
124
|
+
providerStrategy: "add",
|
|
125
|
+
theme: "light",
|
|
126
|
+
providers: [{
|
|
127
|
+
name: "openai",
|
|
128
|
+
apiKey: "OPENAI_API_KEY",
|
|
129
|
+
defaultModel: "gpt-4o",
|
|
130
|
+
discoveredModels: [{ id: "gpt-4o", reasoning: false, input: ["text", "image"], contextWindow: 128000, maxTokens: 16384 }],
|
|
131
|
+
}],
|
|
132
|
+
}));
|
|
133
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
134
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
135
|
+
expect(settings.defaultProvider).toBe("anthropic");
|
|
136
|
+
expect(settings.defaultModel).toBe("claude-sonnet-4-20250514");
|
|
137
|
+
expect(settings.theme).toBe("light");
|
|
138
|
+
expect(settings.enabledModels).toContain("claude-sonnet-4-20250514");
|
|
139
|
+
expect(settings.enabledModels).toContain("gpt-4o");
|
|
140
|
+
expect(auth.anthropic).toBeTruthy();
|
|
141
|
+
expect(auth.openai).toEqual({ type: "api_key", key: "OPENAI_API_KEY" });
|
|
142
|
+
});
|
|
143
|
+
it("writeModelConfig merges custom providers when strategy is add", () => {
|
|
144
|
+
const dir = makeTempDir();
|
|
145
|
+
const modelsPath = join(dir, "models.json");
|
|
146
|
+
writeFileSync(modelsPath, JSON.stringify({
|
|
147
|
+
providers: {
|
|
148
|
+
existing: { baseUrl: "https://example.com/v1", api: "openai-completions" },
|
|
149
|
+
},
|
|
150
|
+
}, null, 2));
|
|
151
|
+
writeModelConfig(dir, makeConfig({
|
|
152
|
+
providerStrategy: "add",
|
|
153
|
+
providers: [{
|
|
154
|
+
name: "custom-openai",
|
|
155
|
+
apiKey: "OPENAI_API_KEY",
|
|
156
|
+
baseUrl: "https://api.openai.com",
|
|
157
|
+
defaultModel: "gpt-4o",
|
|
158
|
+
api: "openai-responses",
|
|
159
|
+
}],
|
|
160
|
+
}));
|
|
161
|
+
const models = JSON.parse(readFileSync(modelsPath, "utf8"));
|
|
162
|
+
expect(models.providers.existing).toBeTruthy();
|
|
163
|
+
expect(models.providers["custom-openai"]).toBeTruthy();
|
|
164
|
+
expect(JSON.stringify(models.providers["custom-openai"])).toContain("openai-responses");
|
|
165
|
+
});
|
|
166
|
+
});
|