proxitor 0.5.0 → 0.5.2

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
@@ -109,8 +109,37 @@ proxitor.config.yaml → proxitor.config.yml → proxitor.config.json
109
109
 
110
110
  **Priority:** CLI flags > config file > environment variables > defaults
111
111
 
112
+ All defaults are derived from a single Zod schema (`DEFAULTS`) — no hardcoded constants scattered across modules. Config values are validated through Zod on load, including the final merged result.
113
+
112
114
  See [`proxitor.config.example.yaml`](./proxitor.config.example.yaml) for the complete reference.
113
115
 
116
+ ### Authentication type
117
+
118
+ By default, proxitor sends the API key as a `Bearer` token (`Authorization: Bearer sk-...`). If you're using a custom proxy provider that expects an `OAuth` header instead, set `authType` to `oauth`:
119
+
120
+ ```yaml
121
+ authType: oauth # "bearer" (default) or "oauth"
122
+ ```
123
+
124
+ This changes the header to `Authorization: OAuth sk-...`.
125
+
126
+ ### Custom API URL and data fallback
127
+
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
+
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)
131
+ - **`openrouterDataUrl`** — set this explicitly to control the primary URL for data fetching, independent of `openrouterBaseUrl` (which is used for proxying requests)
132
+
133
+ ```yaml
134
+ # Proxy requests go to custom service, data fetching falls back to OpenRouter
135
+ openrouterBaseUrl: 'https://custom-service.example.com/v1'
136
+
137
+ # Explicitly set the primary data URL (optional, defaults to openrouterBaseUrl)
138
+ # openrouterDataUrl: 'https://openrouter.ai/api/v1'
139
+ ```
140
+
141
+ When a fallback occurs, proxitor logs a warning: `Custom API did not return providers, using OpenRouter data as fallback`.
142
+
114
143
  ### Provider routing
115
144
 
116
145
  Control which provider handles your requests. All three options accept a string or an array:
@@ -246,6 +275,8 @@ The wizard asks for:
246
275
  - **OpenRouter API key** — stored in config or set as `OPENROUTER_API_KEY` env var
247
276
  - **Port** — default `8828` (avoids conflicts with common dev servers on 8080)
248
277
  - **API base URL** — default `https://openrouter.ai/api/v1`; change for self-hosted or custom endpoints
278
+ - **Data URL** — separate URL for provider/model data fetching; falls back to OpenRouter automatically if the custom API doesn't support these endpoints
279
+ - **Authentication type** — `bearer` (default) or `oauth`; use `oauth` for custom proxy providers that pass tokens in the `Authorization: OAuth ...` header
249
280
  - **Host** — all interfaces (`0.0.0.0`) or localhost only (`127.0.0.1`)
250
281
  - **Save location** — project directory, `~/.config/proxitor/`, or `$XDG_CONFIG_HOME/proxitor/`
251
282
 
@@ -333,6 +364,7 @@ The interface uses live data from the OpenRouter API — model search with type-
333
364
  | `-c, --config <path>` | auto-discovered | Path to config file |
334
365
  | `--openrouter-key <key>` | `$OPENROUTER_API_KEY` | OpenRouter API key |
335
366
  | `--verbose` | `false` | Enable verbose logging |
367
+ | `--no-config` | | Skip config file discovery |
336
368
  | `-v, --version` | | Print version |
337
369
  | `--help` | | Print help |
338
370
 
package/dist/add.mjs CHANGED
@@ -1,13 +1,12 @@
1
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";
2
- import { c as formatModelHint, f as fetchModels, g as OpenRouterClient, l as formatModelLabel, n as selectProvidersByMode, o as formatContextLength, p as formatPrice, r as selectRoutingMode, t as fetchProvidersForModel, u as formatPricing } from "./providers.mjs";
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
- async function addOverrideCommand(apiKey) {
7
+ async function addOverrideCommand(client) {
8
8
  ye("Add Model Override");
9
9
  const configPath = requireConfigPath();
10
- const client = new OpenRouterClient(apiKey);
11
10
  const models = await loadModelsWithSpinner(client);
12
11
  if (!models) return;
13
12
  const modelId = await searchModel(models);
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 { OpenRouterClient } from '../../openrouter/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(apiKey: string): Promise<void> {\n clack.intro('Add Model Override')\n\n const configPath = requireConfigPath()\n const client = new OpenRouterClient(apiKey)\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: OpenRouterClient,\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: OpenRouterClient,\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","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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"}
package/dist/browse.mjs CHANGED
@@ -1,5 +1,5 @@
1
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";
2
- import { a as getUniqueProviders, c as formatModelHint, d as formatThroughput, f as fetchModels, g as OpenRouterClient, 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";
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
5
5
  function toOption(m) {
@@ -43,9 +43,8 @@ async function displayProviders(client, model) {
43
43
  }
44
44
  }
45
45
  /** Run the interactive "Browse models" flow. */
46
- async function browseModelsCommand(apiKey) {
46
+ async function browseModelsCommand(client) {
47
47
  ye("Browse Models");
48
- const client = new OpenRouterClient(apiKey);
49
48
  const s = vt();
50
49
  s.start("Loading models...");
51
50
  let models;
@@ -78,7 +77,7 @@ async function browseModelsCommand(apiKey) {
78
77
  fe("Bye!");
79
78
  return;
80
79
  }
81
- await addOverrideCommand(apiKey);
80
+ await addOverrideCommand(client);
82
81
  }
83
82
  //#endregion
84
83
  export { browseModelsCommand };
@@ -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 { OpenRouterClient } from '../../openrouter/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: OpenRouterClient,\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(apiKey: string): Promise<void> {\n clack.intro('Browse Models')\n\n const client = new OpenRouterClient(apiKey)\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(apiKey)\n}\n"],"mappings":"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"}
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":";;;;AAqBA,SAAS,SAAS,GAAoB;CACpC,OAAO;EAAE,OAAO,EAAE;EAAI,OAAO,iBAAiB,CAAC;EAAG,MAAM,gBAAgB,CAAC;CAAE;AAC7E;AAEA,SAAS,oBAAoB,OAA8B;CACzD,EAAU,QAAQ,GAAG,MAAM,QAAQ,MAAM,IAAI;CAC7C,IAAI,MAAM,aAAa;EACrB,MAAM,OACJ,MAAM,YAAY,SAAS,MACvB,GAAG,MAAM,YAAY,MAAM,GAAG,GAAG,EAAE,OACnC,MAAM;EACZ,EAAU,KAAK,KAAK,MAAM;CAC5B;CACA,EAAU,KAAK,cAAc,oBAAoB,MAAM,cAAc,EAAE,QAAQ;CAC/E,IAAI,MAAM,cAAc,uBACtB,EAAU,KACR,iBAAiB,oBAAoB,MAAM,aAAa,qBAAqB,EAAE,QACjF;CAEF,EAAU,KACR,cAAc,cAAc,MAAM,QAAQ,QAAQ,MAAM,QAAQ,UAAU,GAC5E;CACA,IAAI,MAAM,QAAQ,oBAAoB,MAAM,QAAQ,qBAAqB,KACvE,EAAU,KAAK,iBAAiB,YAAY,MAAM,QAAQ,gBAAgB,GAAG;CAE/E,IAAI,MAAM,QAAQ,qBAAqB,MAAM,QAAQ,sBAAsB,KACzE,EAAU,KAAK,kBAAkB,YAAY,MAAM,QAAQ,iBAAiB,GAAG;CAEjF,IAAI,MAAM,cAAc,UACtB,EAAU,KAAK,eAAe,MAAM,aAAa,UAAU;CAE7D,IAAI,MAAM,sBAAsB,QAC9B,EAAU,KAAK,iBAAiB,MAAM,qBAAqB,KAAK,IAAI,GAAG;AAE3E;AAEA,eAAe,iBACb,QACA,OACe;CACf,MAAM,SAAS,iBAAiB,MAAM,EAAE;CACxC,MAAM,OAAO,eAAe,MAAM,EAAE;CACpC,MAAM,IAAIA,GAAc;CACxB,EAAE,MAAM,uBAAuB;CAC/B,IAAI;EACF,MAAM,YAAY,MAAM,oBAAoB,QAAQ,QAAQ,IAAI;EAChE,MAAM,SAAS,mBAAmB,SAAS;EAC3C,EAAE,KAAK,GAAG,OAAO,OAAO,qBAAqB;EAE7C,KAAK,MAAM,KAAK,QAAQ;GACtB,MAAM,KAAK,UAAU,MAAK,MAAK,EAAE,QAAQ,EAAE,GAAG;GAC9C,MAAM,UAAU,cAAc,IAAI,kBAAkB,OAAO,IAAI;GAC/D,MAAM,aAAa,iBAAiB,IAAI,qBAAqB,OAAO,IAAI;GACxE,EAAU,KAAK,OAAO,EAAE,aAAa,IAAI,EAAE,IAAI,MAAM,QAAQ,KAAK,YAAY;EAChF;CACF,QAAQ;EACN,EAAE,KAAK,2BAA2B;CACpC;AACF;;AAGA,eAAsB,oBAAoB,QAA6C;CACrF,GAAY,eAAe;CAE3B,MAAM,IAAIA,GAAc;CACxB,EAAE,MAAM,mBAAmB;CAC3B,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,YAAY,MAAM;EACjC,EAAE,KAAK,GAAG,OAAO,OAAO,kBAAkB;CAC5C,SAAS,OAAO;EACd,EAAE,KAAK,uBAAuB;EAC9B,EAAU,MAAM,OAAO,KAAK,CAAC;EAC7B;CACF;CAEA,MAAM,UAAU,MAAMC,GAAmB;EACvC,SAAS;EACT,aAAa;EACb,UAAU;EACV,UAAqC;GACnC,MAAM,QAAQ,KAAK,UAAU,KAAK,EAAE,YAAY;GAChD,IAAI,CAAC,OAAO,OAAO,OAAO,MAAM,GAAG,EAAE,EAAE,IAAI,QAAQ;GAEnD,OAAO,OACJ,QAAO,MAAK,GAAG,EAAE,GAAG,GAAG,EAAE,OAAO,YAAY,EAAE,SAAS,KAAK,CAAC,EAC7D,MAAM,GAAG,EAAE,EACX,IAAI,QAAQ;EACjB;EACA,SAAS,SAAiB,YAA+B;CAC3D,CAAC;CAED,IAAIC,IAAS,OAAO,GAAG;CAEvB,MAAM,QAAQ,OAAO,MAAK,MAAK,EAAE,OAAO,OAAO;CAC/C,IAAI,CAAC,OAAO;CAEZ,oBAAoB,KAAK;CACzB,MAAM,iBAAiB,QAAQ,KAAK;CAEpC,MAAM,YAAY,MAAMC,GAAc,EACpC,SAAS,yBAAyB,MAAM,GAAG,GAC7C,CAAC;CAED,IAAID,IAAS,SAAS,KAAK,CAAC,WAAW;EACrC,GAAY,MAAM;EAClB;CACF;CAEA,MAAM,mBAAmB,MAAM;AACjC"}
package/dist/cli.mjs CHANGED
@@ -10541,14 +10541,12 @@ function superRefine(fn, params) {
10541
10541
  }
10542
10542
  //#endregion
10543
10543
  //#region src/config-schema.ts
10544
- /** Percentile cutoffs for performance thresholds */
10545
10544
  const percentileCutoffsSchema = object({
10546
10545
  p50: number$1().positive().optional(),
10547
10546
  p75: number$1().positive().optional(),
10548
10547
  p90: number$1().positive().optional(),
10549
10548
  p99: number$1().positive().optional()
10550
10549
  }).strict();
10551
- /** Provider sorting options */
10552
10550
  const providerSortSchema = union([_enum([
10553
10551
  "price",
10554
10552
  "throughput",
@@ -10561,14 +10559,12 @@ const providerSortSchema = union([_enum([
10561
10559
  ]),
10562
10560
  partition: _enum(["model", "none"]).optional()
10563
10561
  }).strict()]);
10564
- /** Maximum pricing for a request */
10565
10562
  const maxPriceSchema = object({
10566
10563
  prompt: number$1().nonnegative().optional(),
10567
10564
  completion: number$1().nonnegative().optional(),
10568
10565
  request: number$1().nonnegative().optional(),
10569
10566
  image: number$1().nonnegative().optional()
10570
10567
  }).strict();
10571
- /** Provider routing configuration */
10572
10568
  const providerConfigSchema = object({
10573
10569
  only: union([string$1(), array(string$1())]).optional(),
10574
10570
  order: union([string$1(), array(string$1())]).optional(),
@@ -10584,33 +10580,33 @@ const providerConfigSchema = object({
10584
10580
  preferredMinThroughput: union([number$1().positive(), percentileCutoffsSchema]).optional(),
10585
10581
  preferredMaxLatency: union([number$1().positive(), percentileCutoffsSchema]).optional()
10586
10582
  }).strict();
10587
- /** Per-model override: layers on top of global config */
10588
10583
  const modelOverrideSchema = object({
10589
10584
  provider: providerConfigSchema.optional(),
10590
10585
  headers: record(string$1(), string$1()).optional()
10591
10586
  }).strict();
10592
- /** Schema for validating raw file content — all top-level keys optional */
10593
- const proxyConfigFileSchema = object({
10594
- host: string$1().min(1),
10595
- port: number$1().int().min(1).max(65535),
10596
- openrouterKey: string$1(),
10597
- openrouterBaseUrl: string$1().url(),
10598
- verbose: boolean(),
10599
- bodyLimit: string$1().min(1),
10587
+ const proxyConfigSchema = object({
10588
+ host: string$1().min(1).default("0.0.0.0"),
10589
+ port: number$1().int().min(1).max(65535).default(8828),
10590
+ openrouterKey: string$1().default(""),
10591
+ openrouterBaseUrl: string$1().url().default("https://openrouter.ai/api/v1"),
10592
+ openrouterDataUrl: string$1().url().optional(),
10593
+ authType: _enum(["bearer", "oauth"]).default("bearer"),
10594
+ verbose: boolean().default(false),
10595
+ bodyLimit: string$1().min(1).default("50mb"),
10600
10596
  provider: providerConfigSchema.optional(),
10601
- attributionReferer: string$1().min(1),
10602
- attributionTitle: string$1().min(1),
10597
+ attributionReferer: string$1().min(1).default("http://localhost"),
10598
+ attributionTitle: string$1().min(1).default("proxitor"),
10603
10599
  headers: record(string$1(), string$1()).optional(),
10604
10600
  modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
10605
- }).strict().partial();
10606
- /** Wraps YAML/JSON parse errors with the config file path */
10601
+ }).strict();
10602
+ const DEFAULTS = proxyConfigSchema.parse({});
10603
+ const proxyConfigFileSchema = proxyConfigSchema.partial();
10607
10604
  var ConfigParseError = class extends Error {
10608
10605
  constructor(filePath, cause) {
10609
10606
  super(`Failed to parse config file ${filePath}: ${cause?.message ?? "unknown error"}`, { cause });
10610
10607
  this.name = "ConfigParseError";
10611
10608
  }
10612
10609
  };
10613
- /** Formats zod validation issues into a readable multi-line message */
10614
10610
  var ConfigValidationError = class extends Error {
10615
10611
  constructor(filePath, zodError) {
10616
10612
  const lines = zodError.issues.map((issue) => {
@@ -10622,6 +10618,10 @@ var ConfigValidationError = class extends Error {
10622
10618
  };
10623
10619
  //#endregion
10624
10620
  //#region src/utils.ts
10621
+ /** Format an API key with the appropriate auth prefix based on authType. Defaults to Bearer. */
10622
+ function formatAuthHeader(key, authType) {
10623
+ return `${authType === "oauth" ? "OAuth" : "Bearer"} ${key}`;
10624
+ }
10625
10625
  /** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
10626
10626
  function toArray(value) {
10627
10627
  if (value === void 0) return void 0;
@@ -10639,17 +10639,6 @@ function tryParseBody(raw) {
10639
10639
  }
10640
10640
  //#endregion
10641
10641
  //#region src/config.ts
10642
- const DEFAULT_CONFIG = {
10643
- host: "0.0.0.0",
10644
- port: 8828,
10645
- openrouterKey: "",
10646
- openrouterBaseUrl: "https://openrouter.ai/api/v1",
10647
- verbose: false,
10648
- bodyLimit: "50mb",
10649
- attributionReferer: "http://localhost",
10650
- attributionTitle: "proxitor"
10651
- };
10652
- /** Fields that need toArray normalization (string | string[] → string[] | undefined) */
10653
10642
  const ARRAY_FIELDS = [
10654
10643
  {
10655
10644
  key: "only",
@@ -10668,7 +10657,6 @@ const ARRAY_FIELDS = [
10668
10657
  apiName: "quantizations"
10669
10658
  }
10670
10659
  ];
10671
- /** Direct camelCase → snake_case field mappings */
10672
10660
  const DIRECT_FIELDS = [
10673
10661
  {
10674
10662
  key: "sort",
@@ -10703,7 +10691,6 @@ const DIRECT_FIELDS = [
10703
10691
  apiName: "preferred_max_latency"
10704
10692
  }
10705
10693
  ];
10706
- /** Build the provider routing object for OpenRouter request body injection */
10707
10694
  function buildProviderRouting(provider) {
10708
10695
  if (!provider) return void 0;
10709
10696
  const result = {};
@@ -10721,13 +10708,11 @@ function buildProviderRouting(provider) {
10721
10708
  if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
10722
10709
  return Object.keys(result).length > 0 ? result : void 0;
10723
10710
  }
10724
- /** Score a pattern against a model name. Higher = better match. -1 = no match. */
10725
10711
  function matchScore(pattern, modelName) {
10726
10712
  if (pattern === modelName) return modelName.length + 1e3;
10727
10713
  if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
10728
10714
  return -1;
10729
10715
  }
10730
- /** Resolve the effective config for a given model by merging global defaults with the best-matching override */
10731
10716
  function resolveModelConfig(config, modelName) {
10732
10717
  const result = {
10733
10718
  provider: config.provider,
@@ -10754,23 +10739,25 @@ function resolveModelConfig(config, modelName) {
10754
10739
  return result;
10755
10740
  }
10756
10741
  async function loadConfig(options) {
10757
- const config = { ...DEFAULT_CONFIG };
10742
+ let fileConfig = {};
10758
10743
  if (!options.noConfig) {
10759
10744
  const configPath = findConfigFile(options.configPath);
10760
- if (configPath) {
10761
- const fileConfig = readConfigFile(configPath);
10762
- Object.assign(config, fileConfig);
10763
- }
10764
- }
10765
- if (options.host) config.host = options.host;
10766
- if (options.port) config.port = options.port;
10767
- if (options.verbose) config.verbose = options.verbose;
10768
- if (options.openrouterKey) config.openrouterKey = options.openrouterKey;
10769
- else if (!config.openrouterKey) config.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
10770
- if (!config.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
10771
- return config;
10745
+ if (configPath) fileConfig = readConfigFile(configPath);
10746
+ }
10747
+ const merged = {
10748
+ ...DEFAULTS,
10749
+ ...fileConfig,
10750
+ ...options.host ? { host: options.host } : {},
10751
+ ...options.port ? { port: options.port } : {},
10752
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {},
10753
+ ...options.openrouterKey ? { openrouterKey: options.openrouterKey } : {}
10754
+ };
10755
+ if (!merged.openrouterKey) merged.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
10756
+ const result = proxyConfigSchema.safeParse(merged);
10757
+ if (!result.success) throw new ConfigValidationError("(merged config)", result.error);
10758
+ if (!result.data.openrouterKey) throw new Error("OpenRouter API key is required. Set OPENROUTER_API_KEY env var, pass --openrouter-key flag, or set it in config file.");
10759
+ return result.data;
10772
10760
  }
10773
- /** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */
10774
10761
  function getXdgConfigDir() {
10775
10762
  const xdgHome = process.env.XDG_CONFIG_HOME;
10776
10763
  return xdgHome ? resolve(xdgHome, "proxitor") : join(homedir(), ".config", "proxitor");
@@ -11764,6 +11751,126 @@ function _getDefaultLogLevel() {
11764
11751
  //#region src/logger.ts
11765
11752
  const logger = createConsola().withTag("proxitor");
11766
11753
  //#endregion
11754
+ //#region src/openrouter/client.ts
11755
+ var OpenRouterClientError = class extends Error {
11756
+ status;
11757
+ constructor(status, message) {
11758
+ super(`OpenRouter API error (${status}): ${message}`);
11759
+ this.name = "OpenRouterClientError";
11760
+ this.status = status;
11761
+ }
11762
+ };
11763
+ /** HTTP client for OpenRouter REST endpoints. */
11764
+ var OpenRouterClient = class {
11765
+ apiKey;
11766
+ baseUrl;
11767
+ authType;
11768
+ constructor(apiKeyOrUrl, baseUrl, authType) {
11769
+ if (baseUrl !== void 0) {
11770
+ this.apiKey = apiKeyOrUrl;
11771
+ this.baseUrl = baseUrl;
11772
+ this.authType = authType;
11773
+ } else {
11774
+ this.apiKey = void 0;
11775
+ this.baseUrl = apiKeyOrUrl;
11776
+ this.authType = void 0;
11777
+ }
11778
+ }
11779
+ async get(path) {
11780
+ const url = `${this.baseUrl}${path}`;
11781
+ const headers = {};
11782
+ if (this.apiKey !== void 0 && this.authType !== void 0) headers.Authorization = formatAuthHeader(this.apiKey, this.authType);
11783
+ const res = await fetch(url, { headers });
11784
+ if (!res.ok) {
11785
+ const body = await res.text().catch(() => "");
11786
+ throw new OpenRouterClientError(res.status, body || res.statusText);
11787
+ }
11788
+ return res.json();
11789
+ }
11790
+ };
11791
+ //#endregion
11792
+ //#region src/openrouter/data-client.ts
11793
+ const OPENROUTER_FALLBACK_URL = "https://openrouter.ai/api/v1";
11794
+ function isValidProvidersResponse(data) {
11795
+ return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
11796
+ }
11797
+ function isValidModelsResponse(data) {
11798
+ return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
11799
+ }
11800
+ function isValidEndpointsResponse(data) {
11801
+ return typeof data === "object" && data !== null && "data" in data && typeof data.data === "object" && data.data !== null && "endpoints" in data.data && Array.isArray(data.data.endpoints);
11802
+ }
11803
+ /**
11804
+ * Client for fetching provider/model data with automatic fallback to OpenRouter.
11805
+ *
11806
+ * When the primary API (openrouterDataUrl or openrouterBaseUrl) doesn't support
11807
+ * OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api/v1
11808
+ * which hosts public, unauthenticated endpoints for /providers, /models, etc.
11809
+ */
11810
+ var OpenRouterDataClient = class {
11811
+ primaryClient;
11812
+ fallbackClient;
11813
+ skipFallback;
11814
+ onFallback;
11815
+ constructor(config) {
11816
+ const primaryUrl = config.openrouterDataUrl ?? config.openrouterBaseUrl;
11817
+ this.skipFallback = primaryUrl === OPENROUTER_FALLBACK_URL;
11818
+ this.primaryClient = new OpenRouterClient(config.apiKey, primaryUrl, config.authType);
11819
+ this.fallbackClient = new OpenRouterClient(OPENROUTER_FALLBACK_URL);
11820
+ this.onFallback = config.onFallback;
11821
+ }
11822
+ async fetchProviders() {
11823
+ return (await this.withFallback("/providers", () => this.primaryClient.get("/providers"), isValidProvidersResponse)).data.data;
11824
+ }
11825
+ async fetchModels() {
11826
+ return (await this.withFallback("/models", () => this.primaryClient.get("/models"), isValidModelsResponse)).data.data;
11827
+ }
11828
+ async fetchModelEndpoints(author, slug) {
11829
+ const path = `/models/${author}/${slug}/endpoints`;
11830
+ return (await this.withFallback(path, () => this.primaryClient.get(path), isValidEndpointsResponse)).data.data.endpoints ?? [];
11831
+ }
11832
+ /**
11833
+ * Try primary, validate response, fallback on failure.
11834
+ * Network errors get 1 retry before fallback.
11835
+ */
11836
+ async withFallback(path, primaryFn, validate) {
11837
+ if (this.skipFallback) {
11838
+ const data = await primaryFn();
11839
+ if (!validate(data)) throw new Error(`Unexpected response format from primary API for ${path}`);
11840
+ return {
11841
+ data,
11842
+ usedFallback: false
11843
+ };
11844
+ }
11845
+ try {
11846
+ const data = await primaryFn();
11847
+ if (validate(data)) return {
11848
+ data,
11849
+ usedFallback: false
11850
+ };
11851
+ } catch (error) {
11852
+ if (error instanceof Error && isNetworkError(error)) try {
11853
+ const data = await primaryFn();
11854
+ if (validate(data)) return {
11855
+ data,
11856
+ usedFallback: false
11857
+ };
11858
+ } catch {}
11859
+ }
11860
+ this.onFallback?.(path);
11861
+ const data = await this.fallbackClient.get(path);
11862
+ if (!validate(data)) throw new Error(`Unexpected response format from OpenRouter fallback for ${path}`);
11863
+ return {
11864
+ data,
11865
+ usedFallback: true
11866
+ };
11867
+ }
11868
+ };
11869
+ function isNetworkError(error) {
11870
+ const message = error.message.toLowerCase();
11871
+ return message.includes("fetch") || message.includes("network") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("timeout") || message.includes("aborted") || error.name === "TypeError";
11872
+ }
11873
+ //#endregion
11767
11874
  //#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.23/node_modules/@hono/node-server/dist/index.mjs
11768
11875
  var RequestError = class extends Error {
11769
11876
  constructor(message, options) {
@@ -14497,7 +14604,7 @@ function filterHeaders(incoming, blocklist) {
14497
14604
  /** Build request headers for upstream fetch */
14498
14605
  function buildRequestHeaders(incoming, config, inject, extraHeaders) {
14499
14606
  const headers = filterHeaders(incoming, STRIP_REQUEST);
14500
- headers.Authorization = `Bearer ${config.openrouterKey}`;
14607
+ headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
14501
14608
  headers["HTTP-Referer"] = config.attributionReferer;
14502
14609
  headers["X-Title"] = config.attributionTitle;
14503
14610
  headers["Accept-Encoding"] = "identity";
@@ -14719,7 +14826,7 @@ function startProxyServer(config, onReady) {
14719
14826
  }
14720
14827
  //#endregion
14721
14828
  //#region src/version.ts
14722
- const version = "0.5.0";
14829
+ const version = "0.5.2";
14723
14830
  //#endregion
14724
14831
  //#region src/cli.ts
14725
14832
  const argv = process.argv.slice(2);
@@ -14758,14 +14865,14 @@ const startCommand = (0, import_cjs.command)({
14758
14865
  short: "p",
14759
14866
  type: import_cjs.number,
14760
14867
  description: "Proxy server port",
14761
- defaultValue: () => 8828,
14868
+ defaultValue: () => DEFAULTS.port,
14762
14869
  defaultValueIsSerializable: true
14763
14870
  }),
14764
14871
  host: (0, import_cjs.option)({
14765
14872
  long: "host",
14766
14873
  type: import_cjs.string,
14767
14874
  description: "Proxy server host",
14768
- defaultValue: () => "0.0.0.0",
14875
+ defaultValue: () => DEFAULTS.host,
14769
14876
  defaultValueIsSerializable: true
14770
14877
  }),
14771
14878
  config: (0, import_cjs.option)({
@@ -14808,9 +14915,24 @@ const startCommand = (0, import_cjs.command)({
14808
14915
  }
14809
14916
  }
14810
14917
  });
14811
- const withApiKey = (fn) => async (args) => {
14918
+ const withClient = (fn) => async (args) => {
14812
14919
  const apiKey = await resolveApiKey(args.config ?? void 0, args.openrouterKey ?? void 0);
14813
- if (apiKey) await fn(apiKey);
14920
+ if (!apiKey) return;
14921
+ try {
14922
+ const cfg = await loadConfig({ configPath: args.config ?? void 0 });
14923
+ await fn(new OpenRouterDataClient({
14924
+ openrouterBaseUrl: cfg.openrouterBaseUrl,
14925
+ openrouterDataUrl: cfg.openrouterDataUrl,
14926
+ apiKey,
14927
+ authType: cfg.authType,
14928
+ onFallback: (path) => {
14929
+ const endpoint = path === "/providers" ? "providers" : path === "/models" ? "models" : "model providers";
14930
+ logger.warn(`Custom API did not return ${endpoint}, using OpenRouter data as fallback`);
14931
+ }
14932
+ }));
14933
+ } catch (error) {
14934
+ logger.error("Failed to load config:", error);
14935
+ }
14814
14936
  };
14815
14937
  const rootCli = (0, import_cjs.subcommands)({
14816
14938
  name: "proxitor",
@@ -14826,13 +14948,13 @@ const rootCli = (0, import_cjs.subcommands)({
14826
14948
  name: "add",
14827
14949
  description: "Add model override",
14828
14950
  args: configOptionArgs,
14829
- handler: withApiKey(async (k) => (await import("./add.mjs")).addOverrideCommand(k))
14951
+ handler: withClient(async (client) => (await import("./add.mjs")).addOverrideCommand(client))
14830
14952
  }),
14831
14953
  edit: (0, import_cjs.command)({
14832
14954
  name: "edit",
14833
14955
  description: "Edit model override",
14834
14956
  args: configOptionArgs,
14835
- handler: withApiKey(async (k) => (await import("./edit.mjs")).editOverrideCommand(k))
14957
+ handler: withClient(async (client) => (await import("./edit.mjs")).editOverrideCommand(client))
14836
14958
  }),
14837
14959
  remove: (0, import_cjs.command)({
14838
14960
  name: "remove",
@@ -14850,7 +14972,7 @@ const rootCli = (0, import_cjs.subcommands)({
14850
14972
  name: "browse",
14851
14973
  description: "Browse models",
14852
14974
  args: configOptionArgs,
14853
- handler: withApiKey(async (k) => (await import("./browse.mjs")).browseModelsCommand(k))
14975
+ handler: withClient(async (client) => (await import("./browse.mjs")).browseModelsCommand(client))
14854
14976
  }),
14855
14977
  validate: (0, import_cjs.command)({
14856
14978
  name: "validate",
@@ -14862,7 +14984,7 @@ const rootCli = (0, import_cjs.subcommands)({
14862
14984
  name: "menu",
14863
14985
  description: "Interactive configuration menu",
14864
14986
  args: configOptionArgs,
14865
- handler: withApiKey(async (k) => (await import("./config2.mjs")).runConfigMenu(k))
14987
+ handler: withClient(async (client) => (await import("./config2.mjs")).runConfigMenu(client))
14866
14988
  }),
14867
14989
  wizard: (0, import_cjs.command)({
14868
14990
  name: "wizard",
@@ -14896,6 +15018,6 @@ const handleError = async (err) => {
14896
15018
  if (argv.some((a) => !a.startsWith("-")) || isInfo) (0, import_cjs.run)(rootCli, argv).catch((err) => void handleError(err));
14897
15019
  else (0, import_cjs.run)(startCommand, argv).catch((err) => void handleError(err));
14898
15020
  //#endregion
14899
- export { __commonJSMin$1 as a, readConfigFile as i, findConfigFile as n, __require as o, getXdgConfigDir as r, logger as t };
15021
+ export { DEFAULTS as a, readConfigFile as i, findConfigFile as n, __commonJSMin$1 as o, getXdgConfigDir as r, __require as s, logger as t };
14900
15022
 
14901
15023
  //# sourceMappingURL=cli.mjs.map