pi-free 2.2.2 → 2.2.4

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.
@@ -18,6 +18,7 @@ import type {
18
18
  ProviderModelConfig,
19
19
  } from "@earendil-works/pi-coding-agent";
20
20
  import {
21
+ getKiloApiKey,
21
22
  getKiloFreeOnly,
22
23
  getKiloShowPaid,
23
24
  PROVIDER_KILO,
@@ -152,7 +153,7 @@ function parseXmlToolCalls(
152
153
  const KILO_PROVIDER_CONFIG = {
153
154
  providerId: PROVIDER_KILO,
154
155
  baseUrl: KILO_GATEWAY_BASE,
155
- apiKey: "$KILO_API_KEY",
156
+ apiKey: getKiloApiKey() || "$KILO_API_KEY",
156
157
  headers: {
157
158
  "X-KILOCODE-EDITORNAME": "Pi",
158
159
  },
@@ -172,14 +173,17 @@ function applyKiloCompat<T extends { compat?: ProviderModelConfig["compat"] }>(
172
173
  }
173
174
 
174
175
  export default async function kiloProvider(pi: ExtensionAPI) {
176
+ // Resolve API key (env var or ~/.pi/free.json)
177
+ const kiloApiKey = getKiloApiKey();
178
+
175
179
  // Try to fetch ALL models at startup (like Cline/OpenRouter)
176
- // If no API key, this will return free models only
180
+ // With API key: returns all models; without: returns free-only
177
181
  let allModels: ProviderModelConfig[] = [];
178
182
  let freeModels: ProviderModelConfig[] = [];
179
183
 
180
184
  try {
181
185
  // Fetch all models (returns free-only if no auth, all if auth available)
182
- allModels = await fetchKiloModels({ freeOnly: false });
186
+ allModels = await fetchKiloModels({ token: kiloApiKey, freeOnly: false });
183
187
  // Derive free list using isFreeModel with allModels for detection
184
188
  freeModels = allModels.filter((m) =>
185
189
  isFreeModel({ ...m, provider: PROVIDER_KILO }, allModels),
@@ -211,11 +215,12 @@ export default async function kiloProvider(pi: ExtensionAPI) {
211
215
  baseReRegister(applyKiloCompat(models));
212
216
 
213
217
  // Register with global toggle system
218
+ const hasKiloKey = !!kiloApiKey;
214
219
  registerWithGlobalToggle(
215
220
  PROVIDER_KILO,
216
221
  stored,
217
222
  reRegister,
218
- !!process.env.KILO_API_KEY,
223
+ hasKiloKey,
219
224
  );
220
225
 
221
226
  // OAuth config for Kilo
@@ -283,14 +288,14 @@ export default async function kiloProvider(pi: ExtensionAPI) {
283
288
  const modelsWithCompat = applyKiloCompat(currentModels);
284
289
  pi.registerProvider(PROVIDER_KILO, {
285
290
  baseUrl: KILO_GATEWAY_BASE,
286
- apiKey: "$KILO_API_KEY",
291
+ apiKey: kiloApiKey || "$KILO_API_KEY",
287
292
  api: "openai-completions" as const,
288
293
  headers: {
289
294
  "X-KILOCODE-EDITORNAME": "Pi",
290
295
  "User-Agent": "pi-free-providers",
291
296
  },
292
297
  models: enhanceWithCI(modelsWithCompat),
293
- oauth: oauthConfig,
298
+ ...(!!kiloApiKey ? {} : { oauth: oauthConfig }),
294
299
  });
295
300
 
296
301
  // Registration complete - models registered silently (use LOG_LEVEL=info to see details)
@@ -138,7 +138,7 @@ export async function fetchOpenRouterModelsWithFree(
138
138
 
139
139
  const free = all.filter((m) => {
140
140
  const cost = m.cost;
141
- return cost && cost.input === 0 && cost.output === 0;
141
+ return cost != null && cost.input === 0 && cost.output === 0;
142
142
  });
143
143
 
144
144
  return { free, all };
@@ -226,8 +226,8 @@ function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
226
226
  }
227
227
 
228
228
  class DeferredAssistantMessageEventStream {
229
- private queue: AssistantMessageEvent[] = [];
230
- private waiting: Array<
229
+ private readonly queue: AssistantMessageEvent[] = [];
230
+ private readonly waiting: Array<
231
231
  (result: IteratorResult<AssistantMessageEvent>) => void
232
232
  > = [];
233
233
  private done = false;
@@ -0,0 +1,525 @@
1
+ /**
2
+ * OpenModel Provider Extension
3
+ *
4
+ * OpenModel (https://openmodel.ai) is a multi-model LLM gateway exposing
5
+ * ~40 models through three protocols:
6
+ * - /v1/messages (Anthropic-compatible) ← we use this
7
+ * - /v1/responses (OpenAI Responses API)
8
+ * - /v1/gemini (Gemini-compatible)
9
+ * - /v1/images (image generation)
10
+ *
11
+ * The /v1/messages endpoint serves DeepSeek, Anthropic (Claude), DashScope
12
+ * (Qwen), Xiaomi (MiMo), Moonshot (Kimi), MiniMax, and Zai models with a
13
+ * standard Anthropic Messages request/response shape.
14
+ *
15
+ * Pricing is exposed via the public, no-auth catalog endpoint
16
+ * GET /web/v1/models?page=N (paginated, 20 per page)
17
+ * Effective cost = `prices.input_cost_per_token × price_multiplier`.
18
+ * A `price_multiplier` of 0 makes a model free.
19
+ *
20
+ * Current DeepSeek V4 Flash Free Event: the `deepseek-v4-flash` model has
21
+ * price_multiplier=0 (input $0 / output $0), 10 RPM / 100K TPM, up to
22
+ * 1M-token context. See https://docs.openmodel.ai/en/docs/event.
23
+ *
24
+ * Setup:
25
+ * OPENMODEL_API_KEY=om-...
26
+ * # or add openmodel_api_key to ~/.pi/free.json
27
+ */
28
+
29
+ import type {
30
+ ExtensionAPI,
31
+ ProviderModelConfig,
32
+ } from "@earendil-works/pi-coding-agent";
33
+ import {
34
+ getOpenmodelApiKey,
35
+ getOpenmodelShowPaid,
36
+ applyHidden,
37
+ } from "../../config.ts";
38
+ import {
39
+ BASE_URL_OPENMODEL,
40
+ DEFAULT_FETCH_TIMEOUT_MS,
41
+ PROVIDER_OPENMODEL,
42
+ } from "../../constants.ts";
43
+ import { createLogger } from "../../lib/logger.ts";
44
+ import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
45
+ import { isLikelyReasoningModel } from "../../lib/provider-compat.ts";
46
+ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
47
+ import { fetchWithRetry } from "../../lib/util.ts";
48
+ import { createReRegister, setupProvider } from "../../provider-helper.ts";
49
+
50
+ const _logger = createLogger("openmodel");
51
+
52
+ // =============================================================================
53
+ // Types
54
+ // =============================================================================
55
+
56
+ /** A model item from the public, no-auth catalog endpoint /web/v1/models. */
57
+ interface OpenModelCatalogItem {
58
+ key: string;
59
+ provider_key: string;
60
+ provider_name: string;
61
+ prices: {
62
+ input_cost_per_token?: number;
63
+ output_cost_per_token?: number;
64
+ cache_read_input_token_cost?: number;
65
+ cache_creation_input_token_cost?: number;
66
+ input_cost_per_image?: number;
67
+ [key: string]: number | undefined;
68
+ };
69
+ max: {
70
+ max_input_tokens?: number;
71
+ max_output_tokens?: number;
72
+ max_tokens?: number;
73
+ };
74
+ supports: {
75
+ supports_vision?: boolean;
76
+ supports_reasoning?: boolean;
77
+ supports_function_calling?: boolean;
78
+ supports_native_streaming?: boolean;
79
+ supports_prompt_caching?: boolean;
80
+ supports_system_messages?: boolean;
81
+ supports_tool_choice?: boolean;
82
+ supports_image_generation?: boolean;
83
+ supports_audio_input?: boolean;
84
+ supports_audio_output?: boolean;
85
+ supports_video_input?: boolean;
86
+ supports_pdf_input?: boolean;
87
+ supports_url_context?: boolean;
88
+ supports_web_search?: boolean;
89
+ supports_assistant_prefill?: boolean;
90
+ supports_computer_use?: boolean;
91
+ supports_parallel_function_calling?: boolean;
92
+ supports_response_schema?: boolean;
93
+ supports_service_tier?: boolean;
94
+ };
95
+ price_multiplier: number;
96
+ }
97
+
98
+ /** A model item from the OpenAI-compatible /v1/models endpoint (auth required). */
99
+ interface OpenModelProtocolItem {
100
+ id: string;
101
+ object?: string;
102
+ created?: number;
103
+ owned_by?: string;
104
+ supported_protocols?: string[];
105
+ }
106
+
107
+ interface OpenModelWebResponse<T> {
108
+ success: boolean;
109
+ meta?: {
110
+ pagination?: {
111
+ page: number;
112
+ pageSize: number;
113
+ total: number;
114
+ totalPages: number;
115
+ };
116
+ };
117
+ data: T[];
118
+ }
119
+
120
+ /**
121
+ * Source of a model in the merged result.
122
+ * "priced" — model has real pricing from /web/v1/models (Route A free detection)
123
+ * "unpriced" — model has no web pricing; conservatively treated as paid
124
+ */
125
+ type ModelSource = "priced" | "unpriced";
126
+
127
+ interface MergedOpenModelModel {
128
+ item: OpenModelCatalogItem;
129
+ source: ModelSource;
130
+ /** True when the model's protocol set includes "messages". */
131
+ supportsMessages: boolean;
132
+ }
133
+
134
+ // =============================================================================
135
+ // Helpers (exported for testing)
136
+ // =============================================================================
137
+
138
+ /** Strip the "free" alias suffix that the catalog uses for promo models. */
139
+ function cleanModelName(id: string): string {
140
+ // The catalog returns ids like "1024-x-1024/gpt-image-1.5" — keep as-is.
141
+ // For chat models like "deepseek-v4-flash", return the id directly.
142
+ return id;
143
+ }
144
+
145
+ function toNumber(value: number | undefined): number {
146
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
147
+ }
148
+
149
+ /**
150
+ * Compute the effective per-token cost for a catalog item.
151
+ * `price_multiplier` of 0 → all costs become 0 (free).
152
+ */
153
+ export function effectiveCost(
154
+ prices: OpenModelCatalogItem["prices"],
155
+ multiplier: number,
156
+ ): { input: number; output: number; cacheRead: number; cacheWrite: number } {
157
+ if (multiplier === 0) {
158
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
159
+ }
160
+ return {
161
+ input: toNumber(prices.input_cost_per_token) * multiplier,
162
+ output: toNumber(prices.output_cost_per_token) * multiplier,
163
+ cacheRead: toNumber(prices.cache_read_input_token_cost) * multiplier,
164
+ cacheWrite: toNumber(prices.cache_creation_input_token_cost) * multiplier,
165
+ };
166
+ }
167
+
168
+ function effectiveInputModalities(
169
+ supports: OpenModelCatalogItem["supports"],
170
+ ): readonly ("text" | "image")[] {
171
+ const hasImage =
172
+ supports.supports_vision ||
173
+ supports.supports_pdf_input ||
174
+ supports.supports_image_generation;
175
+ return hasImage ? (["text", "image"] as const) : (["text"] as const);
176
+ }
177
+
178
+ function effectiveMaxTokens(item: OpenModelCatalogItem): number {
179
+ return (
180
+ item.max.max_output_tokens ??
181
+ item.max.max_tokens ??
182
+ // Anthropic Messages API requires max_tokens; fall back to a safe default.
183
+ 16_384
184
+ );
185
+ }
186
+
187
+ function effectiveContextWindow(item: OpenModelCatalogItem): number {
188
+ return item.max.max_input_tokens ?? 128_000;
189
+ }
190
+
191
+ /**
192
+ * Detect whether a model is a reasoning model.
193
+ * Prefer the explicit `supports.supports_reasoning` flag from the catalog;
194
+ * fall back to name-based heuristics from provider-compat.
195
+ */
196
+ function detectReasoning(item: OpenModelCatalogItem): boolean {
197
+ if (item.supports.supports_reasoning === true) return true;
198
+ return isLikelyReasoningModel({
199
+ id: item.key,
200
+ name: cleanModelName(item.key),
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Build a {@link ProviderModelConfig} from a merged catalog item.
206
+ * Pure function — no I/O — exported for unit testing.
207
+ */
208
+ export function mapOpenModelModel(
209
+ merged: MergedOpenModelModel,
210
+ ): ProviderModelConfig & {
211
+ _pricingKnown?: boolean;
212
+ _freeKnown?: boolean;
213
+ _isFree?: boolean;
214
+ } {
215
+ const { item, source } = merged;
216
+ const reasoning = detectReasoning(item);
217
+ const multiplier = item.price_multiplier;
218
+ const cost = effectiveCost(item.prices, multiplier);
219
+
220
+ // The catalog returns multipliers in [0, 1]. If a model has input+output
221
+ // costs > 0 but multiplier === 0, it is an explicit promo/free model.
222
+ // Only set the authoritative _freeKnown override in two cases:
223
+ // 1. priced + multiplier=0 → bulletproof free (handles edge case where
224
+ // every priced model happens to be cost-0, which would flip
225
+ // isFreeModel to Route B / name-based and hide this model).
226
+ // 2. unpriced → conservatively paid (no data to know).
227
+ // For other priced models (multiplier>0 with real prices, OR missing
228
+ // per-token prices), let isFreeModel's Route A decide from effective cost.
229
+ const isAuthoritativelyFree = source === "priced" && multiplier === 0;
230
+ const isAuthoritativelyPaid = source === "unpriced";
231
+
232
+ return {
233
+ id: item.key,
234
+ name: cleanModelName(item.key),
235
+ reasoning,
236
+ input: effectiveInputModalities(item.supports),
237
+ cost,
238
+ contextWindow: effectiveContextWindow(item),
239
+ maxTokens: effectiveMaxTokens(item),
240
+ _pricingKnown: source === "priced",
241
+ ...(isAuthoritativelyFree && {
242
+ _freeKnown: true as const,
243
+ _isFree: true as const,
244
+ }),
245
+ ...(isAuthoritativelyPaid && {
246
+ _freeKnown: true as const,
247
+ _isFree: false as const,
248
+ }),
249
+ } as ProviderModelConfig & {
250
+ _pricingKnown?: boolean;
251
+ _freeKnown?: boolean;
252
+ _isFree?: boolean;
253
+ };
254
+ }
255
+
256
+ /**
257
+ * Merge the priced catalog (with real pricing) and the protocol list
258
+ * (for protocol filtering), then return only the models whose protocol
259
+ * set includes `"messages"`. Unpriced messages-protocol models are
260
+ * included with source `"unpriced"` so the user can still see them
261
+ * under /toggle-openmodel — we just don't claim they're free.
262
+ *
263
+ * Pure function — exported for unit testing.
264
+ */
265
+ export function mergeOpenModelModels(
266
+ catalog: OpenModelCatalogItem[],
267
+ protocolItems: OpenModelProtocolItem[],
268
+ ): MergedOpenModelModel[] {
269
+ const protocolsById = new Map<string, string[]>();
270
+ for (const item of protocolItems) {
271
+ if (item.id) protocolsById.set(item.id, item.supported_protocols ?? []);
272
+ }
273
+
274
+ const seen = new Set<string>();
275
+ const result: MergedOpenModelModel[] = [];
276
+
277
+ // 1) Priced catalog models, filtered to "messages" support.
278
+ for (const item of catalog) {
279
+ if (!item.key) continue;
280
+ const protocols = protocolsById.get(item.key) ?? [];
281
+ if (!protocols.includes("messages")) continue;
282
+ seen.add(item.key);
283
+ result.push({
284
+ item,
285
+ source: "priced",
286
+ supportsMessages: true,
287
+ });
288
+ }
289
+
290
+ // 2) Unpriced messages-protocol models (e.g. MiniMax, MiMo, Kimi, Qwen).
291
+ for (const item of protocolItems) {
292
+ if (!item.id) continue;
293
+ if (seen.has(item.id)) continue;
294
+ const protocols = item.supported_protocols ?? [];
295
+ if (!protocols.includes("messages")) continue;
296
+
297
+ // Synthesize a minimal catalog item so mapOpenModelModel has
298
+ // uniform input shape. All cost fields default to 0 and
299
+ // reasoning is detected name-based.
300
+ result.push({
301
+ item: {
302
+ key: item.id,
303
+ provider_key: item.owned_by ?? "unknown",
304
+ provider_name: item.owned_by ?? "unknown",
305
+ prices: {},
306
+ max: {},
307
+ supports: {},
308
+ price_multiplier: 1,
309
+ },
310
+ source: "unpriced",
311
+ supportsMessages: true,
312
+ });
313
+ }
314
+
315
+ return result;
316
+ }
317
+
318
+ // =============================================================================
319
+ // Fetch
320
+ // =============================================================================
321
+
322
+ const OPENMODEL_PAGINATION_DELAY_MS = 200;
323
+
324
+ async function fetchOpenModelWebCatalog(
325
+ baseUrl: string,
326
+ ): Promise<OpenModelCatalogItem[]> {
327
+ const items: OpenModelCatalogItem[] = [];
328
+ let cleanBase = baseUrl;
329
+ while (cleanBase.endsWith("/")) cleanBase = cleanBase.slice(0, -1);
330
+ let page = 1;
331
+
332
+ while (true) {
333
+ const url = `${cleanBase}/web/v1/models?page=${page}`;
334
+ _logger.info(`[openmodel] Fetching public catalog page ${page}: ${url}`);
335
+
336
+ const response = await fetchWithRetry(
337
+ url,
338
+ { headers: { Accept: "application/json" } },
339
+ 3,
340
+ 1000,
341
+ DEFAULT_FETCH_TIMEOUT_MS,
342
+ );
343
+
344
+ if (!response.ok) {
345
+ throw new Error(
346
+ `OpenModel web catalog error: ${response.status} ${response.statusText}`,
347
+ );
348
+ }
349
+
350
+ const json =
351
+ (await response.json()) as OpenModelWebResponse<OpenModelCatalogItem>;
352
+ if (json.success !== true || !Array.isArray(json.data)) {
353
+ throw new Error(
354
+ "OpenModel web catalog: unexpected response shape (missing success/data)",
355
+ );
356
+ }
357
+
358
+ items.push(...json.data);
359
+
360
+ const pagination = json.meta?.pagination;
361
+ if (
362
+ !pagination ||
363
+ page >= pagination.totalPages ||
364
+ json.data.length === 0
365
+ ) {
366
+ break;
367
+ }
368
+ page += 1;
369
+ // Be polite to the public endpoint — small delay between pages.
370
+ await new Promise((resolve) =>
371
+ setTimeout(resolve, OPENMODEL_PAGINATION_DELAY_MS),
372
+ );
373
+ }
374
+
375
+ _logger.info(
376
+ `[openmodel] Fetched ${items.length} models from public catalog`,
377
+ );
378
+ return items;
379
+ }
380
+
381
+ async function fetchOpenModelProtocols(
382
+ apiKey: string,
383
+ baseUrl: string,
384
+ ): Promise<OpenModelProtocolItem[]> {
385
+ let cleanBase = baseUrl;
386
+ while (cleanBase.endsWith("/")) cleanBase = cleanBase.slice(0, -1);
387
+ const response = await fetchWithRetry(
388
+ `${cleanBase}/v1/models`,
389
+ {
390
+ headers: {
391
+ Authorization: `Bearer ${apiKey}`,
392
+ Accept: "application/json",
393
+ },
394
+ },
395
+ 3,
396
+ 1000,
397
+ DEFAULT_FETCH_TIMEOUT_MS,
398
+ );
399
+
400
+ if (!response.ok) {
401
+ throw new Error(
402
+ `OpenModel /v1/models error: ${response.status} ${response.statusText}`,
403
+ );
404
+ }
405
+
406
+ const json = (await response.json()) as
407
+ | { data?: OpenModelProtocolItem[] }
408
+ | OpenModelProtocolItem[];
409
+ const items = Array.isArray(json) ? json : (json.data ?? []);
410
+ _logger.info(`[openmodel] Fetched ${items.length} protocol entries`);
411
+ return items;
412
+ }
413
+
414
+ async function fetchOpenModelModels(
415
+ apiKey: string,
416
+ ): Promise<ProviderModelConfig[]> {
417
+ const [catalog, protocols] = await Promise.all([
418
+ fetchOpenModelWebCatalog(BASE_URL_OPENMODEL).catch((error) => {
419
+ _logger.error("[openmodel] Failed to fetch public catalog", {
420
+ error: error instanceof Error ? error.message : String(error),
421
+ });
422
+ return [] as OpenModelCatalogItem[];
423
+ }),
424
+ fetchOpenModelProtocols(apiKey, BASE_URL_OPENMODEL).catch((error) => {
425
+ _logger.error("[openmodel] Failed to fetch /v1/models", {
426
+ error: error instanceof Error ? error.message : String(error),
427
+ });
428
+ return [] as OpenModelProtocolItem[];
429
+ }),
430
+ ]);
431
+
432
+ if (catalog.length === 0 && protocols.length === 0) {
433
+ _logger.warn(
434
+ "[openmodel] Both catalog and protocol fetch failed — no models to register",
435
+ );
436
+ return [];
437
+ }
438
+
439
+ const merged = mergeOpenModelModels(catalog, protocols);
440
+ const mapped = merged.map(mapOpenModelModel);
441
+
442
+ const pricedCount = merged.filter((m) => m.source === "priced").length;
443
+ const unpricedCount = merged.length - pricedCount;
444
+ _logger.info(
445
+ `[openmodel] ${merged.length} messages-protocol models (${pricedCount} priced, ${unpricedCount} unpriced)`,
446
+ );
447
+
448
+ const enriched = await safeEnrichModelsWithModelsDev(mapped, {
449
+ providerId: PROVIDER_OPENMODEL,
450
+ });
451
+ return applyHidden(enriched, PROVIDER_OPENMODEL);
452
+ }
453
+
454
+ // =============================================================================
455
+ // Extension Entry Point
456
+ // =============================================================================
457
+
458
+ export default async function openmodelProvider(pi: ExtensionAPI) {
459
+ const apiKey = getOpenmodelApiKey();
460
+
461
+ if (!apiKey) {
462
+ _logger.info(
463
+ "[openmodel] Skipping — OPENMODEL_API_KEY not set. Sign up at https://openmodel.ai/",
464
+ );
465
+ return;
466
+ }
467
+
468
+ const allModels = await fetchOpenModelModels(apiKey);
469
+
470
+ if (allModels.length === 0) {
471
+ _logger.warn(
472
+ "[openmodel] No models available — verify OPENMODEL_API_KEY is valid and see ~/.pi/free.log for details",
473
+ );
474
+ return;
475
+ }
476
+
477
+ // isFreeModel handles the heavy lifting:
478
+ // - Priced models (multiplier>0): Route A — free if effective cost is 0.
479
+ // - Free-event models (multiplier=0, e.g. deepseek-v4-flash): Route A
480
+ // sees cost 0 → free. This is the headline free model.
481
+ // - Unpriced models: _freeKnown=true, _isFree=false → definitively paid.
482
+ const freeModels = allModels.filter((m) =>
483
+ isFreeModel({ ...m, provider: PROVIDER_OPENMODEL }, allModels),
484
+ );
485
+ const stored = { free: freeModels, all: allModels };
486
+
487
+ _logger.info(
488
+ `[openmodel] Registered ${allModels.length} models (${freeModels.length} free)`,
489
+ );
490
+
491
+ const reRegister = createReRegister(pi, {
492
+ providerId: PROVIDER_OPENMODEL,
493
+ baseUrl: BASE_URL_OPENMODEL,
494
+ apiKey,
495
+ // OpenModel is an Anthropic-protocol gateway — /v1/chat/completions
496
+ // does not exist on it. Without `api: "anthropic-messages"`, the
497
+ // helper defaults to openai-completions and pi-ai POSTs to a 404
498
+ // path. Pin the wire format so it dispatches to the Anthropic SDK.
499
+ api: "anthropic-messages",
500
+ });
501
+
502
+ registerWithGlobalToggle(PROVIDER_OPENMODEL, stored, reRegister, true);
503
+
504
+ setupProvider(
505
+ pi,
506
+ {
507
+ providerId: PROVIDER_OPENMODEL,
508
+ initialShowPaid: getOpenmodelShowPaid(),
509
+ tosUrl: "https://docs.openmodel.ai/en/docs",
510
+ reRegister: (models, _stored) => {
511
+ if (_stored) {
512
+ stored.free = _stored.free;
513
+ stored.all = _stored.all;
514
+ }
515
+ reRegister(models);
516
+ },
517
+ },
518
+ stored,
519
+ );
520
+
521
+ const showPaid = getOpenmodelShowPaid();
522
+ const initialModels =
523
+ showPaid && stored.all.length > 0 ? stored.all : freeModels;
524
+ reRegister(initialModels);
525
+ }