proxitor 0.2.0 → 0.3.0

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.
Files changed (52) hide show
  1. package/README.md +221 -81
  2. package/dist/add.cjs +139 -0
  3. package/dist/add.cjs.map +1 -0
  4. package/dist/add.mjs +138 -0
  5. package/dist/add.mjs.map +1 -0
  6. package/dist/browse.cjs +88 -0
  7. package/dist/browse.cjs.map +1 -0
  8. package/dist/browse.mjs +87 -0
  9. package/dist/browse.mjs.map +1 -0
  10. package/dist/cli.cjs +148 -25
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.mjs +149 -26
  13. package/dist/cli.mjs.map +1 -1
  14. package/dist/config.cjs +68 -0
  15. package/dist/config.cjs.map +1 -0
  16. package/dist/config.mjs +45 -0
  17. package/dist/config.mjs.map +1 -0
  18. package/dist/config2.cjs +75 -0
  19. package/dist/config2.cjs.map +1 -0
  20. package/dist/config2.mjs +74 -0
  21. package/dist/config2.mjs.map +1 -0
  22. package/dist/edit.cjs +82 -0
  23. package/dist/edit.cjs.map +1 -0
  24. package/dist/edit.mjs +81 -0
  25. package/dist/edit.mjs.map +1 -0
  26. package/dist/index.cjs +2 -0
  27. package/dist/index.d.cts +223 -53
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +223 -53
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +2 -2
  32. package/dist/list.cjs +33 -0
  33. package/dist/list.cjs.map +1 -0
  34. package/dist/list.mjs +31 -0
  35. package/dist/list.mjs.map +1 -0
  36. package/dist/providers.cjs +376 -0
  37. package/dist/providers.cjs.map +1 -0
  38. package/dist/providers.mjs +279 -0
  39. package/dist/providers.mjs.map +1 -0
  40. package/dist/proxy.cjs +128 -8
  41. package/dist/proxy.cjs.map +1 -1
  42. package/dist/proxy.mjs +99 -9
  43. package/dist/proxy.mjs.map +1 -1
  44. package/dist/remove.cjs +38 -0
  45. package/dist/remove.cjs.map +1 -0
  46. package/dist/remove.mjs +37 -0
  47. package/dist/remove.mjs.map +1 -0
  48. package/dist/validate.cjs +26 -0
  49. package/dist/validate.cjs.map +1 -0
  50. package/dist/validate.mjs +25 -0
  51. package/dist/validate.mjs.map +1 -0
  52. package/package.json +10 -3
@@ -0,0 +1,376 @@
1
+ const require_proxy = require("./proxy.cjs");
2
+ let node_fs = require("node:fs");
3
+ let node_os = require("node:os");
4
+ let node_path = require("node:path");
5
+ let _clack_prompts = require("@clack/prompts");
6
+ _clack_prompts = require_proxy.__toESM(_clack_prompts, 1);
7
+ //#region src/openrouter/client.ts
8
+ const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
9
+ var OpenRouterClientError = class extends Error {
10
+ status;
11
+ constructor(status, message) {
12
+ super(`OpenRouter API error (${status}): ${message}`);
13
+ this.name = "OpenRouterClientError";
14
+ this.status = status;
15
+ }
16
+ };
17
+ /** HTTP client for OpenRouter REST endpoints. Auth header is only sent when apiKey is non-empty. */
18
+ var OpenRouterClient = class {
19
+ apiKey;
20
+ baseUrl;
21
+ constructor(apiKey, baseUrl) {
22
+ this.apiKey = apiKey;
23
+ this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;
24
+ }
25
+ async get(path) {
26
+ const url = `${this.baseUrl}${path}`;
27
+ const res = await fetch(url, { headers: { ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {} } });
28
+ if (!res.ok) {
29
+ const body = await res.text().catch(() => "");
30
+ throw new OpenRouterClientError(res.status, body || res.statusText);
31
+ }
32
+ return res.json();
33
+ }
34
+ };
35
+ //#endregion
36
+ //#region src/openrouter/cache.ts
37
+ const CACHE_DIR = (0, node_path.join)((0, node_os.homedir)(), ".proxitor", "cache");
38
+ /** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */
39
+ function readCache(key, ttlMs) {
40
+ const path = (0, node_path.join)(CACHE_DIR, `${key}.json`);
41
+ if (!(0, node_fs.existsSync)(path)) return null;
42
+ try {
43
+ const entry = JSON.parse((0, node_fs.readFileSync)(path, "utf-8"));
44
+ if (Date.now() - entry.fetchedAt > ttlMs) return null;
45
+ return entry.data;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function writeCache(key, data) {
51
+ (0, node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
52
+ const entry = {
53
+ fetchedAt: Date.now(),
54
+ data
55
+ };
56
+ (0, node_fs.writeFileSync)((0, node_path.join)(CACHE_DIR, `${key}.json`), JSON.stringify(entry));
57
+ }
58
+ //#endregion
59
+ //#region src/openrouter/models.ts
60
+ const CACHE_KEY$1 = "models";
61
+ const CACHE_TTL$1 = 3600 * 1e3;
62
+ async function fetchModels(client) {
63
+ const cached = readCache(CACHE_KEY$1, CACHE_TTL$1);
64
+ if (cached) return cached;
65
+ const response = await client.get("/models");
66
+ writeCache(CACHE_KEY$1, response.data);
67
+ return response.data;
68
+ }
69
+ /** `"anthropic/claude-sonnet-4"` → `"anthropic"` */
70
+ function parseModelAuthor(modelId) {
71
+ return modelId.split("/")[0] ?? "";
72
+ }
73
+ /** `"anthropic/claude-sonnet-4"` → `"claude-sonnet-4"` */
74
+ function parseModelSlug(modelId) {
75
+ return modelId.split("/").slice(1).join("/");
76
+ }
77
+ /** `"0.000003"` → `"$3.00"`, `"0"` → `"free"` */
78
+ function formatPrice(pricePerToken) {
79
+ const per1M = Number.parseFloat(pricePerToken) * 1e6;
80
+ if (per1M === 0) return "free";
81
+ if (per1M < .01) return `$${per1M.toFixed(4)}`;
82
+ return `$${per1M.toFixed(2)}`;
83
+ }
84
+ //#endregion
85
+ //#region src/commands/config/format.ts
86
+ function formatPricing(prompt, completion) {
87
+ const fmt = (perToken) => {
88
+ const per1M = Number.parseFloat(perToken) * 1e6;
89
+ if (per1M === 0) return "free";
90
+ if (per1M < .01) return `$${per1M.toFixed(4)}`;
91
+ return `$${per1M.toFixed(2)}`;
92
+ };
93
+ return `${fmt(prompt)} / ${fmt(completion)}`;
94
+ }
95
+ /** `200000` → `"200k"`, `1000000` → `"1.0M"` */
96
+ function formatContextLength(tokens) {
97
+ if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
98
+ if (tokens >= 1e3) return `${Math.round(tokens / 1e3)}k`;
99
+ return `${tokens}`;
100
+ }
101
+ /** `1137` → `"1.1s"`, `null` → `"N/A"` */
102
+ function formatLatency(ms) {
103
+ if (ms === null) return "N/A";
104
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
105
+ return `${(ms / 1e3).toFixed(1)}s`;
106
+ }
107
+ function formatThroughput(tokensPerSec) {
108
+ if (tokensPerSec === null) return "N/A";
109
+ return `${tokensPerSec.toFixed(0)} t/s`;
110
+ }
111
+ function formatModelLabel(m) {
112
+ return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`;
113
+ }
114
+ function formatModelHint(m) {
115
+ const parts = [`out ${formatPrice(m.pricing.completion)}`];
116
+ if (m.pricing.input_cache_read && m.pricing.input_cache_read !== "0") parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`);
117
+ return parts.join(" · ");
118
+ }
119
+ //#endregion
120
+ //#region src/openrouter/endpoints.ts
121
+ async function fetchModelEndpoints(client, author, slug) {
122
+ return (await client.get(`/models/${author}/${slug}/endpoints`)).data.endpoints ?? [];
123
+ }
124
+ function getUniqueProviders(endpoints) {
125
+ const seen = /* @__PURE__ */ new Set();
126
+ const result = [];
127
+ for (const ep of endpoints) {
128
+ if (seen.has(ep.tag)) continue;
129
+ seen.add(ep.tag);
130
+ result.push({
131
+ tag: ep.tag,
132
+ providerName: ep.provider_name
133
+ });
134
+ }
135
+ result.sort((a, b) => a.providerName.localeCompare(b.providerName));
136
+ return result;
137
+ }
138
+ //#endregion
139
+ //#region src/openrouter/providers.ts
140
+ const CACHE_KEY = "providers";
141
+ const CACHE_TTL = 1440 * 60 * 1e3;
142
+ async function fetchProviders(client) {
143
+ const cached = readCache(CACHE_KEY, CACHE_TTL);
144
+ if (cached) return cached;
145
+ const response = await client.get("/providers");
146
+ writeCache(CACHE_KEY, response.data);
147
+ return response.data;
148
+ }
149
+ //#endregion
150
+ //#region src/commands/config/providers.ts
151
+ async function fetchProvidersForPattern(client) {
152
+ const s = _clack_prompts.spinner();
153
+ s.start("Fetching providers...");
154
+ try {
155
+ const providers = await fetchProviders(client);
156
+ const options = providers.map((p) => ({
157
+ value: p.slug,
158
+ label: p.name
159
+ })).sort((a, b) => a.label.localeCompare(b.label));
160
+ s.stop(`${providers.length} providers available`);
161
+ return options;
162
+ } catch (error) {
163
+ s.stop("Failed to fetch providers");
164
+ _clack_prompts.log.error(String(error));
165
+ return null;
166
+ }
167
+ }
168
+ async function fetchEndpointsForModel(client, modelId) {
169
+ const author = parseModelAuthor(modelId);
170
+ const slug = parseModelSlug(modelId);
171
+ const s = _clack_prompts.spinner();
172
+ s.start("Fetching providers for this model...");
173
+ try {
174
+ const endpoints = await fetchModelEndpoints(client, author, slug);
175
+ const unique = getUniqueProviders(endpoints);
176
+ const options = unique.map((p) => {
177
+ const ep = endpoints.find((e) => e.tag === p.tag);
178
+ const latency = ep?.latency_last_30m?.p50 ?? null;
179
+ const throughput = ep?.throughput_last_30m?.p50 ?? null;
180
+ return {
181
+ value: p.tag,
182
+ label: `${p.providerName} (${p.tag})`,
183
+ hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`
184
+ };
185
+ });
186
+ s.stop(`${unique.length} providers available for this model`);
187
+ return options;
188
+ } catch (error) {
189
+ s.stop("Failed to fetch providers");
190
+ _clack_prompts.log.error(String(error));
191
+ return null;
192
+ }
193
+ }
194
+ async function fetchProvidersForModel(client, modelKey, isPattern) {
195
+ if (isPattern) return fetchProvidersForPattern(client);
196
+ return fetchEndpointsForModel(client, modelKey);
197
+ }
198
+ const DONE_OPTION = "__done__";
199
+ async function selectRoutingMode(message) {
200
+ return _clack_prompts.select({
201
+ message,
202
+ options: [
203
+ {
204
+ value: "only",
205
+ label: "Use specific providers only"
206
+ },
207
+ {
208
+ value: "order",
209
+ label: "Set provider priority order"
210
+ },
211
+ {
212
+ value: "ignore",
213
+ label: "Ignore specific providers"
214
+ },
215
+ {
216
+ value: "skip",
217
+ label: "Skip provider routing"
218
+ }
219
+ ]
220
+ });
221
+ }
222
+ async function selectProvidersByMode(mode, providerOptions) {
223
+ if (mode === "only") return selectOnlyProviders(providerOptions);
224
+ if (mode === "order") return selectOrderedProviders(providerOptions);
225
+ if (mode === "ignore") return selectIgnoreProviders(providerOptions);
226
+ return null;
227
+ }
228
+ async function selectOnlyProviders(providerOptions) {
229
+ const selected = await _clack_prompts.multiselect({
230
+ message: "Select providers",
231
+ options: providerOptions,
232
+ required: false
233
+ });
234
+ if (_clack_prompts.isCancel(selected)) return null;
235
+ const values = selected;
236
+ if (values.length === 0) return {};
237
+ return { provider: { only: values.length === 1 ? values[0] : values } };
238
+ }
239
+ async function selectOrderedProviders(providerOptions) {
240
+ const order = [];
241
+ for (let i = 1;; i++) {
242
+ const remaining = providerOptions.filter((p) => !order.includes(p.value));
243
+ if (remaining.length === 0) break;
244
+ const pick = await _clack_prompts.select({
245
+ message: `Select provider #${i} (or cancel to finish)`,
246
+ options: [...remaining, {
247
+ value: DONE_OPTION,
248
+ label: "✓ Done"
249
+ }]
250
+ });
251
+ if (_clack_prompts.isCancel(pick) || pick === DONE_OPTION) break;
252
+ order.push(pick);
253
+ }
254
+ if (order.length === 0) {
255
+ _clack_prompts.log.warn("No providers selected");
256
+ return null;
257
+ }
258
+ const allowFallbacks = await _clack_prompts.confirm({
259
+ message: "Allow fallbacks to other providers?",
260
+ initialValue: true
261
+ });
262
+ return { provider: {
263
+ order: order.length === 1 ? order[0] : order,
264
+ allowFallbacks: _clack_prompts.isCancel(allowFallbacks) ? true : allowFallbacks
265
+ } };
266
+ }
267
+ async function selectIgnoreProviders(providerOptions) {
268
+ const selected = await _clack_prompts.multiselect({
269
+ message: "Select providers to ignore",
270
+ options: providerOptions,
271
+ required: false
272
+ });
273
+ if (_clack_prompts.isCancel(selected)) return null;
274
+ const values = selected;
275
+ if (values.length === 0) return {};
276
+ return { provider: { ignore: values.length === 1 ? values[0] : values } };
277
+ }
278
+ //#endregion
279
+ Object.defineProperty(exports, "OpenRouterClient", {
280
+ enumerable: true,
281
+ get: function() {
282
+ return OpenRouterClient;
283
+ }
284
+ });
285
+ Object.defineProperty(exports, "fetchModelEndpoints", {
286
+ enumerable: true,
287
+ get: function() {
288
+ return fetchModelEndpoints;
289
+ }
290
+ });
291
+ Object.defineProperty(exports, "fetchModels", {
292
+ enumerable: true,
293
+ get: function() {
294
+ return fetchModels;
295
+ }
296
+ });
297
+ Object.defineProperty(exports, "fetchProvidersForModel", {
298
+ enumerable: true,
299
+ get: function() {
300
+ return fetchProvidersForModel;
301
+ }
302
+ });
303
+ Object.defineProperty(exports, "formatContextLength", {
304
+ enumerable: true,
305
+ get: function() {
306
+ return formatContextLength;
307
+ }
308
+ });
309
+ Object.defineProperty(exports, "formatLatency", {
310
+ enumerable: true,
311
+ get: function() {
312
+ return formatLatency;
313
+ }
314
+ });
315
+ Object.defineProperty(exports, "formatModelHint", {
316
+ enumerable: true,
317
+ get: function() {
318
+ return formatModelHint;
319
+ }
320
+ });
321
+ Object.defineProperty(exports, "formatModelLabel", {
322
+ enumerable: true,
323
+ get: function() {
324
+ return formatModelLabel;
325
+ }
326
+ });
327
+ Object.defineProperty(exports, "formatPrice", {
328
+ enumerable: true,
329
+ get: function() {
330
+ return formatPrice;
331
+ }
332
+ });
333
+ Object.defineProperty(exports, "formatPricing", {
334
+ enumerable: true,
335
+ get: function() {
336
+ return formatPricing;
337
+ }
338
+ });
339
+ Object.defineProperty(exports, "formatThroughput", {
340
+ enumerable: true,
341
+ get: function() {
342
+ return formatThroughput;
343
+ }
344
+ });
345
+ Object.defineProperty(exports, "getUniqueProviders", {
346
+ enumerable: true,
347
+ get: function() {
348
+ return getUniqueProviders;
349
+ }
350
+ });
351
+ Object.defineProperty(exports, "parseModelAuthor", {
352
+ enumerable: true,
353
+ get: function() {
354
+ return parseModelAuthor;
355
+ }
356
+ });
357
+ Object.defineProperty(exports, "parseModelSlug", {
358
+ enumerable: true,
359
+ get: function() {
360
+ return parseModelSlug;
361
+ }
362
+ });
363
+ Object.defineProperty(exports, "selectProvidersByMode", {
364
+ enumerable: true,
365
+ get: function() {
366
+ return selectProvidersByMode;
367
+ }
368
+ });
369
+ Object.defineProperty(exports, "selectRoutingMode", {
370
+ enumerable: true,
371
+ get: function() {
372
+ return selectRoutingMode;
373
+ }
374
+ });
375
+
376
+ //# sourceMappingURL=providers.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.cjs","names":["CACHE_KEY","CACHE_TTL","clack"],"sources":["../src/openrouter/client.ts","../src/openrouter/cache.ts","../src/openrouter/models.ts","../src/commands/config/format.ts","../src/openrouter/endpoints.ts","../src/openrouter/providers.ts","../src/commands/config/providers.ts"],"sourcesContent":["const DEFAULT_BASE_URL = 'https://openrouter.ai/api/v1'\n\nexport class OpenRouterClientError extends Error {\n readonly status: number\n\n constructor(status: number, message: string) {\n super(`OpenRouter API error (${status}): ${message}`)\n this.name = 'OpenRouterClientError'\n this.status = status\n }\n}\n\n/** HTTP client for OpenRouter REST endpoints. Auth header is only sent when apiKey is non-empty. */\nexport class OpenRouterClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n\n constructor(apiKey: string, baseUrl?: string) {\n this.apiKey = apiKey\n this.baseUrl = baseUrl ?? DEFAULT_BASE_URL\n }\n\n async get<T>(path: string): Promise<T> {\n const url = `${this.baseUrl}${path}`\n\n const res = await fetch(url, {\n headers: {\n ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),\n },\n })\n\n if (!res.ok) {\n const body = await res.text().catch(() => '')\n throw new OpenRouterClientError(res.status, body || res.statusText)\n }\n\n return res.json() as Promise<T>\n }\n}\n","import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport type CacheEntry<T> = {\n data: T\n fetchedAt: number\n}\n\nexport const CACHE_DIR = join(homedir(), '.proxitor', 'cache')\n\n/** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */\nexport function readCache<T>(key: string, ttlMs: number): T | null {\n const path = join(CACHE_DIR, `${key}.json`)\n if (!existsSync(path)) return null\n\n try {\n const entry: CacheEntry<T> = JSON.parse(readFileSync(path, 'utf-8'))\n if (Date.now() - entry.fetchedAt > ttlMs) return null\n return entry.data\n } catch {\n return null\n }\n}\n\nexport function writeCache<T>(key: string, data: T): void {\n mkdirSync(CACHE_DIR, { recursive: true })\n const entry: CacheEntry<T> = { fetchedAt: Date.now(), data }\n writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(entry))\n}\n\nexport function clearCache(key: string): void {\n const path = join(CACHE_DIR, `${key}.json`)\n if (existsSync(path)) unlinkSync(path)\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterModel, OpenRouterModelsResponse } from './types.js'\n\nconst CACHE_KEY = 'models'\nconst CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nexport async function fetchModels(client: OpenRouterClient): Promise<OpenRouterModel[]> {\n const cached = readCache<OpenRouterModel[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterModelsResponse>('/models')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"anthropic\"` */\nexport function parseModelAuthor(modelId: string): string {\n return modelId.split('/')[0] ?? ''\n}\n\n/** `\"anthropic/claude-sonnet-4\"` → `\"claude-sonnet-4\"` */\nexport function parseModelSlug(modelId: string): string {\n return modelId.split('/').slice(1).join('/')\n}\n\n/** `\"0.000003\"` → `\"$3.00\"`, `\"0\"` → `\"free\"` */\nexport function formatPrice(pricePerToken: string): string {\n const per1M = Number.parseFloat(pricePerToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n}\n","import { formatPrice } from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\n\nexport function formatPricing(prompt: string, completion: string): string {\n const fmt = (perToken: string) => {\n const per1M = Number.parseFloat(perToken) * 1_000_000\n if (per1M === 0) return 'free'\n if (per1M < 0.01) return `$${per1M.toFixed(4)}`\n return `$${per1M.toFixed(2)}`\n }\n return `${fmt(prompt)} / ${fmt(completion)}`\n}\n\n/** `200000` → `\"200k\"`, `1000000` → `\"1.0M\"` */\nexport function formatContextLength(tokens: number): string {\n if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`\n if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}k`\n return `${tokens}`\n}\n\n/** `1137` → `\"1.1s\"`, `null` → `\"N/A\"` */\nexport function formatLatency(ms: number | null): string {\n if (ms === null) return 'N/A'\n if (ms < 1000) return `${Math.round(ms)}ms`\n return `${(ms / 1000).toFixed(1)}s`\n}\n\nexport function formatThroughput(tokensPerSec: number | null): string {\n if (tokensPerSec === null) return 'N/A'\n return `${tokensPerSec.toFixed(0)} t/s`\n}\n\nexport function formatModelLabel(m: OpenRouterModel): string {\n return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`\n}\n\nexport function formatModelHint(m: OpenRouterModel): string {\n const parts = [`out ${formatPrice(m.pricing.completion)}`]\n if (m.pricing.input_cache_read && m.pricing.input_cache_read !== '0') {\n parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`)\n }\n return parts.join(' · ')\n}\n","import type { OpenRouterClient } from './client.js'\nimport type { ModelEndpoint, ModelEndpointsResponse } from './types.js'\n\nexport async function fetchModelEndpoints(\n client: OpenRouterClient,\n author: string,\n slug: string,\n): Promise<ModelEndpoint[]> {\n const response = await client.get<ModelEndpointsResponse>(\n `/models/${author}/${slug}/endpoints`,\n )\n return response.data.endpoints ?? []\n}\n\nexport type ProviderOption = {\n providerName: string\n /** Routing slug for `provider.only/order/ignore` (e.g. \"anthropic\", \"google-vertex/global\"). */\n tag: string\n}\n\nexport function getUniqueProviders(endpoints: ModelEndpoint[]): ProviderOption[] {\n const seen = new Set<string>()\n const result: ProviderOption[] = []\n\n for (const ep of endpoints) {\n if (seen.has(ep.tag)) continue\n seen.add(ep.tag)\n result.push({ tag: ep.tag, providerName: ep.provider_name })\n }\n\n result.sort((a, b) => a.providerName.localeCompare(b.providerName))\n return result\n}\n","import { readCache, writeCache } from './cache.js'\nimport type { OpenRouterClient } from './client.js'\nimport type { OpenRouterProvider, OpenRouterProvidersResponse } from './types.js'\n\nconst CACHE_KEY = 'providers'\nconst CACHE_TTL = 24 * 60 * 60 * 1000 // 24 hours\n\nexport async function fetchProviders(\n client: OpenRouterClient,\n): Promise<OpenRouterProvider[]> {\n const cached = readCache<OpenRouterProvider[]>(CACHE_KEY, CACHE_TTL)\n if (cached) return cached\n\n const response = await client.get<OpenRouterProvidersResponse>('/providers')\n writeCache(CACHE_KEY, response.data)\n return response.data\n}\n","import * as clack from '@clack/prompts'\nimport type { OpenRouterClient } from '../../openrouter/client.js'\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js'\nimport { parseModelAuthor, parseModelSlug } from '../../openrouter/models.js'\nimport { fetchProviders } from '../../openrouter/providers.js'\nimport { formatLatency, formatThroughput } from './format.js'\n\nexport async function fetchProvidersForPattern(\n client: OpenRouterClient,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const s = clack.spinner()\n s.start('Fetching providers...')\n try {\n const providers = await fetchProviders(client)\n const options = providers\n .map(p => ({ value: p.slug, label: p.name }))\n .sort((a, b) => a.label.localeCompare(b.label))\n s.stop(`${providers.length} providers available`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchEndpointsForModel(\n client: OpenRouterClient,\n modelId: string,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n const author = parseModelAuthor(modelId)\n const slug = parseModelSlug(modelId)\n\n const s = clack.spinner()\n s.start('Fetching providers for this model...')\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug)\n const unique = getUniqueProviders(endpoints)\n\n const options = unique.map(p => {\n const ep = endpoints.find(e => e.tag === p.tag)\n const latency = ep?.latency_last_30m?.p50 ?? null\n const throughput = ep?.throughput_last_30m?.p50 ?? null\n return {\n value: p.tag,\n label: `${p.providerName} (${p.tag})`,\n hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`,\n }\n })\n\n s.stop(`${unique.length} providers available for this model`)\n return options\n } catch (error) {\n s.stop('Failed to fetch providers')\n clack.log.error(String(error))\n return null\n }\n}\n\nexport async function fetchProvidersForModel(\n client: OpenRouterClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<Array<{ value: string; label: string; hint?: string }> | null> {\n if (isPattern) return fetchProvidersForPattern(client)\n return fetchEndpointsForModel(client, modelKey)\n}\n\nconst DONE_OPTION = '__done__'\n\nexport async function selectRoutingMode(message: string): Promise<string | symbol> {\n return clack.select({\n message,\n options: [\n { value: 'only', label: 'Use specific providers only' },\n { value: 'order', label: 'Set provider priority order' },\n { value: 'ignore', label: 'Ignore specific providers' },\n { value: 'skip', label: 'Skip provider routing' },\n ],\n })\n}\n\nexport async function selectProvidersByMode(\n mode: string,\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n if (mode === 'only') return selectOnlyProviders(providerOptions)\n if (mode === 'order') return selectOrderedProviders(providerOptions)\n if (mode === 'ignore') return selectIgnoreProviders(providerOptions)\n return null\n}\n\nasync function selectOnlyProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const only = values.length === 1 ? values[0] : values\n return { provider: { only } }\n}\n\nasync function selectOrderedProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const order: string[] = []\n\n for (let i = 1; ; i++) {\n const remaining = providerOptions.filter(p => !order.includes(p.value))\n if (remaining.length === 0) break\n\n const pick = await clack.select({\n message: `Select provider #${i} (or cancel to finish)`,\n options: [...remaining, { value: DONE_OPTION, label: '✓ Done' }],\n })\n\n if (clack.isCancel(pick) || pick === DONE_OPTION) break\n order.push(pick as string)\n }\n\n if (order.length === 0) {\n clack.log.warn('No providers selected')\n return null\n }\n\n const allowFallbacks = await clack.confirm({\n message: 'Allow fallbacks to other providers?',\n initialValue: true,\n })\n\n return {\n provider: {\n order: order.length === 1 ? order[0] : order,\n allowFallbacks: clack.isCancel(allowFallbacks) ? true : (allowFallbacks as boolean),\n },\n }\n}\n\nasync function selectIgnoreProviders(\n providerOptions: Array<{ value: string; label: string; hint?: string }>,\n): Promise<Record<string, unknown> | null> {\n const selected = await clack.multiselect({\n message: 'Select providers to ignore',\n options: providerOptions,\n required: false,\n })\n\n if (clack.isCancel(selected)) return null\n\n const values = selected as string[]\n if (values.length === 0) return {}\n\n const ignore = values.length === 1 ? values[0] : values\n return { provider: { ignore } }\n}\n"],"mappings":";;;;;;;AAAA,MAAM,mBAAmB;AAEzB,IAAa,wBAAb,cAA2C,MAAM;CAC/C;CAEA,YAAY,QAAgB,SAAiB;EAC3C,MAAM,yBAAyB,OAAO,KAAK,SAAS;EACpD,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;AACF;;AAGA,IAAa,mBAAb,MAA8B;CAC5B;CACA;CAEA,YAAY,QAAgB,SAAkB;EAC5C,KAAK,SAAS;EACd,KAAK,UAAU,WAAW;CAC5B;CAEA,MAAM,IAAO,MAA0B;EACrC,MAAM,MAAM,GAAG,KAAK,UAAU;EAE9B,MAAM,MAAM,MAAM,MAAM,KAAK,EAC3B,SAAS,EACP,GAAI,KAAK,SAAS,EAAE,eAAe,UAAU,KAAK,SAAS,IAAI,CAAC,EAClE,EACF,CAAC;EAED,IAAI,CAAC,IAAI,IAAI;GACX,MAAM,OAAO,MAAM,IAAI,KAAK,EAAE,YAAY,EAAE;GAC5C,MAAM,IAAI,sBAAsB,IAAI,QAAQ,QAAQ,IAAI,UAAU;EACpE;EAEA,OAAO,IAAI,KAAK;CAClB;AACF;;;AC7BA,MAAa,aAAA,GAAA,UAAA,OAAA,GAAA,QAAA,SAAyB,GAAG,aAAa,OAAO;;AAG7D,SAAgB,UAAa,KAAa,OAAyB;CACjE,MAAM,QAAA,GAAA,UAAA,MAAY,WAAW,GAAG,IAAI,MAAM;CAC1C,IAAI,EAAA,GAAA,QAAA,YAAY,IAAI,GAAG,OAAO;CAE9B,IAAI;EACF,MAAM,QAAuB,KAAK,OAAA,GAAA,QAAA,cAAmB,MAAM,OAAO,CAAC;EACnE,IAAI,KAAK,IAAI,IAAI,MAAM,YAAY,OAAO,OAAO;EACjD,OAAO,MAAM;CACf,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,WAAc,KAAa,MAAe;CACxD,CAAA,GAAA,QAAA,WAAU,WAAW,EAAE,WAAW,KAAK,CAAC;CACxC,MAAM,QAAuB;EAAE,WAAW,KAAK,IAAI;EAAG;CAAK;CAC3D,CAAA,GAAA,QAAA,gBAAA,GAAA,UAAA,MAAmB,WAAW,GAAG,IAAI,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AACrE;;;ACzBA,MAAMA,cAAY;AAClB,MAAMC,cAAY,OAAU;AAE5B,eAAsB,YAAY,QAAsD;CACtF,MAAM,SAAS,UAA6BD,aAAWC,WAAS;CAChE,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,MAAM,OAAO,IAA8B,SAAS;CACrE,WAAWD,aAAW,SAAS,IAAI;CACnC,OAAO,SAAS;AAClB;;AAGA,SAAgB,iBAAiB,SAAyB;CACxD,OAAO,QAAQ,MAAM,GAAG,EAAE,MAAM;AAClC;;AAGA,SAAgB,eAAe,SAAyB;CACtD,OAAO,QAAQ,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG;AAC7C;;AAGA,SAAgB,YAAY,eAA+B;CACzD,MAAM,QAAQ,OAAO,WAAW,aAAa,IAAI;CACjD,IAAI,UAAU,GAAG,OAAO;CACxB,IAAI,QAAQ,KAAM,OAAO,IAAI,MAAM,QAAQ,CAAC;CAC5C,OAAO,IAAI,MAAM,QAAQ,CAAC;AAC5B;;;AC7BA,SAAgB,cAAc,QAAgB,YAA4B;CACxE,MAAM,OAAO,aAAqB;EAChC,MAAM,QAAQ,OAAO,WAAW,QAAQ,IAAI;EAC5C,IAAI,UAAU,GAAG,OAAO;EACxB,IAAI,QAAQ,KAAM,OAAO,IAAI,MAAM,QAAQ,CAAC;EAC5C,OAAO,IAAI,MAAM,QAAQ,CAAC;CAC5B;CACA,OAAO,GAAG,IAAI,MAAM,EAAE,KAAK,IAAI,UAAU;AAC3C;;AAGA,SAAgB,oBAAoB,QAAwB;CAC1D,IAAI,UAAU,KAAW,OAAO,IAAI,SAAS,KAAW,QAAQ,CAAC,EAAE;CACnE,IAAI,UAAU,KAAO,OAAO,GAAG,KAAK,MAAM,SAAS,GAAK,EAAE;CAC1D,OAAO,GAAG;AACZ;;AAGA,SAAgB,cAAc,IAA2B;CACvD,IAAI,OAAO,MAAM,OAAO;CACxB,IAAI,KAAK,KAAM,OAAO,GAAG,KAAK,MAAM,EAAE,EAAE;CACxC,OAAO,IAAI,KAAK,KAAM,QAAQ,CAAC,EAAE;AACnC;AAEA,SAAgB,iBAAiB,cAAqC;CACpE,IAAI,iBAAiB,MAAM,OAAO;CAClC,OAAO,GAAG,aAAa,QAAQ,CAAC,EAAE;AACpC;AAEA,SAAgB,iBAAiB,GAA4B;CAC3D,OAAO,GAAG,EAAE,QAAQ,EAAE,GAAG,OAAO,YAAY,EAAE,QAAQ,MAAM,EAAE,KAAK,oBAAoB,EAAE,cAAc;AACzG;AAEA,SAAgB,gBAAgB,GAA4B;CAC1D,MAAM,QAAQ,CAAC,OAAO,YAAY,EAAE,QAAQ,UAAU,GAAG;CACzD,IAAI,EAAE,QAAQ,oBAAoB,EAAE,QAAQ,qBAAqB,KAC/D,MAAM,KAAK,SAAS,YAAY,EAAE,QAAQ,gBAAgB,GAAG;CAE/D,OAAO,MAAM,KAAK,KAAK;AACzB;;;ACvCA,eAAsB,oBACpB,QACA,QACA,MAC0B;CAI1B,QAAO,MAHgB,OAAO,IAC5B,WAAW,OAAO,GAAG,KAAK,WAC5B,GACgB,KAAK,aAAa,CAAC;AACrC;AAQA,SAAgB,mBAAmB,WAA8C;CAC/E,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAA2B,CAAC;CAElC,KAAK,MAAM,MAAM,WAAW;EAC1B,IAAI,KAAK,IAAI,GAAG,GAAG,GAAG;EACtB,KAAK,IAAI,GAAG,GAAG;EACf,OAAO,KAAK;GAAE,KAAK,GAAG;GAAK,cAAc,GAAG;EAAc,CAAC;CAC7D;CAEA,OAAO,MAAM,GAAG,MAAM,EAAE,aAAa,cAAc,EAAE,YAAY,CAAC;CAClE,OAAO;AACT;;;AC5BA,MAAM,YAAY;AAClB,MAAM,YAAY,OAAU,KAAK;AAEjC,eAAsB,eACpB,QAC+B;CAC/B,MAAM,SAAS,UAAgC,WAAW,SAAS;CACnE,IAAI,QAAQ,OAAO;CAEnB,MAAM,WAAW,MAAM,OAAO,IAAiC,YAAY;CAC3E,WAAW,WAAW,SAAS,IAAI;CACnC,OAAO,SAAS;AAClB;;;ACTA,eAAsB,yBACpB,QACwE;CACxE,MAAM,IAAIE,eAAM,QAAQ;CACxB,EAAE,MAAM,uBAAuB;CAC/B,IAAI;EACF,MAAM,YAAY,MAAM,eAAe,MAAM;EAC7C,MAAM,UAAU,UACb,KAAI,OAAM;GAAE,OAAO,EAAE;GAAM,OAAO,EAAE;EAAK,EAAE,EAC3C,MAAM,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;EAChD,EAAE,KAAK,GAAG,UAAU,OAAO,qBAAqB;EAChD,OAAO;CACT,SAAS,OAAO;EACd,EAAE,KAAK,2BAA2B;EAClC,eAAM,IAAI,MAAM,OAAO,KAAK,CAAC;EAC7B,OAAO;CACT;AACF;AAEA,eAAsB,uBACpB,QACA,SACwE;CACxE,MAAM,SAAS,iBAAiB,OAAO;CACvC,MAAM,OAAO,eAAe,OAAO;CAEnC,MAAM,IAAIA,eAAM,QAAQ;CACxB,EAAE,MAAM,sCAAsC;CAC9C,IAAI;EACF,MAAM,YAAY,MAAM,oBAAoB,QAAQ,QAAQ,IAAI;EAChE,MAAM,SAAS,mBAAmB,SAAS;EAE3C,MAAM,UAAU,OAAO,KAAI,MAAK;GAC9B,MAAM,KAAK,UAAU,MAAK,MAAK,EAAE,QAAQ,EAAE,GAAG;GAC9C,MAAM,UAAU,IAAI,kBAAkB,OAAO;GAC7C,MAAM,aAAa,IAAI,qBAAqB,OAAO;GACnD,OAAO;IACL,OAAO,EAAE;IACT,OAAO,GAAG,EAAE,aAAa,IAAI,EAAE,IAAI;IACnC,MAAM,GAAG,cAAc,OAAO,EAAE,KAAK,iBAAiB,UAAU;GAClE;EACF,CAAC;EAED,EAAE,KAAK,GAAG,OAAO,OAAO,oCAAoC;EAC5D,OAAO;CACT,SAAS,OAAO;EACd,EAAE,KAAK,2BAA2B;EAClC,eAAM,IAAI,MAAM,OAAO,KAAK,CAAC;EAC7B,OAAO;CACT;AACF;AAEA,eAAsB,uBACpB,QACA,UACA,WACwE;CACxE,IAAI,WAAW,OAAO,yBAAyB,MAAM;CACrD,OAAO,uBAAuB,QAAQ,QAAQ;AAChD;AAEA,MAAM,cAAc;AAEpB,eAAsB,kBAAkB,SAA2C;CACjF,OAAOA,eAAM,OAAO;EAClB;EACA,SAAS;GACP;IAAE,OAAO;IAAQ,OAAO;GAA8B;GACtD;IAAE,OAAO;IAAS,OAAO;GAA8B;GACvD;IAAE,OAAO;IAAU,OAAO;GAA4B;GACtD;IAAE,OAAO;IAAQ,OAAO;GAAwB;EAClD;CACF,CAAC;AACH;AAEA,eAAsB,sBACpB,MACA,iBACyC;CACzC,IAAI,SAAS,QAAQ,OAAO,oBAAoB,eAAe;CAC/D,IAAI,SAAS,SAAS,OAAO,uBAAuB,eAAe;CACnE,IAAI,SAAS,UAAU,OAAO,sBAAsB,eAAe;CACnE,OAAO;AACT;AAEA,eAAe,oBACb,iBACyC;CACzC,MAAM,WAAW,MAAMA,eAAM,YAAY;EACvC,SAAS;EACT,SAAS;EACT,UAAU;CACZ,CAAC;CAED,IAAIA,eAAM,SAAS,QAAQ,GAAG,OAAO;CAErC,MAAM,SAAS;CACf,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;CAGjC,OAAO,EAAE,UAAU,EAAE,MADR,OAAO,WAAW,IAAI,OAAO,KAAK,OACrB,EAAE;AAC9B;AAEA,eAAe,uBACb,iBACyC;CACzC,MAAM,QAAkB,CAAC;CAEzB,KAAK,IAAI,IAAI,IAAK,KAAK;EACrB,MAAM,YAAY,gBAAgB,QAAO,MAAK,CAAC,MAAM,SAAS,EAAE,KAAK,CAAC;EACtE,IAAI,UAAU,WAAW,GAAG;EAE5B,MAAM,OAAO,MAAMA,eAAM,OAAO;GAC9B,SAAS,oBAAoB,EAAE;GAC/B,SAAS,CAAC,GAAG,WAAW;IAAE,OAAO;IAAa,OAAO;GAAS,CAAC;EACjE,CAAC;EAED,IAAIA,eAAM,SAAS,IAAI,KAAK,SAAS,aAAa;EAClD,MAAM,KAAK,IAAc;CAC3B;CAEA,IAAI,MAAM,WAAW,GAAG;EACtB,eAAM,IAAI,KAAK,uBAAuB;EACtC,OAAO;CACT;CAEA,MAAM,iBAAiB,MAAMA,eAAM,QAAQ;EACzC,SAAS;EACT,cAAc;CAChB,CAAC;CAED,OAAO,EACL,UAAU;EACR,OAAO,MAAM,WAAW,IAAI,MAAM,KAAK;EACvC,gBAAgBA,eAAM,SAAS,cAAc,IAAI,OAAQ;CAC3D,EACF;AACF;AAEA,eAAe,sBACb,iBACyC;CACzC,MAAM,WAAW,MAAMA,eAAM,YAAY;EACvC,SAAS;EACT,SAAS;EACT,UAAU;CACZ,CAAC;CAED,IAAIA,eAAM,SAAS,QAAQ,GAAG,OAAO;CAErC,MAAM,SAAS;CACf,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;CAGjC,OAAO,EAAE,UAAU,EAAE,QADN,OAAO,WAAW,IAAI,OAAO,KAAK,OACrB,EAAE;AAChC"}
@@ -0,0 +1,279 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import * as clack from "@clack/prompts";
5
+ //#region src/openrouter/client.ts
6
+ const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
7
+ var OpenRouterClientError = class extends Error {
8
+ status;
9
+ constructor(status, message) {
10
+ super(`OpenRouter API error (${status}): ${message}`);
11
+ this.name = "OpenRouterClientError";
12
+ this.status = status;
13
+ }
14
+ };
15
+ /** HTTP client for OpenRouter REST endpoints. Auth header is only sent when apiKey is non-empty. */
16
+ var OpenRouterClient = class {
17
+ apiKey;
18
+ baseUrl;
19
+ constructor(apiKey, baseUrl) {
20
+ this.apiKey = apiKey;
21
+ this.baseUrl = baseUrl ?? DEFAULT_BASE_URL;
22
+ }
23
+ async get(path) {
24
+ const url = `${this.baseUrl}${path}`;
25
+ const res = await fetch(url, { headers: { ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {} } });
26
+ if (!res.ok) {
27
+ const body = await res.text().catch(() => "");
28
+ throw new OpenRouterClientError(res.status, body || res.statusText);
29
+ }
30
+ return res.json();
31
+ }
32
+ };
33
+ //#endregion
34
+ //#region src/openrouter/cache.ts
35
+ const CACHE_DIR = join(homedir(), ".proxitor", "cache");
36
+ /** Read a cached value. Returns `null` when missing, expired (older than `ttlMs`), or unparseable. */
37
+ function readCache(key, ttlMs) {
38
+ const path = join(CACHE_DIR, `${key}.json`);
39
+ if (!existsSync(path)) return null;
40
+ try {
41
+ const entry = JSON.parse(readFileSync(path, "utf-8"));
42
+ if (Date.now() - entry.fetchedAt > ttlMs) return null;
43
+ return entry.data;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function writeCache(key, data) {
49
+ mkdirSync(CACHE_DIR, { recursive: true });
50
+ const entry = {
51
+ fetchedAt: Date.now(),
52
+ data
53
+ };
54
+ writeFileSync(join(CACHE_DIR, `${key}.json`), JSON.stringify(entry));
55
+ }
56
+ //#endregion
57
+ //#region src/openrouter/models.ts
58
+ const CACHE_KEY$1 = "models";
59
+ const CACHE_TTL$1 = 3600 * 1e3;
60
+ async function fetchModels(client) {
61
+ const cached = readCache(CACHE_KEY$1, CACHE_TTL$1);
62
+ if (cached) return cached;
63
+ const response = await client.get("/models");
64
+ writeCache(CACHE_KEY$1, response.data);
65
+ return response.data;
66
+ }
67
+ /** `"anthropic/claude-sonnet-4"` → `"anthropic"` */
68
+ function parseModelAuthor(modelId) {
69
+ return modelId.split("/")[0] ?? "";
70
+ }
71
+ /** `"anthropic/claude-sonnet-4"` → `"claude-sonnet-4"` */
72
+ function parseModelSlug(modelId) {
73
+ return modelId.split("/").slice(1).join("/");
74
+ }
75
+ /** `"0.000003"` → `"$3.00"`, `"0"` → `"free"` */
76
+ function formatPrice(pricePerToken) {
77
+ const per1M = Number.parseFloat(pricePerToken) * 1e6;
78
+ if (per1M === 0) return "free";
79
+ if (per1M < .01) return `$${per1M.toFixed(4)}`;
80
+ return `$${per1M.toFixed(2)}`;
81
+ }
82
+ //#endregion
83
+ //#region src/commands/config/format.ts
84
+ function formatPricing(prompt, completion) {
85
+ const fmt = (perToken) => {
86
+ const per1M = Number.parseFloat(perToken) * 1e6;
87
+ if (per1M === 0) return "free";
88
+ if (per1M < .01) return `$${per1M.toFixed(4)}`;
89
+ return `$${per1M.toFixed(2)}`;
90
+ };
91
+ return `${fmt(prompt)} / ${fmt(completion)}`;
92
+ }
93
+ /** `200000` → `"200k"`, `1000000` → `"1.0M"` */
94
+ function formatContextLength(tokens) {
95
+ if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
96
+ if (tokens >= 1e3) return `${Math.round(tokens / 1e3)}k`;
97
+ return `${tokens}`;
98
+ }
99
+ /** `1137` → `"1.1s"`, `null` → `"N/A"` */
100
+ function formatLatency(ms) {
101
+ if (ms === null) return "N/A";
102
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
103
+ return `${(ms / 1e3).toFixed(1)}s`;
104
+ }
105
+ function formatThroughput(tokensPerSec) {
106
+ if (tokensPerSec === null) return "N/A";
107
+ return `${tokensPerSec.toFixed(0)} t/s`;
108
+ }
109
+ function formatModelLabel(m) {
110
+ return `${m.name || m.id} — ${formatPrice(m.pricing.prompt)} · ${formatContextLength(m.context_length)}`;
111
+ }
112
+ function formatModelHint(m) {
113
+ const parts = [`out ${formatPrice(m.pricing.completion)}`];
114
+ if (m.pricing.input_cache_read && m.pricing.input_cache_read !== "0") parts.push(`cache ${formatPrice(m.pricing.input_cache_read)}`);
115
+ return parts.join(" · ");
116
+ }
117
+ //#endregion
118
+ //#region src/openrouter/endpoints.ts
119
+ async function fetchModelEndpoints(client, author, slug) {
120
+ return (await client.get(`/models/${author}/${slug}/endpoints`)).data.endpoints ?? [];
121
+ }
122
+ function getUniqueProviders(endpoints) {
123
+ const seen = /* @__PURE__ */ new Set();
124
+ const result = [];
125
+ for (const ep of endpoints) {
126
+ if (seen.has(ep.tag)) continue;
127
+ seen.add(ep.tag);
128
+ result.push({
129
+ tag: ep.tag,
130
+ providerName: ep.provider_name
131
+ });
132
+ }
133
+ result.sort((a, b) => a.providerName.localeCompare(b.providerName));
134
+ return result;
135
+ }
136
+ //#endregion
137
+ //#region src/openrouter/providers.ts
138
+ const CACHE_KEY = "providers";
139
+ const CACHE_TTL = 1440 * 60 * 1e3;
140
+ async function fetchProviders(client) {
141
+ const cached = readCache(CACHE_KEY, CACHE_TTL);
142
+ if (cached) return cached;
143
+ const response = await client.get("/providers");
144
+ writeCache(CACHE_KEY, response.data);
145
+ return response.data;
146
+ }
147
+ //#endregion
148
+ //#region src/commands/config/providers.ts
149
+ async function fetchProvidersForPattern(client) {
150
+ const s = clack.spinner();
151
+ s.start("Fetching providers...");
152
+ try {
153
+ const providers = await fetchProviders(client);
154
+ const options = providers.map((p) => ({
155
+ value: p.slug,
156
+ label: p.name
157
+ })).sort((a, b) => a.label.localeCompare(b.label));
158
+ s.stop(`${providers.length} providers available`);
159
+ return options;
160
+ } catch (error) {
161
+ s.stop("Failed to fetch providers");
162
+ clack.log.error(String(error));
163
+ return null;
164
+ }
165
+ }
166
+ async function fetchEndpointsForModel(client, modelId) {
167
+ const author = parseModelAuthor(modelId);
168
+ const slug = parseModelSlug(modelId);
169
+ const s = clack.spinner();
170
+ s.start("Fetching providers for this model...");
171
+ try {
172
+ const endpoints = await fetchModelEndpoints(client, author, slug);
173
+ const unique = getUniqueProviders(endpoints);
174
+ const options = unique.map((p) => {
175
+ const ep = endpoints.find((e) => e.tag === p.tag);
176
+ const latency = ep?.latency_last_30m?.p50 ?? null;
177
+ const throughput = ep?.throughput_last_30m?.p50 ?? null;
178
+ return {
179
+ value: p.tag,
180
+ label: `${p.providerName} (${p.tag})`,
181
+ hint: `${formatLatency(latency)} · ${formatThroughput(throughput)}`
182
+ };
183
+ });
184
+ s.stop(`${unique.length} providers available for this model`);
185
+ return options;
186
+ } catch (error) {
187
+ s.stop("Failed to fetch providers");
188
+ clack.log.error(String(error));
189
+ return null;
190
+ }
191
+ }
192
+ async function fetchProvidersForModel(client, modelKey, isPattern) {
193
+ if (isPattern) return fetchProvidersForPattern(client);
194
+ return fetchEndpointsForModel(client, modelKey);
195
+ }
196
+ const DONE_OPTION = "__done__";
197
+ async function selectRoutingMode(message) {
198
+ return clack.select({
199
+ message,
200
+ options: [
201
+ {
202
+ value: "only",
203
+ label: "Use specific providers only"
204
+ },
205
+ {
206
+ value: "order",
207
+ label: "Set provider priority order"
208
+ },
209
+ {
210
+ value: "ignore",
211
+ label: "Ignore specific providers"
212
+ },
213
+ {
214
+ value: "skip",
215
+ label: "Skip provider routing"
216
+ }
217
+ ]
218
+ });
219
+ }
220
+ async function selectProvidersByMode(mode, providerOptions) {
221
+ if (mode === "only") return selectOnlyProviders(providerOptions);
222
+ if (mode === "order") return selectOrderedProviders(providerOptions);
223
+ if (mode === "ignore") return selectIgnoreProviders(providerOptions);
224
+ return null;
225
+ }
226
+ async function selectOnlyProviders(providerOptions) {
227
+ const selected = await clack.multiselect({
228
+ message: "Select providers",
229
+ options: providerOptions,
230
+ required: false
231
+ });
232
+ if (clack.isCancel(selected)) return null;
233
+ const values = selected;
234
+ if (values.length === 0) return {};
235
+ return { provider: { only: values.length === 1 ? values[0] : values } };
236
+ }
237
+ async function selectOrderedProviders(providerOptions) {
238
+ const order = [];
239
+ for (let i = 1;; i++) {
240
+ const remaining = providerOptions.filter((p) => !order.includes(p.value));
241
+ if (remaining.length === 0) break;
242
+ const pick = await clack.select({
243
+ message: `Select provider #${i} (or cancel to finish)`,
244
+ options: [...remaining, {
245
+ value: DONE_OPTION,
246
+ label: "✓ Done"
247
+ }]
248
+ });
249
+ if (clack.isCancel(pick) || pick === DONE_OPTION) break;
250
+ order.push(pick);
251
+ }
252
+ if (order.length === 0) {
253
+ clack.log.warn("No providers selected");
254
+ return null;
255
+ }
256
+ const allowFallbacks = await clack.confirm({
257
+ message: "Allow fallbacks to other providers?",
258
+ initialValue: true
259
+ });
260
+ return { provider: {
261
+ order: order.length === 1 ? order[0] : order,
262
+ allowFallbacks: clack.isCancel(allowFallbacks) ? true : allowFallbacks
263
+ } };
264
+ }
265
+ async function selectIgnoreProviders(providerOptions) {
266
+ const selected = await clack.multiselect({
267
+ message: "Select providers to ignore",
268
+ options: providerOptions,
269
+ required: false
270
+ });
271
+ if (clack.isCancel(selected)) return null;
272
+ const values = selected;
273
+ if (values.length === 0) return {};
274
+ return { provider: { ignore: values.length === 1 ? values[0] : values } };
275
+ }
276
+ //#endregion
277
+ export { getUniqueProviders as a, formatModelHint as c, formatThroughput as d, fetchModels as f, OpenRouterClient as g, parseModelSlug as h, fetchModelEndpoints as i, formatModelLabel as l, parseModelAuthor as m, selectProvidersByMode as n, formatContextLength as o, formatPrice as p, selectRoutingMode as r, formatLatency as s, fetchProvidersForModel as t, formatPricing as u };
278
+
279
+ //# sourceMappingURL=providers.mjs.map