@juvio15/pi-cliproxyapi-provider 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,477 @@
1
+ import type {
2
+ ModelCost,
3
+ ModelCostRates,
4
+ ModelCostTier,
5
+ } from "@earendil-works/pi-ai";
6
+ import { Type } from "typebox";
7
+ import type { Static } from "typebox";
8
+ import { Value } from "typebox/value";
9
+
10
+ export interface CostEntry {
11
+ providerId: string;
12
+ modelId: string;
13
+ standard: ModelCost;
14
+ fast?: ModelCost;
15
+ }
16
+
17
+ export interface CostCatalog {
18
+ exact: Map<string, CostEntry[]>;
19
+ normalized: Map<string, CostEntry[]>;
20
+ }
21
+
22
+ export interface CostFetchOptions {
23
+ url?: string;
24
+ signal?: AbortSignal;
25
+ fetch?: typeof globalThis.fetch;
26
+ /** fetchCosts only: returned instead of an empty catalog when the fetch fails. */
27
+ fallback?: CostCatalog;
28
+ /** fetchCostsConditional only: sent as If-None-Match, so an unchanged catalog answers 304. */
29
+ etag?: string;
30
+ /** Defaults to 10 s. */
31
+ timeoutMs?: number;
32
+ }
33
+
34
+ export type CostFetchResult =
35
+ | { status: "fresh"; costs: CostCatalog; etag?: string }
36
+ | { status: "not-modified" }
37
+ /** `reason` is short enough for a notice: "timeout", "HTTP 503", "invalid catalog" or a network error. */
38
+ | { status: "failed"; reason: string };
39
+
40
+ /** Prices for one catalog id. Null means models.dev has no price for it. */
41
+ export type ResolvedPrice = { standard: ModelCost; fast?: ModelCost } | null;
42
+
43
+ /** Resolved prices by catalog id. */
44
+ export type ResolvedPrices = Record<string, ResolvedPrice>;
45
+
46
+ export const DEFAULT_COSTS_URL = "https://models.dev/api.json";
47
+
48
+ const DEFAULT_COSTS_TIMEOUT_MS = 10_000;
49
+
50
+ /** Every resolved price is stored under this provider id, so lookups use selectEntry's single-variant branch. */
51
+ const RESOLVED_PROVIDER_ID = "resolved";
52
+
53
+ export const ZERO_COST = {
54
+ cacheRead: 0,
55
+ cacheWrite: 0,
56
+ input: 0,
57
+ output: 0,
58
+ } as const;
59
+
60
+ const NAMESPACE =
61
+ /^(?<provider>openai|anthropic|google(?:-vertex)?|xai|deepseek|mistral|cohere|zhipuai|moonshotai|minimax|meta)[/:.]/iu;
62
+
63
+ const NON_ALPHANUMERIC = /[^a-z0-9]/gu;
64
+
65
+ const PROVIDER_PREFERENCES = [
66
+ {
67
+ pattern: /^(?:gpt-|o[134](?:-|$)|chatgpt-|codex-)/u,
68
+ providers: ["openai", "openai-codex", "opencode"],
69
+ },
70
+ { pattern: /^claude-/u, providers: ["anthropic"] },
71
+ { pattern: /^(?:gemini-|gemma-)/u, providers: ["google", "google-vertex"] },
72
+ { pattern: /^grok-/u, providers: ["xai"] },
73
+ { pattern: /^deepseek-/u, providers: ["deepseek"] },
74
+ { pattern: /^mistral-/u, providers: ["mistral"] },
75
+ { pattern: /^command-/u, providers: ["cohere"] },
76
+ { pattern: /^glm-/u, providers: ["zhipuai"] },
77
+ { pattern: /^(?:kimi-|moonshot-)/u, providers: ["moonshotai"] },
78
+ { pattern: /^minimax-/u, providers: ["minimax"] },
79
+ { pattern: /^(?:llama-|muse-)/u, providers: ["meta"] },
80
+ ];
81
+
82
+ const PRICE_ALIASES = new Map<string, readonly string[]>(
83
+ Object.entries({
84
+ "gemini-3-flash-agent": ["gemini-3.5-flash"],
85
+ "gemini-3.1-pro-low": ["gemini-3.1-pro-preview"],
86
+ "gemini-3.6-flash-high": ["gemini-3.6-flash"],
87
+ "gemini-pro-agent": ["gemini-3.1-pro-preview"],
88
+ "grok-3-mini": ["xai/grok-3-mini"],
89
+ "grok-composer-2.5-fast": ["grok-4.3"],
90
+ })
91
+ );
92
+
93
+ const Rate = Type.Optional(Type.Number());
94
+
95
+ const RateFields = {
96
+ cacheRead: Rate,
97
+ cacheWrite: Rate,
98
+ cache_read: Rate,
99
+ cache_write: Rate,
100
+ input: Rate,
101
+ output: Rate,
102
+ };
103
+
104
+ const RatesSchema = Type.Object(RateFields);
105
+
106
+ const TierSchema = Type.Object({
107
+ ...RateFields,
108
+ inputTokensAbove: Rate,
109
+ tier: Type.Optional(
110
+ Type.Object({ size: Rate, type: Type.Optional(Type.String()) })
111
+ ),
112
+ });
113
+
114
+ const CostSchema = Type.Object({
115
+ ...RateFields,
116
+ context_over_200k: Type.Optional(TierSchema),
117
+ tiers: Type.Optional(Type.Array(TierSchema)),
118
+ });
119
+
120
+ const ModelEntrySchema = Type.Object({
121
+ cost: Type.Optional(CostSchema),
122
+ experimental: Type.Optional(
123
+ Type.Object({
124
+ modes: Type.Optional(
125
+ Type.Object({
126
+ fast: Type.Optional(Type.Object({ cost: Type.Optional(CostSchema) })),
127
+ })
128
+ ),
129
+ })
130
+ ),
131
+ });
132
+
133
+ const ProviderSchema = Type.Object({
134
+ models: Type.Record(Type.String(), Type.Unknown()),
135
+ });
136
+
137
+ const NetworkErrorSchema = Type.Object({
138
+ cause: Type.Object({ code: Type.String() }),
139
+ });
140
+
141
+ /** models.dev api.json. Each provider and model is checked on its own, so one odd entry never hides the rest. */
142
+ export const ModelsDevCatalogSchema = Type.Record(
143
+ Type.String(),
144
+ Type.Unknown()
145
+ );
146
+
147
+ export type ModelsDevCatalog = Static<typeof ModelsDevCatalogSchema>;
148
+
149
+ type Rates = Static<typeof RatesSchema>;
150
+
151
+ type Tier = Static<typeof TierSchema>;
152
+
153
+ type Cost = Static<typeof CostSchema>;
154
+
155
+ const readRates = (
156
+ source: Rates,
157
+ fallback: ModelCostRates
158
+ ): ModelCostRates => ({
159
+ cacheRead: source.cacheRead ?? source.cache_read ?? fallback.cacheRead,
160
+ cacheWrite: source.cacheWrite ?? source.cache_write ?? fallback.cacheWrite,
161
+ input: source.input ?? fallback.input,
162
+ output: source.output ?? fallback.output,
163
+ });
164
+
165
+ const parseCost = (source: Cost | undefined): ModelCost | undefined => {
166
+ if (!source || (source.input === undefined && source.output === undefined)) {
167
+ return undefined;
168
+ }
169
+
170
+ const cost: ModelCost = readRates(source, ZERO_COST);
171
+ const tiers = new Map<number, ModelCostTier>();
172
+
173
+ const addTier = (tier: Tier | undefined, fallbackThreshold?: number) => {
174
+ if (!tier) {
175
+ return;
176
+ }
177
+
178
+ const descriptor = tier.tier;
179
+
180
+ if (descriptor?.type !== undefined && descriptor.type !== "context") {
181
+ return;
182
+ }
183
+
184
+ const threshold =
185
+ tier.inputTokensAbove ?? descriptor?.size ?? fallbackThreshold;
186
+
187
+ if (threshold === undefined || threshold <= 0) {
188
+ return;
189
+ }
190
+
191
+ tiers.set(threshold, {
192
+ ...readRates(tier, cost),
193
+ inputTokensAbove: threshold,
194
+ });
195
+ };
196
+
197
+ for (const tier of source.tiers ?? []) {
198
+ addTier(tier);
199
+ }
200
+
201
+ if (tiers.size === 0) {
202
+ addTier(source.context_over_200k, 200_000);
203
+ }
204
+
205
+ if (tiers.size > 0) {
206
+ cost.tiers = [...tiers.values()].toSorted(
207
+ (left, right) => left.inputTokensAbove - right.inputTokensAbove
208
+ );
209
+ }
210
+
211
+ return cost;
212
+ };
213
+
214
+ const stripNamespace = (modelId: string): string =>
215
+ modelId.trim().toLowerCase().replace(NAMESPACE, "");
216
+
217
+ const normalizedKey = (modelId: string): string =>
218
+ stripNamespace(modelId).replaceAll(NON_ALPHANUMERIC, "");
219
+
220
+ const addEntry = (
221
+ index: Map<string, CostEntry[]>,
222
+ key: string,
223
+ entry: CostEntry
224
+ ): void => {
225
+ if (!key) {
226
+ return;
227
+ }
228
+
229
+ const entries = index.get(key) ?? [];
230
+
231
+ if (
232
+ entries.some(
233
+ (candidate) =>
234
+ candidate.modelId === entry.modelId &&
235
+ candidate.providerId === entry.providerId
236
+ )
237
+ ) {
238
+ return;
239
+ }
240
+
241
+ entries.push(entry);
242
+ index.set(key, entries);
243
+ };
244
+
245
+ export const parseCosts = (payload: ModelsDevCatalog): CostCatalog => {
246
+ const catalog: CostCatalog = { exact: new Map(), normalized: new Map() };
247
+
248
+ for (const [providerId, provider] of Object.entries(payload)) {
249
+ if (!Value.Check(ProviderSchema, provider)) {
250
+ continue;
251
+ }
252
+
253
+ for (const [modelId, model] of Object.entries(provider.models)) {
254
+ if (!Value.Check(ModelEntrySchema, model)) {
255
+ continue;
256
+ }
257
+
258
+ const standard = parseCost(model.cost);
259
+
260
+ if (!standard) {
261
+ continue;
262
+ }
263
+
264
+ const fast = parseCost(model.experimental?.modes?.fast?.cost);
265
+ const entry = { fast, modelId, providerId, standard };
266
+ const rawId = modelId.trim().toLowerCase();
267
+
268
+ for (const key of new Set([rawId, stripNamespace(rawId)])) {
269
+ addEntry(catalog.exact, key, entry);
270
+ }
271
+
272
+ addEntry(catalog.normalized, normalizedKey(rawId), entry);
273
+ }
274
+ }
275
+
276
+ return catalog;
277
+ };
278
+
279
+ const selectEntry = (
280
+ entries: readonly CostEntry[],
281
+ modelId: string
282
+ ): CostEntry | undefined => {
283
+ const raw = modelId.trim().toLowerCase();
284
+ const namespace = raw.match(NAMESPACE)?.groups?.provider;
285
+
286
+ const family = PROVIDER_PREFERENCES.find(({ pattern }) =>
287
+ pattern.test(stripNamespace(raw))
288
+ );
289
+
290
+ for (const provider of [namespace, ...(family?.providers ?? [])]) {
291
+ const match = entries.find((entry) => entry.providerId === provider);
292
+
293
+ if (match) {
294
+ return match;
295
+ }
296
+ }
297
+
298
+ const variants = new Set(
299
+ entries.map((entry) =>
300
+ JSON.stringify({ fast: entry.fast, standard: entry.standard })
301
+ )
302
+ );
303
+
304
+ if (variants.size === 1) {
305
+ return entries.toSorted((left, right) =>
306
+ left.providerId.localeCompare(right.providerId)
307
+ )[0];
308
+ }
309
+
310
+ return undefined;
311
+ };
312
+
313
+ const findCostEntry = (
314
+ modelId: string,
315
+ catalog: CostCatalog
316
+ ): CostEntry | undefined => {
317
+ const rawId = modelId.trim().toLowerCase();
318
+
319
+ for (const id of [rawId, ...(PRICE_ALIASES.get(rawId) ?? [])]) {
320
+ const entry =
321
+ selectEntry(catalog.exact.get(id) ?? [], id) ??
322
+ selectEntry(catalog.exact.get(stripNamespace(id)) ?? [], id) ??
323
+ selectEntry(catalog.normalized.get(normalizedKey(id)) ?? [], id);
324
+
325
+ if (entry) {
326
+ return entry;
327
+ }
328
+ }
329
+
330
+ return undefined;
331
+ };
332
+
333
+ export const matchModelCost = (
334
+ modelId: string,
335
+ catalog: CostCatalog,
336
+ fast = false
337
+ ): ModelCost => {
338
+ const entry = findCostEntry(modelId, catalog);
339
+
340
+ if (!entry) {
341
+ return { ...ZERO_COST };
342
+ }
343
+
344
+ return structuredClone(fast && entry.fast ? entry.fast : entry.standard);
345
+ };
346
+
347
+ /** Resolves each id once against the full catalog, so only these few prices need to be kept. */
348
+ export const resolvePrices = (
349
+ ids: readonly string[],
350
+ catalog: CostCatalog
351
+ ): ResolvedPrices => {
352
+ const priceOf = (id: string): ResolvedPrice => {
353
+ const entry = findCostEntry(id, catalog);
354
+
355
+ if (!entry) {
356
+ return null;
357
+ }
358
+
359
+ return structuredClone(
360
+ entry.fast
361
+ ? { fast: entry.fast, standard: entry.standard }
362
+ : { standard: entry.standard }
363
+ );
364
+ };
365
+
366
+ return Object.fromEntries(ids.map((id) => [id, priceOf(id)]));
367
+ };
368
+
369
+ /** A catalog that prices exactly the resolved ids. Each id has one entry, so matchModelCost returns it as is. */
370
+ export const costCatalogFromPrices = (prices: ResolvedPrices): CostCatalog => {
371
+ const catalog: CostCatalog = { exact: new Map(), normalized: new Map() };
372
+
373
+ for (const [modelId, price] of Object.entries(prices)) {
374
+ if (price) {
375
+ addEntry(catalog.exact, modelId.trim().toLowerCase(), {
376
+ ...price,
377
+ modelId,
378
+ providerId: RESOLVED_PROVIDER_ID,
379
+ });
380
+ }
381
+ }
382
+
383
+ return catalog;
384
+ };
385
+
386
+ const isCatalog = (payload: unknown): payload is ModelsDevCatalog =>
387
+ Value.Check(ModelsDevCatalogSchema, payload) &&
388
+ Object.values(payload).some((provider) =>
389
+ Value.Check(ProviderSchema, provider)
390
+ );
391
+
392
+ /**
393
+ * Fetches the models.dev catalog, sending `etag` as If-None-Match. Reports a
394
+ * models.dev failure as a result instead of throwing. Throws only when the
395
+ * caller's own signal aborts.
396
+ */
397
+ export const fetchCostsConditional = async (
398
+ options: CostFetchOptions = {}
399
+ ): Promise<CostFetchResult> => {
400
+ const timeout = AbortSignal.timeout(
401
+ options.timeoutMs ?? DEFAULT_COSTS_TIMEOUT_MS
402
+ );
403
+
404
+ const signal = options.signal
405
+ ? AbortSignal.any([options.signal, timeout])
406
+ : timeout;
407
+
408
+ try {
409
+ const response = await (options.fetch ?? globalThis.fetch)(
410
+ options.url ?? DEFAULT_COSTS_URL,
411
+ {
412
+ headers: options.etag ? { "If-None-Match": options.etag } : {},
413
+ signal,
414
+ }
415
+ );
416
+
417
+ if (response.status === 304) {
418
+ await response.body?.cancel();
419
+
420
+ return { status: "not-modified" };
421
+ }
422
+
423
+ if (!response.ok) {
424
+ await response.body?.cancel();
425
+
426
+ return { reason: `HTTP ${response.status}`, status: "failed" };
427
+ }
428
+
429
+ const payload: unknown = await response.json();
430
+
431
+ if (!isCatalog(payload)) {
432
+ return { reason: "invalid catalog", status: "failed" };
433
+ }
434
+
435
+ return {
436
+ costs: parseCosts(payload),
437
+ etag: response.headers.get("etag") ?? undefined,
438
+ status: "fresh",
439
+ };
440
+ } catch (error) {
441
+ if (options.signal?.aborted) {
442
+ throw error;
443
+ }
444
+
445
+ if (timeout.aborted) {
446
+ return { reason: "timeout", status: "failed" };
447
+ }
448
+
449
+ if (error instanceof SyntaxError) {
450
+ return { reason: "invalid catalog", status: "failed" };
451
+ }
452
+
453
+ // Node's fetch names the network failure, such as ENOTFOUND, in the cause's code.
454
+ return {
455
+ reason: Value.Check(NetworkErrorSchema, error)
456
+ ? `network error ${error.cause.code}`
457
+ : "network error",
458
+ status: "failed",
459
+ };
460
+ }
461
+ };
462
+
463
+ /** The whole catalog, or `fallback` (empty by default) when models.dev fails. For callers without a price cache. */
464
+ export const fetchCosts = async (
465
+ options: CostFetchOptions = {}
466
+ ): Promise<CostCatalog> => {
467
+ const result = await fetchCostsConditional({
468
+ fetch: options.fetch,
469
+ signal: options.signal,
470
+ timeoutMs: options.timeoutMs,
471
+ url: options.url,
472
+ });
473
+
474
+ return result.status === "fresh"
475
+ ? result.costs
476
+ : (options.fallback ?? { exact: new Map(), normalized: new Map() });
477
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Public core API (no filesystem or ambient credential reads).
3
+ * resolveEndpoints(baseUrl: string): Endpoints
4
+ * resolveConnection(env: Readonly<Record<string, string | undefined>>, file?: ConnectionValues, stored?: ConnectionValues): ResolvedConnection | null
5
+ * resolveConnectionSources(env: Readonly<Record<string, string | undefined>>, file?: ConnectionValues, stored?: ConnectionValues): ConnectionSources
6
+ * selectModelApi(id: string, families?: ModelFamilies): CliproxyApi
7
+ * fetchCatalog(options: CatalogFetchOptions): Promise<CatalogModel[]>
8
+ * fetchCosts(options?: CostFetchOptions): Promise<CostCatalog>
9
+ * fetchCostsConditional(options?: CostFetchOptions): Promise<CostFetchResult>
10
+ * resolvePrices(ids: readonly string[], catalog: CostCatalog): ResolvedPrices
11
+ * costCatalogFromPrices(prices: ResolvedPrices): CostCatalog
12
+ * parseCosts(payload: ModelsDevCatalog): CostCatalog
13
+ * matchModelCost(modelId: string, catalog: CostCatalog, fast?: boolean): ModelCost
14
+ * mapCatalog(entries: readonly CatalogModel[], options: MappingOptions): MappedCatalog
15
+ * mergeCatalog(previous: readonly CliproxyModel[], fresh: readonly CliproxyModel[], options?: MergeOptions): CliproxyModel[]
16
+ * wrapCliproxyStreams(streams: ProviderStreams, settings: PayloadSettings): ProviderStreams
17
+ * createCliproxyProvider(options: CliproxyProviderOptions): Provider<CliproxyApi>
18
+ * quotaResetFromError(message: string, now?: number): number | undefined
19
+ * startOAuth(options: StartOAuthOptions): Promise<OAuthStart>
20
+ * submitOAuthCallback(options: SubmitOAuthCallbackOptions): Promise<OAuthStatus>
21
+ * pollOAuthStatus(options: PollOAuthStatusOptions): Promise<OAuthStatus>
22
+ * fetchUsage(options: UsageFetchOptions): Promise<ProviderUsage[]>
23
+ * resetCooldowns(options: ResetCooldownsOptions): Promise<string[]>
24
+ * unavailableModels(usage: readonly ProviderUsage[], exhausted: Readonly<Record<string, number>>, now?: number): Map<string, number>
25
+ */
26
+ // pi maps only some pi-ai entry points into extensions. compat re-exports the same lazy API factories.
27
+ import {
28
+ anthropicMessagesApi,
29
+ openAICompletionsApi,
30
+ openAIResponsesApi,
31
+ } from "@earendil-works/pi-ai/compat";
32
+ import { createProvider } from "@earendil-works/pi-ai/models";
33
+ import type { Provider } from "@earendil-works/pi-ai/models";
34
+
35
+ import { fetchCatalog, mapCatalog, mergeCatalog } from "./catalog.ts";
36
+ import type { CliproxyApi, CliproxyModel, MappingOptions } from "./catalog.ts";
37
+ import { resolveEndpoints } from "./connection.ts";
38
+ import { fetchCosts } from "./costs.ts";
39
+ import { wrapCliproxyStreams } from "./streams.ts";
40
+
41
+ export {
42
+ CatalogHttpError,
43
+ DEFAULT_MODEL_STALE_TTL_MS,
44
+ MODEL_API_BY_FAMILY,
45
+ fetchCatalog,
46
+ mapCatalog,
47
+ mergeCatalog,
48
+ selectModelApi,
49
+ type CatalogFetchOptions,
50
+ type CatalogModel,
51
+ type CliproxyApi,
52
+ type CliproxyModel,
53
+ type MappedCatalog,
54
+ type MappingOptions,
55
+ type MergeOptions,
56
+ type ModelFamilies,
57
+ type ThoughtSummary,
58
+ } from "./catalog.ts";
59
+
60
+ export {
61
+ DEFAULT_BASE_URL,
62
+ resolveConnection,
63
+ resolveConnectionSources,
64
+ resolveEndpoints,
65
+ type ConnectionSources,
66
+ type ConnectionValues,
67
+ type Endpoints,
68
+ type ResolvedConnection,
69
+ type ValueSource,
70
+ } from "./connection.ts";
71
+
72
+ export {
73
+ DEFAULT_COSTS_URL,
74
+ ZERO_COST,
75
+ costCatalogFromPrices,
76
+ fetchCosts,
77
+ fetchCostsConditional,
78
+ matchModelCost,
79
+ parseCosts,
80
+ resolvePrices,
81
+ type CostCatalog,
82
+ type CostEntry,
83
+ type CostFetchOptions,
84
+ type CostFetchResult,
85
+ type ResolvedPrice,
86
+ type ResolvedPrices,
87
+ } from "./costs.ts";
88
+
89
+ export {
90
+ pollOAuthStatus,
91
+ startOAuth,
92
+ submitOAuthCallback,
93
+ type OAuthRequestOptions,
94
+ type OAuthStart,
95
+ type OAuthStatus,
96
+ type OAuthStatusValue,
97
+ type PollOAuthStatusOptions,
98
+ type StartOAuthOptions,
99
+ type SubmitOAuthCallbackOptions,
100
+ } from "./oauth.ts";
101
+
102
+ export { wrapCliproxyStreams, type PayloadSettings } from "./streams.ts";
103
+
104
+ export {
105
+ QUOTA_FALLBACK_MS,
106
+ fetchUsage,
107
+ quotaResetFromError,
108
+ resetCooldowns,
109
+ unavailableModels,
110
+ windowLabel,
111
+ type ProviderUsage,
112
+ type ResetCooldownsOptions,
113
+ type UsageFetchOptions,
114
+ type UsageWindow,
115
+ } from "./usage.ts";
116
+
117
+ export interface CliproxyProviderOptions extends MappingOptions {
118
+ apiKey: string;
119
+ providerName?: string;
120
+ /** Static baseline models, available before the first refresh. */
121
+ models?: readonly CliproxyModel[];
122
+ fetch?: typeof globalThis.fetch;
123
+ costsUrl?: string;
124
+ staleTtlMs?: number;
125
+ }
126
+
127
+ export const createCliproxyProvider = (
128
+ options: CliproxyProviderOptions
129
+ ): Provider<CliproxyApi> => {
130
+ if (!options.apiKey.trim()) {
131
+ throw new Error("CLIProxyAPI apiKey is empty");
132
+ }
133
+
134
+ const endpoints = resolveEndpoints(options.baseUrl);
135
+ const id = options.providerId ?? "cliproxyapi";
136
+
137
+ const provider: Provider<CliproxyApi> = createProvider<CliproxyApi>({
138
+ api: {
139
+ "anthropic-messages": wrapCliproxyStreams(
140
+ anthropicMessagesApi(),
141
+ options
142
+ ),
143
+ "openai-completions": wrapCliproxyStreams(
144
+ openAICompletionsApi(),
145
+ options
146
+ ),
147
+ "openai-responses": wrapCliproxyStreams(openAIResponsesApi(), options),
148
+ },
149
+ auth: {
150
+ apiKey: {
151
+ name: "CLIProxyAPI API key",
152
+ resolve: () => Promise.resolve({ auth: { apiKey: options.apiKey } }),
153
+ },
154
+ },
155
+ baseUrl: endpoints.rootUrl,
156
+ fetchModels: async ({ signal, baseUrl }) => {
157
+ const root = baseUrl ?? options.baseUrl;
158
+
159
+ const [entries, costs] = await Promise.all([
160
+ fetchCatalog({
161
+ apiKey: options.apiKey,
162
+ baseUrl: root,
163
+ fetch: options.fetch,
164
+ signal,
165
+ }),
166
+ options.costs ??
167
+ fetchCosts({ fetch: options.fetch, signal, url: options.costsUrl }),
168
+ ]);
169
+
170
+ const mapped = mapCatalog(entries, { ...options, baseUrl: root, costs });
171
+
172
+ const previous = provider
173
+ .getModels()
174
+ .filter(
175
+ (model): model is CliproxyModel =>
176
+ "thoughtSummary" in model && "supportsFast" in model
177
+ );
178
+
179
+ return mergeCatalog(previous, mapped.models, {
180
+ staleTtlMs: options.staleTtlMs,
181
+ });
182
+ },
183
+ id,
184
+ models: options.models ?? [],
185
+ name: options.providerName ?? "CLIProxyAPI",
186
+ });
187
+
188
+ return provider;
189
+ };