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/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 - use OR logic
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
- // Show all models (paid + free)
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(_pi: ExtensionAPI, freeOnly: boolean): void {
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 inputCost =
488
- (typeof m.pricing?.prompt === "number" ||
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
- defaults.cost?.input ??
493
- 0;
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
- defaults.cost?.output ??
500
- 0;
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.11",
3
+ "version": "2.0.13",
4
4
  "type": "module",
5
- "description": "AI model providers for Pi with free model filtering. Shows only $0 cost models by default. Supports Kilo (free OAuth), Cline (free), NVIDIA (freemium), ZenMux, CrofAI, Ollama Cloud, and more.",
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, size threshold)
78
- let usableModels = (json.data ?? []).filter((m) =>
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
- // All models are marked as paid. When free-only mode is ON, no models are shown.
167
- // Toggle free-only OFF to see all models.
168
- const freeModels: ProviderModelConfig[] = [];
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 with 1s timeout per provider, fire-and-forget
9
- * so extension init never blocks. Pi's built-in defaults serve until
10
- * discovery completes and replaces them.
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 ?? 1_000),
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(1_000) },
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: "openai-completions" | "mistral-conversations" | "anthropic-messages";
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
- allModels = await fetchModelsFromEndpoint({
214
- baseUrl: config.baseUrl,
215
- apiKey,
216
- compat: config.compat,
217
- modelDefaults: config.modelDefaults,
218
- timeoutMs: 1_000,
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: config.defaultShowPaid,
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 — Fire-and-Forget
415
+ // Main Entry
345
416
  // =============================================================================
346
417
 
347
418
  /**
348
419
  * Kick off model discovery for all configured providers.
349
- * Runs each fetch concurrently with a 1s timeout so the worst-case
350
- * wall time is ~1s, not `n * 1s`. Extension init never blocks.
420
+ * Runs each fetch concurrently so startup waits for the slowest provider,
421
+ * not `n * provider latency`.
351
422
  *
352
- * Pi's built-in defaults serve until discovery completes and this
353
- * function replaces them via pi.registerProvider().
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 (1s timeout each, concurrent)...`,
472
+ `[dynamic] Kicking off discovery for ${fetchers.length} providers (concurrent)...`,
375
473
  );
376
474
 
377
- // Fire-and-forget: log results, never block init
378
- void Promise.allSettled(fetchers).then((results) => {
379
- const succeeded = results.filter((r) => r.status === "fulfilled").length;
380
- const failed = results.filter((r) => r.status === "rejected").length;
381
- _logger.info(
382
- `[dynamic] Discovery complete: ${succeeded} succeeded, ${failed} failed/rejected`,
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);