proxitor 0.8.0 → 0.9.0-beta.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.
package/README.md CHANGED
@@ -127,15 +127,17 @@ This changes the header to `Authorization: OAuth sk-...`.
127
127
 
128
128
  When using a custom `openrouterBaseUrl` that points to a third-party service, that service may not support OpenRouter-specific endpoints like `/providers` or `/models/{author}/{slug}/endpoints`. Proxitor handles this automatically:
129
129
 
130
- - **Automatic fallback** — if the custom API returns an error (4xx/5xx) or an unexpected response format for data endpoints, proxitor falls back to `https://openrouter.ai/api/v1` (no API key needed — these endpoints are public)
130
+ - **Automatic fallback** — if the custom API returns an error (4xx/5xx) or an unexpected response format for data endpoints, proxitor falls back to `https://openrouter.ai/api` (no API key needed — these endpoints are public)
131
131
  - **`openrouterDataUrl`** — set this explicitly to control the primary URL for data fetching, independent of `openrouterBaseUrl` (which is used for proxying requests)
132
132
 
133
133
  ```yaml
134
134
  # Proxy requests go to custom service, data fetching falls back to OpenRouter
135
- openrouterBaseUrl: 'https://custom-service.example.com/v1'
135
+ # NOTE: do NOT include /v1 in the base URL — request paths like /v1/chat/completions
136
+ # are forwarded as-is, so /v1 would be duplicated if included here
137
+ openrouterBaseUrl: 'https://custom-service.example.com/api'
136
138
 
137
139
  # Explicitly set the primary data URL (optional, defaults to openrouterBaseUrl)
138
- # openrouterDataUrl: 'https://openrouter.ai/api/v1'
140
+ # openrouterDataUrl: 'https://openrouter.ai/api'
139
141
  ```
140
142
 
141
143
  When a fallback occurs, proxitor logs a warning: `Custom API did not return providers, using OpenRouter data as fallback`.
@@ -250,6 +252,57 @@ provider:
250
252
  p90: 3 # seconds (soft threshold)
251
253
  ```
252
254
 
255
+ ### Prompt caching
256
+
257
+ By default, OpenRouter doesn't enable prompt caching — every request pays full token price. Proxitor can inject `cache_control` and `session_id` to make caching work automatically.
258
+
259
+ **`cacheControl`** — injects `cache_control: { "type": "ephemeral" }` into the request body. OpenRouter uses this to set cache breakpoints and advance them as conversations grow.
260
+
261
+ **`cacheControlTtl`** — controls the cache time-to-live. Anthropic's default TTL is 5 minutes (300s). Set to `1h` for a 1-hour cache at higher write cost (2× vs 1.25×). Only applies to Anthropic models — other providers don't support TTL.
262
+
263
+ **`sessionId`** — injects `session_id` for provider sticky routing. Without it, OpenRouter only pins to a provider after detecting a cache hit. With it, routing sticks from the **first request** — critical for OpenAI models where delayed caching means 0 cached tokens on the first 1-2 requests.
264
+
265
+ Both `cacheControl` and `sessionId` support `auto` / `always` / `never` modes:
266
+
267
+ | Mode | `cacheControl` | `sessionId` |
268
+ | --- | --- | --- |
269
+ | `auto` (default) | Anthropic models on `/v1/chat/completions`; all models on `/v1/messages` and `/v1/responses` | Use `X-Claude-Code-Session-Id` header if present; otherwise generate proxy UUID |
270
+ | `always` | All models, all endpoints | Generate a proxy UUID for sticky routing |
271
+ | `never` | Disabled | Disabled |
272
+
273
+ `cacheControlTtl` values:
274
+
275
+ | Value | TTL | Write cost | Use when |
276
+ | --- | --- | --- | --- |
277
+ | _(not set)_ | 5 min (Anthropic default) | 1.25× | High-frequency requests (>1 per 5 min) |
278
+ | `5m` | 5 minutes | 1.25× | Explicit short cache |
279
+ | `1h` | 1 hour | 2.0× | Low-frequency or long-running sessions |
280
+
281
+ ```yaml
282
+ cacheControl: auto # safe default — Anthropic and safe endpoints only
283
+ sessionId: auto # always ensures sticky routing (client header or proxy UUID)
284
+
285
+ # Use 1-hour cache for all Anthropic models (higher write cost, longer TTL)
286
+ cacheControlTtl: 1h
287
+
288
+ # Force caching for all models (may cause 400 on non-Anthropic /v1/chat/completions)
289
+ # cacheControl: always
290
+
291
+ # Per-model overrides — TTL supports '5m', '1h', or 'default' (cancel global TTL)
292
+ modelOverrides:
293
+ "gpt-*":
294
+ cacheControl: never # OpenAI caches automatically, no injection needed
295
+ sessionId: always # but sticky routing still helps
296
+ "claude-opus-*":
297
+ cacheControlTtl: default # cancel global 1h TTL for Opus — use Anthropic's 5 min default
298
+ ```
299
+
300
+ **Why all three matter:**
301
+
302
+ - **Anthropic models** — `cache_control` activates caching, `cacheControlTtl` extends it beyond 5 min, `session_id` prevents provider flip-flopping that would invalidate it
303
+ - **OpenAI models** — caching is automatic (no `cache_control` needed), but `session_id` ensures sticky routing from request #1 instead of waiting for a cache hit
304
+ - **All models** — `session_id` prevents the provider switch that silently resets cache
305
+
253
306
  ### Health check
254
307
 
255
308
  ```sh
@@ -292,7 +345,7 @@ The wizard asks for:
292
345
 
293
346
  - **OpenRouter API key** — stored in config or set as `OPENROUTER_API_KEY` env var
294
347
  - **Port** — default `8828` (avoids conflicts with common dev servers on 8080)
295
- - **API base URL** — default `https://openrouter.ai/api/v1`; change for self-hosted or custom endpoints
348
+ - **API base URL** — default `https://openrouter.ai/api`; change for self-hosted or custom endpoints
296
349
  - **Data URL** — separate URL for provider/model data fetching; falls back to OpenRouter automatically if the custom API doesn't support these endpoints
297
350
  - **Authentication type** — `bearer` (default) or `oauth`; use `oauth` for custom proxy providers that pass tokens in the `Authorization: OAuth ...` header
298
351
  - **Host** — all interfaces (`0.0.0.0`) or localhost only (`127.0.0.1`)
package/dist/add.mjs CHANGED
@@ -1,11 +1,11 @@
1
- import { autocomplete as Vt, confirm as le, intro as ye, isCancel as R$1, log as R, outro as fe, spinner as vt, text as Re } from "./dist.mjs";
1
+ import { autocomplete, confirm, intro, isCancel, log, outro, spinner, text } from "./dist.mjs";
2
2
  import { c as formatModelHint, f as fetchModels, l as formatModelLabel, n as selectProvidersByMode, o as formatContextLength, p as formatPrice, r as selectRoutingMode, t as fetchProvidersForModel, u as formatPricing } from "./providers.mjs";
3
3
  import { i as setModelOverride, r as requireConfigPath, t as getModelOverrides } from "./config.mjs";
4
4
  //#region src/commands/config/add.ts
5
5
  const CUSTOM_PATTERN = "__custom_pattern__";
6
6
  /** Run the interactive "Add model override" flow. */
7
7
  async function addOverrideCommand(client) {
8
- ye("Add Model Override");
8
+ intro("Add Model Override");
9
9
  const configPath = requireConfigPath();
10
10
  const models = await loadModelsWithSpinner(client);
11
11
  if (!models) return;
@@ -16,7 +16,7 @@ async function addOverrideCommand(client) {
16
16
  const pattern = await enterPattern(models);
17
17
  if (!pattern) return;
18
18
  if (getModelOverrides(configPath)[pattern]) {
19
- R.warn(`Override for "${pattern}" already exists. Use Edit instead.`);
19
+ log.warn(`Override for "${pattern}" already exists. Use Edit instead.`);
20
20
  return;
21
21
  }
22
22
  await configureProviderAndSave(configPath, client, pattern, true);
@@ -25,13 +25,13 @@ async function addOverrideCommand(client) {
25
25
  const selected = models.find((m) => m.id === modelId);
26
26
  if (selected) displayModelInfo(selected);
27
27
  if (getModelOverrides(configPath)[modelId]) {
28
- R.warn(`Override for "${modelId}" already exists. Use Edit instead.`);
28
+ log.warn(`Override for "${modelId}" already exists. Use Edit instead.`);
29
29
  return;
30
30
  }
31
31
  await configureProviderAndSave(configPath, client, modelId, false);
32
32
  }
33
33
  async function loadModelsWithSpinner(client) {
34
- const s = vt();
34
+ const s = spinner();
35
35
  s.start("Loading models from OpenRouter...");
36
36
  try {
37
37
  const models = await fetchModels(client);
@@ -39,12 +39,12 @@ async function loadModelsWithSpinner(client) {
39
39
  return models;
40
40
  } catch (error) {
41
41
  s.stop("Failed to load models");
42
- R.error(String(error));
42
+ log.error(String(error));
43
43
  return null;
44
44
  }
45
45
  }
46
46
  async function searchModel(models) {
47
- const result = await Vt({
47
+ const result = await autocomplete({
48
48
  message: "Search for a model",
49
49
  placeholder: "Type to search (e.g. \"claude\", \"gpt-4o\", \"qwen\")",
50
50
  maxItems: 15,
@@ -67,53 +67,53 @@ async function searchModel(models) {
67
67
  },
68
68
  filter: (_search, _option) => true
69
69
  });
70
- if (R$1(result)) return null;
70
+ if (isCancel(result)) return null;
71
71
  return result;
72
72
  }
73
73
  async function enterPattern(models) {
74
- const pattern = await Re({
74
+ const pattern = await text({
75
75
  message: "Enter model pattern",
76
76
  placeholder: "e.g. claude-*, gpt-4*, anthropic/*",
77
77
  validate: (v) => {
78
78
  if (!v?.trim()) return "Pattern cannot be empty";
79
79
  }
80
80
  });
81
- if (R$1(pattern)) return null;
81
+ if (isCancel(pattern)) return null;
82
82
  const pat = pattern.trim();
83
83
  const matches = countPatternMatches(pat, models);
84
- if (matches > 0) R.info(`Pattern "${pat}" matches ${matches} model(s)`);
85
- else R.warn(`Pattern "${pat}" does not match any current models — it will still be saved`);
84
+ if (matches > 0) log.info(`Pattern "${pat}" matches ${matches} model(s)`);
85
+ else log.warn(`Pattern "${pat}" does not match any current models — it will still be saved`);
86
86
  return pat;
87
87
  }
88
88
  async function configureProviderAndSave(configPath, client, modelKey, isPattern) {
89
89
  const mode = await selectRoutingMode("Configure provider routing");
90
- if (R$1(mode)) return;
90
+ if (isCancel(mode)) return;
91
91
  if (mode === "skip") {
92
92
  setModelOverride(configPath, modelKey, {});
93
- fe("Done — override saved without provider routing");
93
+ outro("Done — override saved without provider routing");
94
94
  return;
95
95
  }
96
96
  const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);
97
97
  if (!providerOptions) return;
98
98
  const override = await selectProvidersByMode(mode, providerOptions);
99
99
  if (!override) return;
100
- R.info(`Proposed override:\n ${modelKey}:\n ${formatOverrideYaml(override)}`);
101
- const save = await le({ message: "Save to config?" });
102
- if (R$1(save) || !save) {
103
- fe("Cancelled");
100
+ log.info(`Proposed override:\n ${modelKey}:\n ${formatOverrideYaml(override)}`);
101
+ const save = await confirm({ message: "Save to config?" });
102
+ if (isCancel(save) || !save) {
103
+ outro("Cancelled");
104
104
  return;
105
105
  }
106
106
  setModelOverride(configPath, modelKey, override);
107
- fe("✓ Model override saved");
107
+ outro("✓ Model override saved");
108
108
  }
109
109
  function displayModelInfo(model) {
110
- R.info(`${model.name || model.id}`);
111
- R.info(` Context: ${formatContextLength(model.context_length)} tokens`);
112
- R.info(` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`);
113
- if (model.pricing.input_cache_read && model.pricing.input_cache_read !== "0") R.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);
114
- if (model.pricing.input_cache_write && model.pricing.input_cache_write !== "0") R.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);
115
- if (model.top_provider?.max_completion_tokens) R.info(` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`);
116
- if (model.architecture?.modality) R.info(` Modality: ${model.architecture.modality}`);
110
+ log.info(`${model.name || model.id}`);
111
+ log.info(` Context: ${formatContextLength(model.context_length)} tokens`);
112
+ log.info(` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`);
113
+ if (model.pricing.input_cache_read && model.pricing.input_cache_read !== "0") log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);
114
+ if (model.pricing.input_cache_write && model.pricing.input_cache_write !== "0") log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);
115
+ if (model.top_provider?.max_completion_tokens) log.info(` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`);
116
+ if (model.architecture?.modality) log.info(` Modality: ${model.architecture.modality}`);
117
117
  }
118
118
  function countPatternMatches(pattern, models) {
119
119
  if (pattern.endsWith("*")) {
package/dist/add.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"add.mjs","names":["clack.spinner","clack.autocomplete","isCancel","clack.text","clack.confirm"],"sources":["../src/commands/config/add.ts"],"sourcesContent":["import * as clack from '@clack/prompts';\nimport { isCancel } from '@clack/prompts';\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js';\nimport { fetchModels, formatPrice } from '../../openrouter/models.js';\nimport type { OpenRouterModel } from '../../openrouter/types.js';\nimport { getModelOverrides, requireConfigPath, setModelOverride } from './config.js';\nimport {\n formatContextLength,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n} from './format.js';\nimport {\n fetchProvidersForModel,\n selectProvidersByMode,\n selectRoutingMode,\n} from './providers.js';\n\nconst CUSTOM_PATTERN = '__custom_pattern__';\n\n/** Run the interactive \"Add model override\" flow. */\nexport async function addOverrideCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Add Model Override');\n\n const configPath = requireConfigPath();\n\n const models = await loadModelsWithSpinner(client);\n if (!models) return;\n\n const modelId = await searchModel(models);\n if (!modelId) return;\n\n if (typeof modelId !== 'string') return;\n\n if (modelId === CUSTOM_PATTERN) {\n const pattern = await enterPattern(models);\n if (!pattern) return;\n\n const existing = getModelOverrides(configPath);\n if (existing[pattern]) {\n clack.log.warn(`Override for \"${pattern}\" already exists. Use Edit instead.`);\n return;\n }\n\n await configureProviderAndSave(configPath, client, pattern, true);\n return;\n }\n\n const selected = models.find(m => m.id === modelId);\n if (selected) displayModelInfo(selected);\n\n const existing = getModelOverrides(configPath);\n if (existing[modelId]) {\n clack.log.warn(`Override for \"${modelId}\" already exists. Use Edit instead.`);\n return;\n }\n\n await configureProviderAndSave(configPath, client, modelId, false);\n}\n\nasync function loadModelsWithSpinner(\n client: OpenRouterDataClient,\n): Promise<OpenRouterModel[] | null> {\n const s = clack.spinner();\n s.start('Loading models from OpenRouter...');\n try {\n const models = await fetchModels(client);\n s.stop(`${models.length} models available`);\n return models;\n } catch (error) {\n s.stop('Failed to load models');\n clack.log.error(String(error));\n return null;\n }\n}\n\nasync function searchModel(models: OpenRouterModel[]): Promise<string | symbol | null> {\n const result = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search (e.g. \"claude\", \"gpt-4o\", \"qwen\")',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase();\n\n if (!query) {\n return [\n {\n value: CUSTOM_PATTERN,\n label: '✏️ Enter custom pattern (e.g. \"claude-*\")',\n },\n ];\n }\n\n const filtered = models\n .filter(m => {\n const text = `${m.id} ${m.name}`.toLowerCase();\n return text.includes(query);\n })\n .slice(0, 14)\n .map(m => ({\n value: m.id,\n label: formatModelLabel(m),\n hint: formatModelHint(m),\n }));\n\n return [\n ...filtered,\n { value: CUSTOM_PATTERN, label: '✏️ Enter custom pattern (e.g. \"claude-*\")' },\n ];\n },\n filter: (_search: string, _option: { value: string }) => true,\n });\n\n if (isCancel(result)) return null;\n return result as string;\n}\n\nasync function enterPattern(models: OpenRouterModel[]): Promise<string | null> {\n const pattern = await clack.text({\n message: 'Enter model pattern',\n placeholder: 'e.g. claude-*, gpt-4*, anthropic/*',\n validate: v => {\n if (!v?.trim()) return 'Pattern cannot be empty';\n return undefined;\n },\n });\n\n if (isCancel(pattern)) return null;\n\n const pat = (pattern as string).trim();\n const matches = countPatternMatches(pat, models);\n if (matches > 0) {\n clack.log.info(`Pattern \"${pat}\" matches ${matches} model(s)`);\n } else {\n clack.log.warn(\n `Pattern \"${pat}\" does not match any current models — it will still be saved`,\n );\n }\n\n return pat;\n}\n\nasync function configureProviderAndSave(\n configPath: string,\n client: OpenRouterDataClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<void> {\n const mode = await selectRoutingMode('Configure provider routing');\n if (isCancel(mode)) return;\n\n if (mode === 'skip') {\n setModelOverride(configPath, modelKey, {});\n clack.outro('Done — override saved without provider routing');\n return;\n }\n\n const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);\n if (!providerOptions) return;\n\n const override = await selectProvidersByMode(mode as string, providerOptions);\n if (!override) return;\n\n clack.log.info(\n `Proposed override:\\n ${modelKey}:\\n ${formatOverrideYaml(override)}`,\n );\n\n const save = await clack.confirm({ message: 'Save to config?' });\n if (isCancel(save) || !save) {\n clack.outro('Cancelled');\n return;\n }\n\n setModelOverride(configPath, modelKey, override);\n clack.outro('✓ Model override saved');\n}\n\nfunction displayModelInfo(model: OpenRouterModel): void {\n clack.log.info(`${model.name || model.id}`);\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`);\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n );\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);\n }\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n );\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`);\n }\n}\n\nfunction countPatternMatches(pattern: string, models: OpenRouterModel[]): number {\n if (pattern.endsWith('*')) {\n const prefix = pattern.slice(0, -1);\n return models.filter(m => m.id.startsWith(prefix)).length;\n }\n return models.filter(m => m.id === pattern).length;\n}\n\nfunction formatOverrideYaml(override: Record<string, unknown>): string {\n const parts: string[] = [];\n if (override.provider && typeof override.provider === 'object') {\n const p = override.provider as Record<string, unknown>;\n for (const [key, value] of Object.entries(p)) {\n parts.push(`provider.${key}: ${JSON.stringify(value)}`);\n }\n }\n return parts.join('\\n ') || '(empty)';\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"add.mjs","names":["clack.spinner","clack.autocomplete","clack.text","clack.confirm"],"sources":["../src/commands/config/add.ts"],"sourcesContent":["import * as clack from '@clack/prompts';\nimport { isCancel } from '@clack/prompts';\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js';\nimport { fetchModels, formatPrice } from '../../openrouter/models.js';\nimport type { OpenRouterModel } from '../../openrouter/types.js';\nimport { getModelOverrides, requireConfigPath, setModelOverride } from './config.js';\nimport {\n formatContextLength,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n} from './format.js';\nimport {\n fetchProvidersForModel,\n selectProvidersByMode,\n selectRoutingMode,\n} from './providers.js';\n\nconst CUSTOM_PATTERN = '__custom_pattern__';\n\n/** Run the interactive \"Add model override\" flow. */\nexport async function addOverrideCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Add Model Override');\n\n const configPath = requireConfigPath();\n\n const models = await loadModelsWithSpinner(client);\n if (!models) return;\n\n const modelId = await searchModel(models);\n if (!modelId) return;\n\n if (typeof modelId !== 'string') return;\n\n if (modelId === CUSTOM_PATTERN) {\n const pattern = await enterPattern(models);\n if (!pattern) return;\n\n const existing = getModelOverrides(configPath);\n if (existing[pattern]) {\n clack.log.warn(`Override for \"${pattern}\" already exists. Use Edit instead.`);\n return;\n }\n\n await configureProviderAndSave(configPath, client, pattern, true);\n return;\n }\n\n const selected = models.find(m => m.id === modelId);\n if (selected) displayModelInfo(selected);\n\n const existing = getModelOverrides(configPath);\n if (existing[modelId]) {\n clack.log.warn(`Override for \"${modelId}\" already exists. Use Edit instead.`);\n return;\n }\n\n await configureProviderAndSave(configPath, client, modelId, false);\n}\n\nasync function loadModelsWithSpinner(\n client: OpenRouterDataClient,\n): Promise<OpenRouterModel[] | null> {\n const s = clack.spinner();\n s.start('Loading models from OpenRouter...');\n try {\n const models = await fetchModels(client);\n s.stop(`${models.length} models available`);\n return models;\n } catch (error) {\n s.stop('Failed to load models');\n clack.log.error(String(error));\n return null;\n }\n}\n\nasync function searchModel(models: OpenRouterModel[]): Promise<string | symbol | null> {\n const result = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search (e.g. \"claude\", \"gpt-4o\", \"qwen\")',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase();\n\n if (!query) {\n return [\n {\n value: CUSTOM_PATTERN,\n label: '✏️ Enter custom pattern (e.g. \"claude-*\")',\n },\n ];\n }\n\n const filtered = models\n .filter(m => {\n const text = `${m.id} ${m.name}`.toLowerCase();\n return text.includes(query);\n })\n .slice(0, 14)\n .map(m => ({\n value: m.id,\n label: formatModelLabel(m),\n hint: formatModelHint(m),\n }));\n\n return [\n ...filtered,\n { value: CUSTOM_PATTERN, label: '✏️ Enter custom pattern (e.g. \"claude-*\")' },\n ];\n },\n filter: (_search: string, _option: { value: string }) => true,\n });\n\n if (isCancel(result)) return null;\n return result as string;\n}\n\nasync function enterPattern(models: OpenRouterModel[]): Promise<string | null> {\n const pattern = await clack.text({\n message: 'Enter model pattern',\n placeholder: 'e.g. claude-*, gpt-4*, anthropic/*',\n validate: v => {\n if (!v?.trim()) return 'Pattern cannot be empty';\n return undefined;\n },\n });\n\n if (isCancel(pattern)) return null;\n\n const pat = (pattern as string).trim();\n const matches = countPatternMatches(pat, models);\n if (matches > 0) {\n clack.log.info(`Pattern \"${pat}\" matches ${matches} model(s)`);\n } else {\n clack.log.warn(\n `Pattern \"${pat}\" does not match any current models — it will still be saved`,\n );\n }\n\n return pat;\n}\n\nasync function configureProviderAndSave(\n configPath: string,\n client: OpenRouterDataClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<void> {\n const mode = await selectRoutingMode('Configure provider routing');\n if (isCancel(mode)) return;\n\n if (mode === 'skip') {\n setModelOverride(configPath, modelKey, {});\n clack.outro('Done — override saved without provider routing');\n return;\n }\n\n const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern);\n if (!providerOptions) return;\n\n const override = await selectProvidersByMode(mode as string, providerOptions);\n if (!override) return;\n\n clack.log.info(\n `Proposed override:\\n ${modelKey}:\\n ${formatOverrideYaml(override)}`,\n );\n\n const save = await clack.confirm({ message: 'Save to config?' });\n if (isCancel(save) || !save) {\n clack.outro('Cancelled');\n return;\n }\n\n setModelOverride(configPath, modelKey, override);\n clack.outro('✓ Model override saved');\n}\n\nfunction displayModelInfo(model: OpenRouterModel): void {\n clack.log.info(`${model.name || model.id}`);\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`);\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n );\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);\n }\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n );\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`);\n }\n}\n\nfunction countPatternMatches(pattern: string, models: OpenRouterModel[]): number {\n if (pattern.endsWith('*')) {\n const prefix = pattern.slice(0, -1);\n return models.filter(m => m.id.startsWith(prefix)).length;\n }\n return models.filter(m => m.id === pattern).length;\n}\n\nfunction formatOverrideYaml(override: Record<string, unknown>): string {\n const parts: string[] = [];\n if (override.provider && typeof override.provider === 'object') {\n const p = override.provider as Record<string, unknown>;\n for (const [key, value] of Object.entries(p)) {\n parts.push(`provider.${key}: ${JSON.stringify(value)}`);\n }\n }\n return parts.join('\\n ') || '(empty)';\n}\n"],"mappings":"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"}
package/dist/browse.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { autocomplete as Vt, confirm as le, intro as ye, isCancel as R$1, log as R, outro as fe, spinner as vt } from "./dist.mjs";
1
+ import { autocomplete, confirm, intro, isCancel, log, outro, spinner } from "./dist.mjs";
2
2
  import { a as getUniqueProviders, c as formatModelHint, d as formatThroughput, f as fetchModels, h as parseModelSlug, i as fetchModelEndpoints, l as formatModelLabel, m as parseModelAuthor, o as formatContextLength, p as formatPrice, s as formatLatency, u as formatPricing } from "./providers.mjs";
3
3
  import { addOverrideCommand } from "./add.mjs";
4
4
  //#region src/commands/config/browse.ts
@@ -10,23 +10,23 @@ function toOption(m) {
10
10
  };
11
11
  }
12
12
  function displayModelDetails(model) {
13
- R.success(`${model.name || model.id}`);
13
+ log.success(`${model.name || model.id}`);
14
14
  if (model.description) {
15
15
  const desc = model.description.length > 200 ? `${model.description.slice(0, 200)}...` : model.description;
16
- R.info(` ${desc}`);
16
+ log.info(` ${desc}`);
17
17
  }
18
- R.info(` Context: ${formatContextLength(model.context_length)} tokens`);
19
- if (model.top_provider?.max_completion_tokens) R.info(` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`);
20
- R.info(` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`);
21
- if (model.pricing.input_cache_read && model.pricing.input_cache_read !== "0") R.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);
22
- if (model.pricing.input_cache_write && model.pricing.input_cache_write !== "0") R.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);
23
- if (model.architecture?.modality) R.info(` Modality: ${model.architecture.modality}`);
24
- if (model.supported_parameters?.length) R.info(` Parameters: ${model.supported_parameters.join(", ")}`);
18
+ log.info(` Context: ${formatContextLength(model.context_length)} tokens`);
19
+ if (model.top_provider?.max_completion_tokens) log.info(` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`);
20
+ log.info(` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`);
21
+ if (model.pricing.input_cache_read && model.pricing.input_cache_read !== "0") log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);
22
+ if (model.pricing.input_cache_write && model.pricing.input_cache_write !== "0") log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);
23
+ if (model.architecture?.modality) log.info(` Modality: ${model.architecture.modality}`);
24
+ if (model.supported_parameters?.length) log.info(` Parameters: ${model.supported_parameters.join(", ")}`);
25
25
  }
26
26
  async function displayProviders(client, model) {
27
27
  const author = parseModelAuthor(model.id);
28
28
  const slug = parseModelSlug(model.id);
29
- const s = vt();
29
+ const s = spinner();
30
30
  s.start("Checking providers...");
31
31
  try {
32
32
  const endpoints = await fetchModelEndpoints(client, author, slug);
@@ -36,7 +36,7 @@ async function displayProviders(client, model) {
36
36
  const ep = endpoints.find((e) => e.tag === p.tag);
37
37
  const latency = formatLatency(ep?.latency_last_30m?.p50 ?? null);
38
38
  const throughput = formatThroughput(ep?.throughput_last_30m?.p50 ?? null);
39
- R.info(` ${p.providerName} (${p.tag}) — ${latency} · ${throughput}`);
39
+ log.info(` ${p.providerName} (${p.tag}) — ${latency} · ${throughput}`);
40
40
  }
41
41
  } catch {
42
42
  s.stop("Could not fetch providers");
@@ -44,8 +44,8 @@ async function displayProviders(client, model) {
44
44
  }
45
45
  /** Run the interactive "Browse models" flow. */
46
46
  async function browseModelsCommand(client) {
47
- ye("Browse Models");
48
- const s = vt();
47
+ intro("Browse Models");
48
+ const s = spinner();
49
49
  s.start("Loading models...");
50
50
  let models;
51
51
  try {
@@ -53,10 +53,10 @@ async function browseModelsCommand(client) {
53
53
  s.stop(`${models.length} models available`);
54
54
  } catch (error) {
55
55
  s.stop("Failed to load models");
56
- R.error(String(error));
56
+ log.error(String(error));
57
57
  return;
58
58
  }
59
- const modelId = await Vt({
59
+ const modelId = await autocomplete({
60
60
  message: "Search for a model",
61
61
  placeholder: "Type to search...",
62
62
  maxItems: 15,
@@ -67,14 +67,14 @@ async function browseModelsCommand(client) {
67
67
  },
68
68
  filter: (_search, _option) => true
69
69
  });
70
- if (R$1(modelId)) return;
70
+ if (isCancel(modelId)) return;
71
71
  const model = models.find((m) => m.id === modelId);
72
72
  if (!model) return;
73
73
  displayModelDetails(model);
74
74
  await displayProviders(client, model);
75
- const configure = await le({ message: `Configure routing for ${model.id}?` });
76
- if (R$1(configure) || !configure) {
77
- fe("Bye!");
75
+ const configure = await confirm({ message: `Configure routing for ${model.id}?` });
76
+ if (isCancel(configure) || !configure) {
77
+ outro("Bye!");
78
78
  return;
79
79
  }
80
80
  await addOverrideCommand(client);
@@ -1 +1 @@
1
- {"version":3,"file":"browse.mjs","names":["clack.spinner","clack.autocomplete","isCancel","clack.confirm"],"sources":["../src/commands/config/browse.ts"],"sourcesContent":["import * as clack from '@clack/prompts';\nimport { isCancel } from '@clack/prompts';\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js';\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js';\nimport {\n fetchModels,\n formatPrice,\n parseModelAuthor,\n parseModelSlug,\n} from '../../openrouter/models.js';\nimport type { OpenRouterModel } from '../../openrouter/types.js';\nimport { addOverrideCommand } from './add.js';\nimport {\n formatContextLength,\n formatLatency,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n formatThroughput,\n} from './format.js';\n\nfunction toOption(m: OpenRouterModel) {\n return { value: m.id, label: formatModelLabel(m), hint: formatModelHint(m) };\n}\n\nfunction displayModelDetails(model: OpenRouterModel): void {\n clack.log.success(`${model.name || model.id}`);\n if (model.description) {\n const desc =\n model.description.length > 200\n ? `${model.description.slice(0, 200)}...`\n : model.description;\n clack.log.info(` ${desc}`);\n }\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`);\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n );\n }\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n );\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`);\n }\n if (model.supported_parameters?.length) {\n clack.log.info(` Parameters: ${model.supported_parameters.join(', ')}`);\n }\n}\n\nasync function displayProviders(\n client: OpenRouterDataClient,\n model: OpenRouterModel,\n): Promise<void> {\n const author = parseModelAuthor(model.id);\n const slug = parseModelSlug(model.id);\n const s = clack.spinner();\n s.start('Checking providers...');\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug);\n const unique = getUniqueProviders(endpoints);\n s.stop(`${unique.length} providers available`);\n\n for (const p of unique) {\n const ep = endpoints.find(e => e.tag === p.tag);\n const latency = formatLatency(ep?.latency_last_30m?.p50 ?? null);\n const throughput = formatThroughput(ep?.throughput_last_30m?.p50 ?? null);\n clack.log.info(` ${p.providerName} (${p.tag}) — ${latency} · ${throughput}`);\n }\n } catch {\n s.stop('Could not fetch providers');\n }\n}\n\n/** Run the interactive \"Browse models\" flow. */\nexport async function browseModelsCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Browse Models');\n\n const s = clack.spinner();\n s.start('Loading models...');\n let models: OpenRouterModel[];\n try {\n models = await fetchModels(client);\n s.stop(`${models.length} models available`);\n } catch (error) {\n s.stop('Failed to load models');\n clack.log.error(String(error));\n return;\n }\n\n const modelId = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search...',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase();\n if (!query) return models.slice(0, 15).map(toOption);\n\n return models\n .filter(m => `${m.id} ${m.name}`.toLowerCase().includes(query))\n .slice(0, 15)\n .map(toOption);\n },\n filter: (_search: string, _option: { value: string }) => true,\n });\n\n if (isCancel(modelId)) return;\n\n const model = models.find(m => m.id === modelId);\n if (!model) return;\n\n displayModelDetails(model);\n await displayProviders(client, model);\n\n const configure = await clack.confirm({\n message: `Configure routing for ${model.id}?`,\n });\n\n if (isCancel(configure) || !configure) {\n clack.outro('Bye!');\n return;\n }\n\n await addOverrideCommand(client);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"browse.mjs","names":["clack.spinner","clack.autocomplete","clack.confirm"],"sources":["../src/commands/config/browse.ts"],"sourcesContent":["import * as clack from '@clack/prompts';\nimport { isCancel } from '@clack/prompts';\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js';\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js';\nimport {\n fetchModels,\n formatPrice,\n parseModelAuthor,\n parseModelSlug,\n} from '../../openrouter/models.js';\nimport type { OpenRouterModel } from '../../openrouter/types.js';\nimport { addOverrideCommand } from './add.js';\nimport {\n formatContextLength,\n formatLatency,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n formatThroughput,\n} from './format.js';\n\nfunction toOption(m: OpenRouterModel) {\n return { value: m.id, label: formatModelLabel(m), hint: formatModelHint(m) };\n}\n\nfunction displayModelDetails(model: OpenRouterModel): void {\n clack.log.success(`${model.name || model.id}`);\n if (model.description) {\n const desc =\n model.description.length > 200\n ? `${model.description.slice(0, 200)}...`\n : model.description;\n clack.log.info(` ${desc}`);\n }\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`);\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n );\n }\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n );\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`);\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`);\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`);\n }\n if (model.supported_parameters?.length) {\n clack.log.info(` Parameters: ${model.supported_parameters.join(', ')}`);\n }\n}\n\nasync function displayProviders(\n client: OpenRouterDataClient,\n model: OpenRouterModel,\n): Promise<void> {\n const author = parseModelAuthor(model.id);\n const slug = parseModelSlug(model.id);\n const s = clack.spinner();\n s.start('Checking providers...');\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug);\n const unique = getUniqueProviders(endpoints);\n s.stop(`${unique.length} providers available`);\n\n for (const p of unique) {\n const ep = endpoints.find(e => e.tag === p.tag);\n const latency = formatLatency(ep?.latency_last_30m?.p50 ?? null);\n const throughput = formatThroughput(ep?.throughput_last_30m?.p50 ?? null);\n clack.log.info(` ${p.providerName} (${p.tag}) — ${latency} · ${throughput}`);\n }\n } catch {\n s.stop('Could not fetch providers');\n }\n}\n\n/** Run the interactive \"Browse models\" flow. */\nexport async function browseModelsCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Browse Models');\n\n const s = clack.spinner();\n s.start('Loading models...');\n let models: OpenRouterModel[];\n try {\n models = await fetchModels(client);\n s.stop(`${models.length} models available`);\n } catch (error) {\n s.stop('Failed to load models');\n clack.log.error(String(error));\n return;\n }\n\n const modelId = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search...',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase();\n if (!query) return models.slice(0, 15).map(toOption);\n\n return models\n .filter(m => `${m.id} ${m.name}`.toLowerCase().includes(query))\n .slice(0, 15)\n .map(toOption);\n },\n filter: (_search: string, _option: { value: string }) => true,\n });\n\n if (isCancel(modelId)) return;\n\n const model = models.find(m => m.id === modelId);\n if (!model) return;\n\n displayModelDetails(model);\n await displayProviders(client, model);\n\n const configure = await clack.confirm({\n message: `Configure routing for ${model.id}?`,\n });\n\n if (isCancel(configure) || !configure) {\n clack.outro('Bye!');\n return;\n }\n\n await addOverrideCommand(client);\n}\n"],"mappings":"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"}