oh-pi 0.1.45 → 0.1.46

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.
@@ -2,22 +2,69 @@ import * as p from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { t } from "../i18n.js";
4
4
  import { PROVIDERS } from "../types.js";
5
- /** Fetch models from OpenAI-compatible /v1/models endpoint */
6
- async function fetchModels(baseUrl, apiKey) {
7
- const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
5
+ /** Provider API base URLs for dynamic model fetching */
6
+ const PROVIDER_API_URLS = {
7
+ anthropic: "https://api.anthropic.com",
8
+ openai: "https://api.openai.com",
9
+ google: "https://generativelanguage.googleapis.com",
10
+ groq: "https://api.groq.com",
11
+ openrouter: "https://openrouter.ai",
12
+ xai: "https://api.x.ai",
13
+ mistral: "https://api.mistral.ai",
14
+ };
15
+ /** Fetch models dynamically — tries multiple API styles */
16
+ async function fetchModels(provider, baseUrl, apiKey) {
17
+ const base = baseUrl.replace(/\/+$/, "");
18
+ const resolvedKey = process.env[apiKey] ?? apiKey;
19
+ // Try Anthropic-style: GET /v1/models with x-api-key header
20
+ if (provider === "anthropic") {
21
+ try {
22
+ const res = await fetch(`${base}/v1/models`, {
23
+ headers: { "x-api-key": resolvedKey, "anthropic-version": "2023-06-01" },
24
+ signal: AbortSignal.timeout(8000),
25
+ });
26
+ if (res.ok) {
27
+ const json = await res.json();
28
+ const models = (json.data ?? []).map(m => m.id).sort();
29
+ if (models.length > 0)
30
+ return models;
31
+ }
32
+ }
33
+ catch { /* fall through */ }
34
+ }
35
+ // Try Google-style: GET /v1beta/models with key param
36
+ if (provider === "google") {
37
+ try {
38
+ const res = await fetch(`${base}/v1beta/models?key=${resolvedKey}`, {
39
+ signal: AbortSignal.timeout(8000),
40
+ });
41
+ if (res.ok) {
42
+ const json = await res.json();
43
+ const models = (json.models ?? [])
44
+ .map(m => m.name.replace("models/", ""))
45
+ .filter(m => m.includes("gemini"))
46
+ .sort();
47
+ if (models.length > 0)
48
+ return models;
49
+ }
50
+ }
51
+ catch { /* fall through */ }
52
+ }
53
+ // Try OpenAI-compatible: GET /v1/models with Bearer auth
8
54
  try {
9
- const res = await fetch(url, {
10
- headers: { Authorization: `Bearer ${apiKey}` },
55
+ const res = await fetch(`${base}/v1/models`, {
56
+ headers: { Authorization: `Bearer ${resolvedKey}` },
11
57
  signal: AbortSignal.timeout(8000),
12
58
  });
13
- if (!res.ok)
14
- return [];
15
- const json = await res.json();
16
- return (json.data ?? []).map(m => m.id).sort();
17
- }
18
- catch {
19
- return [];
59
+ if (res.ok) {
60
+ const json = await res.json();
61
+ const models = (json.data ?? []).map(m => m.id).sort();
62
+ if (models.length > 0)
63
+ return models;
64
+ }
20
65
  }
66
+ catch { /* fall through */ }
67
+ return [];
21
68
  }
22
69
  export async function setupProviders(env) {
23
70
  const entries = Object.entries(PROVIDERS);
@@ -95,8 +142,9 @@ export async function setupProviders(env) {
95
142
  else {
96
143
  apiKey = await promptKey(info.label);
97
144
  }
98
- // Model selection — try dynamic fetch for custom endpoints, fall back to static list
99
- const defaultModel = await selectModel(info.label, info.models, baseUrl, apiKey);
145
+ // Dynamic model fetch always try, fall back to static list
146
+ const fetchUrl = baseUrl || PROVIDER_API_URLS[name];
147
+ const defaultModel = await selectModel(name, info.label, info.models, fetchUrl, apiKey);
100
148
  configs.push({ name, apiKey, defaultModel, baseUrl });
101
149
  p.log.success(t("provider.configured", { label: info.label }));
102
150
  }
@@ -131,53 +179,39 @@ async function setupCustomProvider() {
131
179
  apiKey = await promptKey(name);
132
180
  }
133
181
  // Dynamic model fetch
134
- const s = p.spinner();
135
- s.start(t("provider.fetchingModels", { source: baseUrl }));
136
- const models = await fetchModels(baseUrl, apiKey);
137
- s.stop(models.length > 0 ? t("provider.foundModels", { count: models.length }) : t("provider.noModels"));
138
- let defaultModel;
139
- if (models.length > 0) {
140
- const model = await p.select({
141
- message: t("provider.selectModel", { label: name }),
142
- options: models.slice(0, 30).map(m => ({ value: m, label: m })),
143
- });
144
- if (p.isCancel(model)) {
145
- p.cancel(t("cancelled"));
146
- process.exit(0);
147
- }
148
- defaultModel = model;
149
- }
150
- else {
151
- const model = await p.text({
152
- message: t("provider.modelName", { label: name }),
153
- placeholder: t("provider.modelNamePlaceholder"),
154
- validate: (v) => (!v || v.trim().length === 0) ? t("provider.modelNameRequired") : undefined,
155
- });
156
- if (p.isCancel(model)) {
157
- p.cancel(t("cancelled"));
158
- process.exit(0);
159
- }
160
- defaultModel = model;
161
- }
182
+ const defaultModel = await selectModel(name, name, [], baseUrl, apiKey);
162
183
  p.log.success(t("provider.customConfigured", { name, url: baseUrl }));
163
184
  return { name, apiKey, defaultModel, baseUrl };
164
185
  }
165
- async function selectModel(label, staticModels, baseUrl, apiKey) {
186
+ async function selectModel(provider, label, staticModels, baseUrl, apiKey) {
166
187
  let models = staticModels;
167
- // Try dynamic fetch if custom URL or known provider
188
+ // Always try dynamic fetch
168
189
  if (baseUrl && apiKey) {
169
190
  const s = p.spinner();
170
191
  s.start(t("provider.fetchingModels", { source: label }));
171
- const fetched = await fetchModels(baseUrl, apiKey);
192
+ const fetched = await fetchModels(provider, baseUrl, apiKey);
172
193
  s.stop(fetched.length > 0 ? t("provider.foundModels", { count: fetched.length }) : t("provider.defaultModelList"));
173
194
  if (fetched.length > 0)
174
195
  models = fetched;
175
196
  }
197
+ if (models.length === 0) {
198
+ // No models found — manual input
199
+ const model = await p.text({
200
+ message: t("provider.modelName", { label }),
201
+ placeholder: t("provider.modelNamePlaceholder"),
202
+ validate: (v) => (!v || v.trim().length === 0) ? t("provider.modelNameRequired") : undefined,
203
+ });
204
+ if (p.isCancel(model)) {
205
+ p.cancel(t("cancelled"));
206
+ process.exit(0);
207
+ }
208
+ return model;
209
+ }
176
210
  if (models.length === 1)
177
211
  return models[0];
178
212
  const model = await p.select({
179
213
  message: t("provider.selectModel", { label }),
180
- options: models.slice(0, 30).map(m => ({ value: m, label: m })),
214
+ options: models.slice(0, 50).map(m => ({ value: m, label: m })),
181
215
  });
182
216
  if (p.isCancel(model)) {
183
217
  p.cancel(t("cancelled"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {