pi-free 2.0.11 → 2.0.13
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/CHANGELOG.md +628 -576
- package/README.md +29 -42
- package/banner.svg +1 -1
- package/config.ts +99 -2
- package/constants.ts +3 -1
- package/index.ts +5 -3
- package/lib/built-in-toggle.ts +120 -60
- package/lib/registry.ts +40 -16
- package/lib/util.ts +13 -12
- package/package.json +2 -2
- package/providers/cline/cline-models.ts +3 -10
- package/providers/crofai/crofai.ts +5 -1
- package/providers/deepinfra/deepinfra.ts +7 -5
- package/providers/dynamic-built-in/index.ts +129 -33
- package/providers/model-fetcher.ts +2 -13
- package/providers/novita/novita.ts +205 -0
- package/providers/nvidia/nvidia.ts +4 -6
- package/providers/opencode-session.ts +371 -33
- package/providers/sambanova/sambanova.ts +8 -2
- package/providers/together/together.ts +6 -9
- package/providers/zenmux/zenmux.ts +6 -4
package/lib/registry.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ExtensionAPI,
|
|
10
10
|
ProviderModelConfig,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { getFreeOnly, saveConfig } from "../config.ts";
|
|
12
|
+
import { getFreeOnly, getProviderShowPaid, saveConfig } from "../config.ts";
|
|
13
13
|
import { createLogger } from "./logger.ts";
|
|
14
14
|
|
|
15
15
|
const _logger = createLogger("pi-free");
|
|
@@ -82,7 +82,7 @@ function detectPricingExposed(allModels: ProviderModelConfig[]): boolean {
|
|
|
82
82
|
* @returns true if the model is definitively free per the provider's API
|
|
83
83
|
*/
|
|
84
84
|
export function isFreeModel(
|
|
85
|
-
model: ProviderModelConfig & { provider?: string },
|
|
85
|
+
model: ProviderModelConfig & { provider?: string; _pricingKnown?: boolean },
|
|
86
86
|
allModels?: ProviderModelConfig[],
|
|
87
87
|
): boolean {
|
|
88
88
|
return isFreeModelInternal(model, allModels);
|
|
@@ -90,7 +90,7 @@ export function isFreeModel(
|
|
|
90
90
|
|
|
91
91
|
// Internal implementation to work around TypeScript filter callback issues
|
|
92
92
|
function isFreeModelInternal(
|
|
93
|
-
model: ProviderModelConfig & { provider?: string },
|
|
93
|
+
model: ProviderModelConfig & { provider?: string; _pricingKnown?: boolean },
|
|
94
94
|
allModels: ProviderModelConfig[] | undefined,
|
|
95
95
|
): boolean {
|
|
96
96
|
// Determine if pricing is exposed
|
|
@@ -106,12 +106,20 @@ function isFreeModelInternal(
|
|
|
106
106
|
pricingExposed = true;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Route A: Pricing-exposed providers
|
|
110
|
-
// Model is free if EITHER cost is zero OR name contains "free"
|
|
109
|
+
// Route A: Pricing-exposed providers
|
|
110
|
+
// Model is free if EITHER cost is zero OR name contains "free".
|
|
111
|
+
// BUT: when _pricingKnown is explicitly false (API returned no pricing data),
|
|
112
|
+
// cost values are untrustworthy defaults — fall back to name-only detection.
|
|
111
113
|
if (pricingExposed) {
|
|
112
114
|
const isZeroCost =
|
|
113
115
|
(model.cost?.input ?? 0) === 0 && (model.cost?.output ?? 0) === 0;
|
|
114
116
|
const hasFreeInName = model.name.toLowerCase().includes("free");
|
|
117
|
+
|
|
118
|
+
// Pricing missing for this specific model — only trust name-based signal
|
|
119
|
+
if (model._pricingKnown === false) {
|
|
120
|
+
return hasFreeInName;
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
return isZeroCost || hasFreeInName;
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -156,12 +164,32 @@ export function getProviderRegistry(): ReadonlyMap<string, ProviderEntry> {
|
|
|
156
164
|
// Global filter application
|
|
157
165
|
// =============================================================================
|
|
158
166
|
|
|
167
|
+
function showAllForProvider(providerId: string, entry: ProviderEntry): void {
|
|
168
|
+
const allModels =
|
|
169
|
+
entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
|
|
170
|
+
if (allModels.length > 0) {
|
|
171
|
+
entry.reRegister(allModels);
|
|
172
|
+
_logger.info(
|
|
173
|
+
`[pi-free] ${providerId}: showing all ${allModels.length} models`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
function applyFilterToProvider(
|
|
160
179
|
providerId: string,
|
|
161
180
|
entry: ProviderEntry,
|
|
162
181
|
freeOnly: boolean,
|
|
182
|
+
force: boolean,
|
|
163
183
|
): void {
|
|
164
184
|
if (freeOnly) {
|
|
185
|
+
if (!force && getProviderShowPaid(providerId)) {
|
|
186
|
+
showAllForProvider(providerId, entry);
|
|
187
|
+
_logger.info(
|
|
188
|
+
`[pi-free] ${providerId}: preserved persisted all-models toggle`,
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
165
193
|
if (entry.stored.free.length > 0) {
|
|
166
194
|
entry.reRegister(entry.stored.free);
|
|
167
195
|
_logger.info(
|
|
@@ -171,25 +199,21 @@ function applyFilterToProvider(
|
|
|
171
199
|
_logger.warn(`[pi-free] ${providerId}: no free models available`);
|
|
172
200
|
}
|
|
173
201
|
} else {
|
|
174
|
-
|
|
175
|
-
const allModels =
|
|
176
|
-
entry.stored.all.length > 0 ? entry.stored.all : entry.stored.free;
|
|
177
|
-
if (allModels.length > 0) {
|
|
178
|
-
entry.reRegister(allModels);
|
|
179
|
-
_logger.info(
|
|
180
|
-
`[pi-free] ${providerId}: showing all ${allModels.length} models`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
202
|
+
showAllForProvider(providerId, entry);
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
186
|
-
export function applyGlobalFilter(
|
|
206
|
+
export function applyGlobalFilter(
|
|
207
|
+
_pi: ExtensionAPI,
|
|
208
|
+
freeOnly: boolean,
|
|
209
|
+
options: { force?: boolean } = {},
|
|
210
|
+
): void {
|
|
187
211
|
globalFreeOnly = freeOnly;
|
|
188
212
|
saveConfig({ free_only: freeOnly });
|
|
189
213
|
|
|
190
214
|
for (const [providerId, entry] of providerRegistry) {
|
|
191
215
|
try {
|
|
192
|
-
applyFilterToProvider(providerId, entry, freeOnly);
|
|
216
|
+
applyFilterToProvider(providerId, entry, freeOnly, options.force === true);
|
|
193
217
|
} catch (err) {
|
|
194
218
|
_logger.error(
|
|
195
219
|
`[pi-free] Failed to apply filter to ${providerId}`,
|
package/lib/util.ts
CHANGED
|
@@ -361,7 +361,8 @@ export function mapOpenRouterModel(m: {
|
|
|
361
361
|
contextWindow: m.context_length ?? 4096,
|
|
362
362
|
maxTokens:
|
|
363
363
|
m.max_completion_tokens ?? m.top_provider?.max_completion_tokens ?? 4096,
|
|
364
|
-
|
|
364
|
+
_pricingKnown: true,
|
|
365
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
// =============================================================================
|
|
@@ -484,20 +485,19 @@ export async function fetchOpenAICompatibleModels(
|
|
|
484
485
|
(hasVision ? ["text", "image"] : ["text"]);
|
|
485
486
|
|
|
486
487
|
// Use per-model pricing if the API provides it, otherwise use defaults
|
|
487
|
-
const
|
|
488
|
-
|
|
488
|
+
const hasApiPricing = m.pricing !== undefined;
|
|
489
|
+
const apiInput =
|
|
490
|
+
typeof m.pricing?.prompt === "number" ||
|
|
489
491
|
typeof m.pricing?.prompt === "string"
|
|
490
492
|
? Number(m.pricing.prompt)
|
|
491
|
-
: undefined
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const outputCost =
|
|
495
|
-
(typeof m.pricing?.completion === "number" ||
|
|
493
|
+
: undefined;
|
|
494
|
+
const apiOutput =
|
|
495
|
+
typeof m.pricing?.completion === "number" ||
|
|
496
496
|
typeof m.pricing?.completion === "string"
|
|
497
497
|
? Number(m.pricing.completion)
|
|
498
|
-
: undefined
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
: undefined;
|
|
499
|
+
const inputCost = apiInput ?? defaults.cost?.input ?? 0;
|
|
500
|
+
const outputCost = apiOutput ?? defaults.cost?.output ?? 0;
|
|
501
501
|
|
|
502
502
|
return {
|
|
503
503
|
id: m.id,
|
|
@@ -513,7 +513,8 @@ export async function fetchOpenAICompatibleModels(
|
|
|
513
513
|
contextWindow,
|
|
514
514
|
maxTokens,
|
|
515
515
|
compat: getProxyModelCompat({ id: m.id, name }),
|
|
516
|
-
|
|
516
|
+
_pricingKnown: hasApiPricing,
|
|
517
|
+
} as PiProviderModelConfig & { _pricingKnown?: boolean };
|
|
517
518
|
});
|
|
518
519
|
} catch (error) {
|
|
519
520
|
logger.error(`[${providerId}] Failed to fetch models:`, {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-free",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AI model providers for Pi with free model filtering
|
|
5
|
+
"description": "AI model providers for Pi with free model filtering and dynamic model fetching",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
@@ -9,15 +9,10 @@ import { applyHidden } from "../../config.ts";
|
|
|
9
9
|
import {
|
|
10
10
|
BASE_URL_OPENROUTER,
|
|
11
11
|
DEFAULT_FETCH_TIMEOUT_MS,
|
|
12
|
-
DEFAULT_MIN_SIZE_B,
|
|
13
12
|
PROVIDER_CLINE,
|
|
14
13
|
} from "../../constants.ts";
|
|
15
14
|
import type { ProviderModelConfig } from "../../lib/types.ts";
|
|
16
|
-
import {
|
|
17
|
-
cleanModelName,
|
|
18
|
-
fetchWithRetry,
|
|
19
|
-
isUsableModel,
|
|
20
|
-
} from "../../lib/util.ts";
|
|
15
|
+
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
21
16
|
|
|
22
17
|
interface OpenRouterRaw {
|
|
23
18
|
id: string;
|
|
@@ -74,10 +69,8 @@ export async function fetchClineModels(
|
|
|
74
69
|
|
|
75
70
|
const json = (await response.json()) as { data?: OpenRouterRaw[] };
|
|
76
71
|
|
|
77
|
-
// Filter to usable models (chat-capable
|
|
78
|
-
let usableModels =
|
|
79
|
-
isUsableModel(m.id, DEFAULT_MIN_SIZE_B),
|
|
80
|
-
);
|
|
72
|
+
// Filter to usable models (chat-capable)
|
|
73
|
+
let usableModels = json.data ?? [];
|
|
81
74
|
|
|
82
75
|
// If freeOnly, filter to free models
|
|
83
76
|
if (freeOnly) {
|
|
@@ -119,7 +119,11 @@ async function fetchCrofaiModels(
|
|
|
119
119
|
contextWindow: m.context_length ?? 128_000,
|
|
120
120
|
maxTokens: m.max_completion_tokens ?? 16_384,
|
|
121
121
|
compat: getProxyModelCompat({ id: m.id, name }),
|
|
122
|
-
|
|
122
|
+
_pricingKnown:
|
|
123
|
+
m.pricing?.prompt !== undefined ||
|
|
124
|
+
m.pricing?.completion !== undefined ||
|
|
125
|
+
m.pricing?.cache_prompt !== undefined,
|
|
126
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
123
127
|
});
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
getProxyModelCompat,
|
|
45
45
|
isLikelyReasoningModel,
|
|
46
46
|
} from "../../lib/provider-compat.ts";
|
|
47
|
-
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
47
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
48
48
|
import { fetchWithRetry } from "../../lib/util.ts";
|
|
49
49
|
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
50
50
|
|
|
@@ -136,7 +136,8 @@ async function fetchDeepinfraModels(
|
|
|
136
136
|
contextWindow: meta?.context_length ?? 128_000,
|
|
137
137
|
maxTokens: meta?.max_tokens ?? 16_384,
|
|
138
138
|
compat: getProxyModelCompat({ id: m.id, name }),
|
|
139
|
-
|
|
139
|
+
_pricingKnown: meta?.pricing !== undefined,
|
|
140
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
140
141
|
});
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -163,9 +164,10 @@ export default async function deepinfraProvider(pi: ExtensionAPI) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
// DeepInfra is a trial credit provider — $5 one-time credit, no truly free models.
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
// Use isFreeModel for consistent detection across all providers.
|
|
168
|
+
const freeModels = allModels.filter((m) =>
|
|
169
|
+
isFreeModel({ ...m, provider: PROVIDER_DEEPINFRA }, allModels),
|
|
170
|
+
);
|
|
169
171
|
const stored = { free: freeModels, all: allModels };
|
|
170
172
|
|
|
171
173
|
_logger.info(
|
|
@@ -5,39 +5,60 @@
|
|
|
5
5
|
* standard /models endpoints when the user has configured an API key.
|
|
6
6
|
*
|
|
7
7
|
* Uses a single generic fetch function instead of per-provider boilerplate.
|
|
8
|
-
* Discovery runs concurrently
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Discovery runs concurrently and is awaited by the extension entry point.
|
|
9
|
+
* Pi only flushes provider registrations after async extension startup, so
|
|
10
|
+
* dynamic providers must register before setup returns.
|
|
11
11
|
*
|
|
12
12
|
* Providers handled:
|
|
13
13
|
* - mistral (MISTRAL_API_KEY)
|
|
14
14
|
* - groq (GROQ_API_KEY)
|
|
15
15
|
* - cerebras (CEREBRAS_API_KEY)
|
|
16
16
|
* - xai (XAI_API_KEY)
|
|
17
|
+
* - opencode (OPENCODE_API_KEY from auth.json)
|
|
18
|
+
* - openrouter (OPENROUTER_API_KEY from auth.json)
|
|
19
|
+
* - fastrouter (always discovered, FASTROUTER_API_KEY)
|
|
17
20
|
* - huggingface (HF_TOKEN - optional, special-cased API shape)
|
|
18
21
|
*
|
|
19
22
|
* OpenAI is intentionally skipped per user request.
|
|
20
23
|
*/
|
|
21
24
|
|
|
25
|
+
import type { Api } from "@earendil-works/pi-ai";
|
|
22
26
|
import type {
|
|
23
27
|
ExtensionAPI,
|
|
24
28
|
ProviderModelConfig,
|
|
25
29
|
} from "@earendil-works/pi-coding-agent";
|
|
26
30
|
import {
|
|
27
31
|
getCerebrasApiKey,
|
|
32
|
+
getFastrouterApiKey,
|
|
33
|
+
getFastrouterShowPaid,
|
|
28
34
|
getGroqApiKey,
|
|
29
35
|
getHfToken,
|
|
30
36
|
getMistralApiKey,
|
|
37
|
+
getOpencodeApiKey,
|
|
38
|
+
getOpencodeShowPaid,
|
|
39
|
+
getOpenrouterApiKey,
|
|
40
|
+
getOpenrouterShowPaid,
|
|
31
41
|
getXaiApiKey,
|
|
32
42
|
} from "../../config.ts";
|
|
43
|
+
import { DEFAULT_FETCH_TIMEOUT_MS } from "../../constants.ts";
|
|
33
44
|
import { createLogger } from "../../lib/logger.ts";
|
|
34
45
|
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
35
46
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
47
|
+
import { fetchOpenRouterCompatibleModels } from "../model-fetcher.ts";
|
|
36
48
|
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
37
49
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
50
|
+
import {
|
|
51
|
+
OPENCODE_DYNAMIC_API,
|
|
52
|
+
createOpenCodeSessionTracker,
|
|
53
|
+
createOpenCodeStreamSimple,
|
|
54
|
+
isOpenCodeProvider,
|
|
55
|
+
} from "../opencode-session.ts";
|
|
38
56
|
|
|
39
57
|
const _logger = createLogger("dynamic-built-in");
|
|
40
58
|
|
|
59
|
+
// OpenCode headers must be regenerated for every LLM request.
|
|
60
|
+
const _opencodeSession = createOpenCodeSessionTracker();
|
|
61
|
+
|
|
41
62
|
// =============================================================================
|
|
42
63
|
// Generic Model Fetcher
|
|
43
64
|
// =============================================================================
|
|
@@ -68,7 +89,7 @@ async function fetchModelsFromEndpoint(
|
|
|
68
89
|
|
|
69
90
|
const response = await fetch(url, {
|
|
70
91
|
headers,
|
|
71
|
-
signal: AbortSignal.timeout(opts.timeoutMs ??
|
|
92
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS),
|
|
72
93
|
});
|
|
73
94
|
|
|
74
95
|
if (!response.ok) {
|
|
@@ -101,9 +122,10 @@ async function fetchModelsFromEndpoint(
|
|
|
101
122
|
((m.max_tokens ?? m.max_completion_tokens) as number) ??
|
|
102
123
|
opts.modelDefaults?.maxTokens ??
|
|
103
124
|
16_384,
|
|
125
|
+
_pricingKnown: false as boolean | undefined,
|
|
104
126
|
...opts.modelDefaults,
|
|
105
127
|
...(opts.compat ? { compat: opts.compat } : {}),
|
|
106
|
-
} satisfies ProviderModelConfig;
|
|
128
|
+
} satisfies ProviderModelConfig & { _pricingKnown?: boolean };
|
|
107
129
|
});
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -123,7 +145,7 @@ async function fetchHuggingFaceModels(
|
|
|
123
145
|
|
|
124
146
|
const response = await fetch(
|
|
125
147
|
"https://api-inference.huggingface.co/models?pipeline_tag=text-generation&limit=50",
|
|
126
|
-
{ headers, signal: AbortSignal.timeout(
|
|
148
|
+
{ headers, signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS) },
|
|
127
149
|
);
|
|
128
150
|
|
|
129
151
|
if (!response.ok) {
|
|
@@ -158,12 +180,17 @@ interface DynamicProviderDef {
|
|
|
158
180
|
providerId: string;
|
|
159
181
|
getApiKey: () => string | undefined;
|
|
160
182
|
baseUrl: string;
|
|
161
|
-
api:
|
|
162
|
-
defaultShowPaid: boolean;
|
|
183
|
+
api: Api;
|
|
184
|
+
defaultShowPaid: boolean | (() => boolean);
|
|
163
185
|
/** Optional per-provider compat overrides (e.g., DeepSeek proxy). */
|
|
164
186
|
compat?: ProviderModelConfig["compat"];
|
|
165
187
|
/** Per-model field defaults when the API doesn't expose them. */
|
|
166
188
|
modelDefaults?: Partial<ProviderModelConfig>;
|
|
189
|
+
/**
|
|
190
|
+
* Custom model fetcher (e.g., OpenRouter uses its own pricing-aware fetcher).
|
|
191
|
+
* When not provided, fetchModelsFromEndpoint is used (no pricing, _pricingKnown=false).
|
|
192
|
+
*/
|
|
193
|
+
fetchModels?: (apiKey: string) => Promise<ProviderModelConfig[]>;
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
@@ -196,6 +223,36 @@ const DYNAMIC_PROVIDERS: DynamicProviderDef[] = [
|
|
|
196
223
|
api: "openai-completions",
|
|
197
224
|
defaultShowPaid: false,
|
|
198
225
|
},
|
|
226
|
+
{
|
|
227
|
+
providerId: "opencode",
|
|
228
|
+
getApiKey: getOpencodeApiKey,
|
|
229
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
230
|
+
api: OPENCODE_DYNAMIC_API,
|
|
231
|
+
defaultShowPaid: getOpencodeShowPaid,
|
|
232
|
+
// OpenCode API returns no pricing — _pricingKnown=false, name-based detection
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
providerId: "opencode-go",
|
|
236
|
+
getApiKey: getOpencodeApiKey,
|
|
237
|
+
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
238
|
+
api: OPENCODE_DYNAMIC_API,
|
|
239
|
+
defaultShowPaid: getOpencodeShowPaid,
|
|
240
|
+
// OpenCode Go uses the same OPENCODE_API_KEY and per-request headers
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
providerId: "openrouter",
|
|
244
|
+
getApiKey: getOpenrouterApiKey,
|
|
245
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
246
|
+
api: "openai-completions",
|
|
247
|
+
defaultShowPaid: getOpenrouterShowPaid,
|
|
248
|
+
// OpenRouter returns full pricing — use its dedicated fetcher
|
|
249
|
+
fetchModels: (apiKey) =>
|
|
250
|
+
fetchOpenRouterCompatibleModels({
|
|
251
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
252
|
+
apiKey,
|
|
253
|
+
freeOnly: false,
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
199
256
|
];
|
|
200
257
|
|
|
201
258
|
// =============================================================================
|
|
@@ -210,22 +267,29 @@ async function discoverAndRegister(
|
|
|
210
267
|
let allModels: ProviderModelConfig[];
|
|
211
268
|
|
|
212
269
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
270
|
+
if (config.fetchModels) {
|
|
271
|
+
allModels = await config.fetchModels(apiKey);
|
|
272
|
+
} else {
|
|
273
|
+
allModels = await fetchModelsFromEndpoint({
|
|
274
|
+
baseUrl: config.baseUrl,
|
|
275
|
+
apiKey,
|
|
276
|
+
compat: config.compat,
|
|
277
|
+
modelDefaults: config.modelDefaults,
|
|
278
|
+
timeoutMs: DEFAULT_FETCH_TIMEOUT_MS,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
220
281
|
|
|
221
|
-
// Apply DeepSeek proxy compat to matching models
|
|
282
|
+
// Apply DeepSeek proxy compat to matching models. OpenCode headers are
|
|
283
|
+
// injected per request by createOpenCodeStreamSimple(), not stored here.
|
|
222
284
|
allModels = allModels.map((m) => ({
|
|
223
285
|
...m,
|
|
286
|
+
api: isOpenCodeProvider(config.providerId) ? OPENCODE_DYNAMIC_API : m.api,
|
|
224
287
|
compat: getProxyModelCompat(m) ?? m.compat,
|
|
225
288
|
}));
|
|
226
|
-
} catch {
|
|
289
|
+
} catch (error) {
|
|
227
290
|
_logger.info(
|
|
228
291
|
`[dynamic] ${config.providerId}: discovery failed, Pi keeps its defaults`,
|
|
292
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
229
293
|
);
|
|
230
294
|
return;
|
|
231
295
|
}
|
|
@@ -248,9 +312,10 @@ async function discoverAndRegisterHF(
|
|
|
248
312
|
let allModels: ProviderModelConfig[];
|
|
249
313
|
try {
|
|
250
314
|
allModels = await fetchHuggingFaceModels(apiKey);
|
|
251
|
-
} catch {
|
|
315
|
+
} catch (error) {
|
|
252
316
|
_logger.info(
|
|
253
317
|
"[dynamic] huggingface: discovery failed, Pi keeps its defaults",
|
|
318
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
254
319
|
);
|
|
255
320
|
return;
|
|
256
321
|
}
|
|
@@ -282,6 +347,9 @@ async function registerProvider(
|
|
|
282
347
|
baseUrl: config.baseUrl,
|
|
283
348
|
apiKey,
|
|
284
349
|
api: config.api,
|
|
350
|
+
...(isOpenCodeProvider(config.providerId)
|
|
351
|
+
? { streamSimple: createOpenCodeStreamSimple(_opencodeSession) }
|
|
352
|
+
: {}),
|
|
285
353
|
models: enhanceWithCI(models, config.providerId),
|
|
286
354
|
});
|
|
287
355
|
};
|
|
@@ -289,7 +357,10 @@ async function registerProvider(
|
|
|
289
357
|
// Toggle state
|
|
290
358
|
const toggleState = createToggleState({
|
|
291
359
|
providerId: config.providerId,
|
|
292
|
-
initialShowPaid:
|
|
360
|
+
initialShowPaid:
|
|
361
|
+
typeof config.defaultShowPaid === "function"
|
|
362
|
+
? config.defaultShowPaid()
|
|
363
|
+
: config.defaultShowPaid,
|
|
293
364
|
initialModels: { free: freeModels, all: allModels },
|
|
294
365
|
});
|
|
295
366
|
|
|
@@ -341,16 +412,18 @@ async function registerProvider(
|
|
|
341
412
|
}
|
|
342
413
|
|
|
343
414
|
// =============================================================================
|
|
344
|
-
// Main Entry
|
|
415
|
+
// Main Entry
|
|
345
416
|
// =============================================================================
|
|
346
417
|
|
|
347
418
|
/**
|
|
348
419
|
* Kick off model discovery for all configured providers.
|
|
349
|
-
* Runs each fetch concurrently
|
|
350
|
-
*
|
|
420
|
+
* Runs each fetch concurrently so startup waits for the slowest provider,
|
|
421
|
+
* not `n * provider latency`.
|
|
351
422
|
*
|
|
352
|
-
* Pi
|
|
353
|
-
* function
|
|
423
|
+
* Pi flushes provider registrations after async extension startup completes,
|
|
424
|
+
* so this function must await discovery before returning. Otherwise late
|
|
425
|
+
* pi.registerProvider() calls may not be visible to startup flows such as
|
|
426
|
+
* `pi --list-models` or the initial model picker.
|
|
354
427
|
*/
|
|
355
428
|
export async function setupDynamicBuiltInProviders(
|
|
356
429
|
pi: ExtensionAPI,
|
|
@@ -368,18 +441,41 @@ export async function setupDynamicBuiltInProviders(
|
|
|
368
441
|
fetchers.push(discoverAndRegisterHF(pi, hfKey));
|
|
369
442
|
}
|
|
370
443
|
|
|
444
|
+
// FastRouter: always discovered (model listing needs no auth), but Pi
|
|
445
|
+
// requires a non-empty apiKey/env-var name when replacing a provider's models.
|
|
446
|
+
// Use the real configured key when present; otherwise register with the env
|
|
447
|
+
// var name so startup does not fail for users who have not configured it yet.
|
|
448
|
+
const fastrouterApiKey = getFastrouterApiKey();
|
|
449
|
+
fetchers.push(
|
|
450
|
+
discoverAndRegister(
|
|
451
|
+
pi,
|
|
452
|
+
{
|
|
453
|
+
providerId: "fastrouter",
|
|
454
|
+
getApiKey: getFastrouterApiKey,
|
|
455
|
+
baseUrl: "https://api.fastrouter.ai/api/v1",
|
|
456
|
+
api: "openai-completions",
|
|
457
|
+
defaultShowPaid: getFastrouterShowPaid,
|
|
458
|
+
fetchModels: () =>
|
|
459
|
+
fetchOpenRouterCompatibleModels({
|
|
460
|
+
baseUrl: "https://api.fastrouter.ai/api/v1",
|
|
461
|
+
apiKey: fastrouterApiKey,
|
|
462
|
+
freeOnly: false,
|
|
463
|
+
}),
|
|
464
|
+
},
|
|
465
|
+
fastrouterApiKey ?? "FASTROUTER_API_KEY",
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
|
|
371
469
|
if (fetchers.length === 0) return;
|
|
372
470
|
|
|
373
471
|
_logger.info(
|
|
374
|
-
`[dynamic] Kicking off discovery for ${fetchers.length} providers (
|
|
472
|
+
`[dynamic] Kicking off discovery for ${fetchers.length} providers (concurrent)...`,
|
|
375
473
|
);
|
|
376
474
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
);
|
|
384
|
-
});
|
|
475
|
+
const results = await Promise.allSettled(fetchers);
|
|
476
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
477
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
478
|
+
_logger.info(
|
|
479
|
+
`[dynamic] Discovery complete: ${succeeded} succeeded, ${failed} failed/rejected`,
|
|
480
|
+
);
|
|
385
481
|
}
|
|
@@ -3,17 +3,9 @@
|
|
|
3
3
|
* Consolidates duplicate logic from openrouter.ts and kilo-models.ts
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
8
|
-
DEFAULT_MIN_SIZE_B,
|
|
9
|
-
URL_MODELS_DEV,
|
|
10
|
-
} from "../constants.ts";
|
|
6
|
+
import { DEFAULT_FETCH_TIMEOUT_MS, URL_MODELS_DEV } from "../constants.ts";
|
|
11
7
|
import type { ModelsDevModel, ProviderModelConfig } from "../lib/types.ts";
|
|
12
|
-
import {
|
|
13
|
-
fetchWithRetry,
|
|
14
|
-
isUsableModel,
|
|
15
|
-
mapOpenRouterModel,
|
|
16
|
-
} from "../lib/util.ts";
|
|
8
|
+
import { fetchWithRetry, mapOpenRouterModel } from "../lib/util.ts";
|
|
17
9
|
|
|
18
10
|
interface OpenRouterCompatibleModel {
|
|
19
11
|
id: string;
|
|
@@ -113,9 +105,6 @@ export async function fetchOpenRouterCompatibleModels(
|
|
|
113
105
|
if (prompt !== 0 || completion !== 0) return false;
|
|
114
106
|
}
|
|
115
107
|
|
|
116
|
-
// Filter unusable and too-small models
|
|
117
|
-
if (!isUsableModel(m.id, DEFAULT_MIN_SIZE_B)) return false;
|
|
118
|
-
|
|
119
108
|
return true;
|
|
120
109
|
})
|
|
121
110
|
.map(mapOpenRouterModel);
|