oh-pi 0.1.46 → 0.1.48
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/tui/provider-setup.js +73 -46
- package/dist/types.d.ts +12 -0
- package/dist/utils/install.js +23 -10
- package/package.json +1 -1
|
@@ -12,27 +12,35 @@ const PROVIDER_API_URLS = {
|
|
|
12
12
|
xai: "https://api.x.ai",
|
|
13
13
|
mistral: "https://api.mistral.ai",
|
|
14
14
|
};
|
|
15
|
-
/** Fetch models dynamically — tries multiple API styles */
|
|
15
|
+
/** Fetch models dynamically — tries multiple API styles, returns metadata + detected API type */
|
|
16
16
|
async function fetchModels(provider, baseUrl, apiKey) {
|
|
17
17
|
const base = baseUrl.replace(/\/+$/, "");
|
|
18
18
|
const resolvedKey = process.env[apiKey] ?? apiKey;
|
|
19
|
-
// Try Anthropic-style
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
// Try Anthropic-style first (for known anthropic or any provider)
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${base}/v1/models`, {
|
|
22
|
+
headers: { "x-api-key": resolvedKey, "anthropic-version": "2023-06-01" },
|
|
23
|
+
signal: AbortSignal.timeout(8000),
|
|
24
|
+
});
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
const json = await res.json();
|
|
27
|
+
const data = json.data ?? [];
|
|
28
|
+
if (data.length > 0 && data[0].owned_by === "anthropic") {
|
|
29
|
+
return {
|
|
30
|
+
api: "anthropic-messages",
|
|
31
|
+
models: data.map(m => ({
|
|
32
|
+
id: m.id,
|
|
33
|
+
reasoning: m.thinking_enabled ?? false,
|
|
34
|
+
input: ["text", "image"],
|
|
35
|
+
contextWindow: m.max_tokens ?? 200000,
|
|
36
|
+
maxTokens: m.thinking_enabled ? Math.min(m.max_tokens ?? 128000, 128000) : Math.min(m.max_tokens ?? 8192, 16384),
|
|
37
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
38
|
+
};
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
|
-
catch { /* fall through */ }
|
|
34
41
|
}
|
|
35
|
-
|
|
42
|
+
catch { /* fall through */ }
|
|
43
|
+
// Try Google-style
|
|
36
44
|
if (provider === "google") {
|
|
37
45
|
try {
|
|
38
46
|
const res = await fetch(`${base}/v1beta/models?key=${resolvedKey}`, {
|
|
@@ -40,17 +48,24 @@ async function fetchModels(provider, baseUrl, apiKey) {
|
|
|
40
48
|
});
|
|
41
49
|
if (res.ok) {
|
|
42
50
|
const json = await res.json();
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const data = (json.models ?? []).filter((m) => m.name?.includes("gemini"));
|
|
52
|
+
if (data.length > 0) {
|
|
53
|
+
return {
|
|
54
|
+
api: "google-generative-ai",
|
|
55
|
+
models: data.map((m) => ({
|
|
56
|
+
id: m.name.replace("models/", ""),
|
|
57
|
+
reasoning: m.name.includes("thinking") || m.name.includes("2.5"),
|
|
58
|
+
input: ["text", "image"],
|
|
59
|
+
contextWindow: m.inputTokenLimit ?? 1048576,
|
|
60
|
+
maxTokens: m.outputTokenLimit ?? 65536,
|
|
61
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
catch { /* fall through */ }
|
|
52
67
|
}
|
|
53
|
-
// Try OpenAI-compatible
|
|
68
|
+
// Try OpenAI-compatible
|
|
54
69
|
try {
|
|
55
70
|
const res = await fetch(`${base}/v1/models`, {
|
|
56
71
|
headers: { Authorization: `Bearer ${resolvedKey}` },
|
|
@@ -58,13 +73,23 @@ async function fetchModels(provider, baseUrl, apiKey) {
|
|
|
58
73
|
});
|
|
59
74
|
if (res.ok) {
|
|
60
75
|
const json = await res.json();
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
76
|
+
const data = json.data ?? [];
|
|
77
|
+
if (data.length > 0) {
|
|
78
|
+
return {
|
|
79
|
+
api: "openai-completions",
|
|
80
|
+
models: data.map((m) => ({
|
|
81
|
+
id: m.id,
|
|
82
|
+
reasoning: m.thinking_enabled ?? m.id.includes("o3") ?? false,
|
|
83
|
+
input: ["text", "image"],
|
|
84
|
+
contextWindow: m.context_window ?? m.max_tokens ?? 128000,
|
|
85
|
+
maxTokens: m.max_output ?? 16384,
|
|
86
|
+
})).sort((a, b) => a.id.localeCompare(b.id)),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
64
89
|
}
|
|
65
90
|
}
|
|
66
91
|
catch { /* fall through */ }
|
|
67
|
-
return [];
|
|
92
|
+
return { models: [] };
|
|
68
93
|
}
|
|
69
94
|
export async function setupProviders(env) {
|
|
70
95
|
const entries = Object.entries(PROVIDERS);
|
|
@@ -142,10 +167,10 @@ export async function setupProviders(env) {
|
|
|
142
167
|
else {
|
|
143
168
|
apiKey = await promptKey(info.label);
|
|
144
169
|
}
|
|
145
|
-
// Dynamic model fetch — always try
|
|
170
|
+
// Dynamic model fetch — always try
|
|
146
171
|
const fetchUrl = baseUrl || PROVIDER_API_URLS[name];
|
|
147
|
-
const defaultModel = await
|
|
148
|
-
configs.push({ name, apiKey, defaultModel, baseUrl });
|
|
172
|
+
const { defaultModel, discoveredModels, api } = await selectModelWithMeta(name, info.label, info.models, fetchUrl, apiKey);
|
|
173
|
+
configs.push({ name, apiKey, defaultModel, baseUrl, api, discoveredModels });
|
|
149
174
|
p.log.success(t("provider.configured", { label: info.label }));
|
|
150
175
|
}
|
|
151
176
|
return configs;
|
|
@@ -178,24 +203,26 @@ async function setupCustomProvider() {
|
|
|
178
203
|
if (needsKey) {
|
|
179
204
|
apiKey = await promptKey(name);
|
|
180
205
|
}
|
|
181
|
-
|
|
182
|
-
const defaultModel = await selectModel(name, name, [], baseUrl, apiKey);
|
|
206
|
+
const { defaultModel, discoveredModels, api } = await selectModelWithMeta(name, name, [], baseUrl, apiKey);
|
|
183
207
|
p.log.success(t("provider.customConfigured", { name, url: baseUrl }));
|
|
184
|
-
return { name, apiKey, defaultModel, baseUrl };
|
|
208
|
+
return { name, apiKey, defaultModel, baseUrl, api, discoveredModels };
|
|
185
209
|
}
|
|
186
|
-
async function
|
|
187
|
-
let
|
|
188
|
-
|
|
210
|
+
async function selectModelWithMeta(provider, label, staticModels, baseUrl, apiKey) {
|
|
211
|
+
let modelIds = staticModels;
|
|
212
|
+
let discoveredModels;
|
|
213
|
+
let api;
|
|
189
214
|
if (baseUrl && apiKey) {
|
|
190
215
|
const s = p.spinner();
|
|
191
216
|
s.start(t("provider.fetchingModels", { source: label }));
|
|
192
|
-
const
|
|
193
|
-
s.stop(
|
|
194
|
-
if (
|
|
195
|
-
|
|
217
|
+
const result = await fetchModels(provider, baseUrl, apiKey);
|
|
218
|
+
s.stop(result.models.length > 0 ? t("provider.foundModels", { count: result.models.length }) : t("provider.defaultModelList"));
|
|
219
|
+
if (result.models.length > 0) {
|
|
220
|
+
discoveredModels = result.models;
|
|
221
|
+
api = result.api;
|
|
222
|
+
modelIds = result.models.map(m => m.id);
|
|
223
|
+
}
|
|
196
224
|
}
|
|
197
|
-
if (
|
|
198
|
-
// No models found — manual input
|
|
225
|
+
if (modelIds.length === 0) {
|
|
199
226
|
const model = await p.text({
|
|
200
227
|
message: t("provider.modelName", { label }),
|
|
201
228
|
placeholder: t("provider.modelNamePlaceholder"),
|
|
@@ -205,19 +232,19 @@ async function selectModel(provider, label, staticModels, baseUrl, apiKey) {
|
|
|
205
232
|
p.cancel(t("cancelled"));
|
|
206
233
|
process.exit(0);
|
|
207
234
|
}
|
|
208
|
-
return model;
|
|
235
|
+
return { defaultModel: model, discoveredModels, api };
|
|
209
236
|
}
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
237
|
+
if (modelIds.length === 1)
|
|
238
|
+
return { defaultModel: modelIds[0], discoveredModels, api };
|
|
212
239
|
const model = await p.select({
|
|
213
240
|
message: t("provider.selectModel", { label }),
|
|
214
|
-
options:
|
|
241
|
+
options: modelIds.slice(0, 50).map(m => ({ value: m, label: m })),
|
|
215
242
|
});
|
|
216
243
|
if (p.isCancel(model)) {
|
|
217
244
|
p.cancel(t("cancelled"));
|
|
218
245
|
process.exit(0);
|
|
219
246
|
}
|
|
220
|
-
return model;
|
|
247
|
+
return { defaultModel: model, discoveredModels, api };
|
|
221
248
|
}
|
|
222
249
|
async function promptKey(label) {
|
|
223
250
|
const key = await p.password({
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** 动态发现的模型信息 */
|
|
2
|
+
export interface DiscoveredModel {
|
|
3
|
+
id: string;
|
|
4
|
+
reasoning: boolean;
|
|
5
|
+
input: ("text" | "image")[];
|
|
6
|
+
contextWindow: number;
|
|
7
|
+
maxTokens: number;
|
|
8
|
+
}
|
|
1
9
|
/** 模型提供商配置 */
|
|
2
10
|
export interface ProviderConfig {
|
|
3
11
|
/** 提供商名称 */
|
|
@@ -8,6 +16,10 @@ export interface ProviderConfig {
|
|
|
8
16
|
defaultModel?: string;
|
|
9
17
|
/** 自定义 API 地址 */
|
|
10
18
|
baseUrl?: string;
|
|
19
|
+
/** 检测到的 API 类型 */
|
|
20
|
+
api?: string;
|
|
21
|
+
/** 动态发现的所有模型 */
|
|
22
|
+
discoveredModels?: DiscoveredModel[];
|
|
11
23
|
/** 上下文窗口大小(自定义提供商用) */
|
|
12
24
|
contextWindow?: number;
|
|
13
25
|
/** 最大输出 token 数(自定义提供商用) */
|
package/dist/utils/install.js
CHANGED
|
@@ -56,13 +56,12 @@ export function applyConfig(config) {
|
|
|
56
56
|
writeFileSync(authPath, JSON.stringify(auth, null, 2), { mode: 0o600 });
|
|
57
57
|
}
|
|
58
58
|
// 2. settings.json
|
|
59
|
-
|
|
59
|
+
// Issue #4 fix: prefer provider with baseUrl+defaultModel as primary (custom endpoint user intent)
|
|
60
|
+
const primary = config.providers.find(p => p.baseUrl && p.defaultModel) ?? config.providers[0];
|
|
60
61
|
const providerInfo = primary ? PROVIDERS[primary.name] : undefined;
|
|
61
62
|
const compactThreshold = config.compactThreshold ?? 0.75;
|
|
63
|
+
const reserveTokens = 32000;
|
|
62
64
|
const primaryModel = primary?.defaultModel ?? providerInfo?.models[0];
|
|
63
|
-
const primaryCaps = primaryModel ? MODEL_CAPABILITIES[primaryModel] : undefined;
|
|
64
|
-
const contextWindow = primary?.contextWindow ?? primaryCaps?.contextWindow ?? 128000;
|
|
65
|
-
const reserveTokens = Math.round(contextWindow * (1 - compactThreshold));
|
|
66
65
|
const settings = {
|
|
67
66
|
...(primary ? { defaultProvider: primary.name, defaultModel: primaryModel } : {}),
|
|
68
67
|
defaultThinkingLevel: config.thinking,
|
|
@@ -74,6 +73,8 @@ export function applyConfig(config) {
|
|
|
74
73
|
};
|
|
75
74
|
if (config.providers.length > 1) {
|
|
76
75
|
settings.enabledModels = config.providers.flatMap((p) => {
|
|
76
|
+
if (p.discoveredModels?.length)
|
|
77
|
+
return p.discoveredModels.map(m => m.id);
|
|
77
78
|
const info = PROVIDERS[p.name];
|
|
78
79
|
return info ? info.models : [];
|
|
79
80
|
});
|
|
@@ -85,23 +86,35 @@ export function applyConfig(config) {
|
|
|
85
86
|
const providers = {};
|
|
86
87
|
for (const cp of customProviders) {
|
|
87
88
|
const isBuiltin = !!PROVIDERS[cp.name];
|
|
88
|
-
if (isBuiltin) {
|
|
89
|
-
// Known provider with custom baseUrl — just override endpoint
|
|
89
|
+
if (isBuiltin && !cp.discoveredModels?.length) {
|
|
90
|
+
// Known provider with custom baseUrl, no discovered models — just override endpoint
|
|
90
91
|
const entry = { baseUrl: cp.baseUrl };
|
|
91
92
|
if (cp.apiKey !== "none")
|
|
92
93
|
entry.apiKey = cp.apiKey;
|
|
93
94
|
providers[cp.name] = entry;
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
//
|
|
97
|
-
const caps = cp.defaultModel ? MODEL_CAPABILITIES[cp.defaultModel] : undefined;
|
|
97
|
+
// Custom provider or builtin with discovered models — write full config
|
|
98
98
|
const entry = {
|
|
99
99
|
baseUrl: cp.baseUrl,
|
|
100
|
-
api: "openai-completions",
|
|
100
|
+
api: cp.api ?? "openai-completions",
|
|
101
101
|
};
|
|
102
102
|
if (cp.apiKey !== "none")
|
|
103
103
|
entry.apiKey = cp.apiKey;
|
|
104
|
-
if (cp.
|
|
104
|
+
if (cp.discoveredModels?.length) {
|
|
105
|
+
// Write ALL discovered models with their metadata
|
|
106
|
+
entry.models = cp.discoveredModels.map(m => ({
|
|
107
|
+
id: m.id,
|
|
108
|
+
name: m.id,
|
|
109
|
+
reasoning: m.reasoning,
|
|
110
|
+
input: m.input,
|
|
111
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
112
|
+
contextWindow: m.contextWindow,
|
|
113
|
+
maxTokens: m.maxTokens,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
else if (cp.defaultModel) {
|
|
117
|
+
const caps = MODEL_CAPABILITIES[cp.defaultModel];
|
|
105
118
|
entry.models = [{
|
|
106
119
|
id: cp.defaultModel,
|
|
107
120
|
name: cp.defaultModel,
|