proxitor 0.4.0 → 0.5.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
@@ -109,8 +109,20 @@ 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
+
114
126
  ### Provider routing
115
127
 
116
128
  Control which provider handles your requests. All three options accept a string or an array:
@@ -245,6 +257,8 @@ The wizard asks for:
245
257
 
246
258
  - **OpenRouter API key** — stored in config or set as `OPENROUTER_API_KEY` env var
247
259
  - **Port** — default `8828` (avoids conflicts with common dev servers on 8080)
260
+ - **API base URL** — default `https://openrouter.ai/api/v1`; change for self-hosted or custom endpoints
261
+ - **Authentication type** — `bearer` (default) or `oauth`; use `oauth` for custom proxy providers that pass tokens in the `Authorization: OAuth ...` header
248
262
  - **Host** — all interfaces (`0.0.0.0`) or localhost only (`127.0.0.1`)
249
263
  - **Save location** — project directory, `~/.config/proxitor/`, or `$XDG_CONFIG_HOME/proxitor/`
250
264
 
@@ -332,6 +346,7 @@ The interface uses live data from the OpenRouter API — model search with type-
332
346
  | `-c, --config <path>` | auto-discovered | Path to config file |
333
347
  | `--openrouter-key <key>` | `$OPENROUTER_API_KEY` | OpenRouter API key |
334
348
  | `--verbose` | `false` | Enable verbose logging |
349
+ | `--no-config` | | Skip config file discovery |
335
350
  | `-v, --version` | | Print version |
336
351
  | `--help` | | Print help |
337
352
 
@@ -355,4 +370,4 @@ pnpm check # typecheck + biome + test (full CI)
355
370
 
356
371
  ## License
357
372
 
358
- [MIT](./LICENSE)
373
+ [MIT](./LICENSE)
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 { 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(client: OpenRouterClient): 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: 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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"}
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 { 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(client: OpenRouterClient): 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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"}
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,32 @@ 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
+ authType: _enum(["bearer", "oauth"]).default("bearer"),
10593
+ verbose: boolean().default(false),
10594
+ bodyLimit: string$1().min(1).default("50mb"),
10600
10595
  provider: providerConfigSchema.optional(),
10601
- attributionReferer: string$1().min(1),
10602
- attributionTitle: string$1().min(1),
10596
+ attributionReferer: string$1().min(1).default("http://localhost"),
10597
+ attributionTitle: string$1().min(1).default("proxitor"),
10603
10598
  headers: record(string$1(), string$1()).optional(),
10604
10599
  modelOverrides: record(string$1().min(1), modelOverrideSchema).optional()
10605
- }).strict().partial();
10606
- /** Wraps YAML/JSON parse errors with the config file path */
10600
+ }).strict();
10601
+ const DEFAULTS = proxyConfigSchema.parse({});
10602
+ const proxyConfigFileSchema = proxyConfigSchema.partial();
10607
10603
  var ConfigParseError = class extends Error {
10608
10604
  constructor(filePath, cause) {
10609
10605
  super(`Failed to parse config file ${filePath}: ${cause?.message ?? "unknown error"}`, { cause });
10610
10606
  this.name = "ConfigParseError";
10611
10607
  }
10612
10608
  };
10613
- /** Formats zod validation issues into a readable multi-line message */
10614
10609
  var ConfigValidationError = class extends Error {
10615
10610
  constructor(filePath, zodError) {
10616
10611
  const lines = zodError.issues.map((issue) => {
@@ -10622,6 +10617,10 @@ var ConfigValidationError = class extends Error {
10622
10617
  };
10623
10618
  //#endregion
10624
10619
  //#region src/utils.ts
10620
+ /** Format an API key with the appropriate auth prefix based on authType. Defaults to Bearer. */
10621
+ function formatAuthHeader(key, authType) {
10622
+ return `${authType === "oauth" ? "OAuth" : "Bearer"} ${key}`;
10623
+ }
10625
10624
  /** Normalize a single string or array of strings to an array. Returns undefined for empty arrays. */
10626
10625
  function toArray(value) {
10627
10626
  if (value === void 0) return void 0;
@@ -10639,17 +10638,6 @@ function tryParseBody(raw) {
10639
10638
  }
10640
10639
  //#endregion
10641
10640
  //#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
10641
  const ARRAY_FIELDS = [
10654
10642
  {
10655
10643
  key: "only",
@@ -10668,7 +10656,6 @@ const ARRAY_FIELDS = [
10668
10656
  apiName: "quantizations"
10669
10657
  }
10670
10658
  ];
10671
- /** Direct camelCase → snake_case field mappings */
10672
10659
  const DIRECT_FIELDS = [
10673
10660
  {
10674
10661
  key: "sort",
@@ -10703,7 +10690,6 @@ const DIRECT_FIELDS = [
10703
10690
  apiName: "preferred_max_latency"
10704
10691
  }
10705
10692
  ];
10706
- /** Build the provider routing object for OpenRouter request body injection */
10707
10693
  function buildProviderRouting(provider) {
10708
10694
  if (!provider) return void 0;
10709
10695
  const result = {};
@@ -10721,13 +10707,11 @@ function buildProviderRouting(provider) {
10721
10707
  if (result.order) result.allow_fallbacks = provider.allowFallbacks ?? true;
10722
10708
  return Object.keys(result).length > 0 ? result : void 0;
10723
10709
  }
10724
- /** Score a pattern against a model name. Higher = better match. -1 = no match. */
10725
10710
  function matchScore(pattern, modelName) {
10726
10711
  if (pattern === modelName) return modelName.length + 1e3;
10727
10712
  if (pattern.endsWith("*") && modelName.startsWith(pattern.slice(0, -1))) return pattern.length;
10728
10713
  return -1;
10729
10714
  }
10730
- /** Resolve the effective config for a given model by merging global defaults with the best-matching override */
10731
10715
  function resolveModelConfig(config, modelName) {
10732
10716
  const result = {
10733
10717
  provider: config.provider,
@@ -10754,23 +10738,25 @@ function resolveModelConfig(config, modelName) {
10754
10738
  return result;
10755
10739
  }
10756
10740
  async function loadConfig(options) {
10757
- const config = { ...DEFAULT_CONFIG };
10741
+ let fileConfig = {};
10758
10742
  if (!options.noConfig) {
10759
10743
  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;
10744
+ if (configPath) fileConfig = readConfigFile(configPath);
10745
+ }
10746
+ const merged = {
10747
+ ...DEFAULTS,
10748
+ ...fileConfig,
10749
+ ...options.host ? { host: options.host } : {},
10750
+ ...options.port ? { port: options.port } : {},
10751
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {},
10752
+ ...options.openrouterKey ? { openrouterKey: options.openrouterKey } : {}
10753
+ };
10754
+ if (!merged.openrouterKey) merged.openrouterKey = process.env.OPENROUTER_API_KEY ?? "";
10755
+ const result = proxyConfigSchema.safeParse(merged);
10756
+ if (!result.success) throw new ConfigValidationError("(merged config)", result.error);
10757
+ 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.");
10758
+ return result.data;
10772
10759
  }
10773
- /** Resolve XDG config directory: $XDG_CONFIG_HOME/proxitor or ~/.config/proxitor */
10774
10760
  function getXdgConfigDir() {
10775
10761
  const xdgHome = process.env.XDG_CONFIG_HOME;
10776
10762
  return xdgHome ? resolve(xdgHome, "proxitor") : join(homedir(), ".config", "proxitor");
@@ -11764,6 +11750,36 @@ function _getDefaultLogLevel() {
11764
11750
  //#region src/logger.ts
11765
11751
  const logger = createConsola().withTag("proxitor");
11766
11752
  //#endregion
11753
+ //#region src/openrouter/client.ts
11754
+ var OpenRouterClientError = class extends Error {
11755
+ status;
11756
+ constructor(status, message) {
11757
+ super(`OpenRouter API error (${status}): ${message}`);
11758
+ this.name = "OpenRouterClientError";
11759
+ this.status = status;
11760
+ }
11761
+ };
11762
+ /** HTTP client for OpenRouter REST endpoints. */
11763
+ var OpenRouterClient = class {
11764
+ apiKey;
11765
+ baseUrl;
11766
+ authType;
11767
+ constructor(apiKey, baseUrl, authType) {
11768
+ this.apiKey = apiKey;
11769
+ this.baseUrl = baseUrl;
11770
+ this.authType = authType;
11771
+ }
11772
+ async get(path) {
11773
+ const url = `${this.baseUrl}${path}`;
11774
+ const res = await fetch(url, { headers: { Authorization: formatAuthHeader(this.apiKey, this.authType) } });
11775
+ if (!res.ok) {
11776
+ const body = await res.text().catch(() => "");
11777
+ throw new OpenRouterClientError(res.status, body || res.statusText);
11778
+ }
11779
+ return res.json();
11780
+ }
11781
+ };
11782
+ //#endregion
11767
11783
  //#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.23/node_modules/@hono/node-server/dist/index.mjs
11768
11784
  var RequestError = class extends Error {
11769
11785
  constructor(message, options) {
@@ -14497,7 +14513,7 @@ function filterHeaders(incoming, blocklist) {
14497
14513
  /** Build request headers for upstream fetch */
14498
14514
  function buildRequestHeaders(incoming, config, inject, extraHeaders) {
14499
14515
  const headers = filterHeaders(incoming, STRIP_REQUEST);
14500
- headers.Authorization = `Bearer ${config.openrouterKey}`;
14516
+ headers.Authorization = formatAuthHeader(config.openrouterKey, config.authType);
14501
14517
  headers["HTTP-Referer"] = config.attributionReferer;
14502
14518
  headers["X-Title"] = config.attributionTitle;
14503
14519
  headers["Accept-Encoding"] = "identity";
@@ -14719,7 +14735,7 @@ function startProxyServer(config, onReady) {
14719
14735
  }
14720
14736
  //#endregion
14721
14737
  //#region src/version.ts
14722
- const version = "0.4.0";
14738
+ const version = "0.5.1";
14723
14739
  //#endregion
14724
14740
  //#region src/cli.ts
14725
14741
  const argv = process.argv.slice(2);
@@ -14758,14 +14774,14 @@ const startCommand = (0, import_cjs.command)({
14758
14774
  short: "p",
14759
14775
  type: import_cjs.number,
14760
14776
  description: "Proxy server port",
14761
- defaultValue: () => 8828,
14777
+ defaultValue: () => DEFAULTS.port,
14762
14778
  defaultValueIsSerializable: true
14763
14779
  }),
14764
14780
  host: (0, import_cjs.option)({
14765
14781
  long: "host",
14766
14782
  type: import_cjs.string,
14767
14783
  description: "Proxy server host",
14768
- defaultValue: () => "0.0.0.0",
14784
+ defaultValue: () => DEFAULTS.host,
14769
14785
  defaultValueIsSerializable: true
14770
14786
  }),
14771
14787
  config: (0, import_cjs.option)({
@@ -14808,9 +14824,15 @@ const startCommand = (0, import_cjs.command)({
14808
14824
  }
14809
14825
  }
14810
14826
  });
14811
- const withApiKey = (fn) => async (args) => {
14827
+ const withClient = (fn) => async (args) => {
14812
14828
  const apiKey = await resolveApiKey(args.config ?? void 0, args.openrouterKey ?? void 0);
14813
- if (apiKey) await fn(apiKey);
14829
+ if (!apiKey) return;
14830
+ try {
14831
+ const cfg = await loadConfig({ configPath: args.config ?? void 0 });
14832
+ await fn(new OpenRouterClient(apiKey, cfg.openrouterBaseUrl, cfg.authType));
14833
+ } catch (error) {
14834
+ logger.error("Failed to load config:", error);
14835
+ }
14814
14836
  };
14815
14837
  const rootCli = (0, import_cjs.subcommands)({
14816
14838
  name: "proxitor",
@@ -14826,13 +14848,13 @@ const rootCli = (0, import_cjs.subcommands)({
14826
14848
  name: "add",
14827
14849
  description: "Add model override",
14828
14850
  args: configOptionArgs,
14829
- handler: withApiKey(async (k) => (await import("./add.mjs")).addOverrideCommand(k))
14851
+ handler: withClient(async (client) => (await import("./add.mjs")).addOverrideCommand(client))
14830
14852
  }),
14831
14853
  edit: (0, import_cjs.command)({
14832
14854
  name: "edit",
14833
14855
  description: "Edit model override",
14834
14856
  args: configOptionArgs,
14835
- handler: withApiKey(async (k) => (await import("./edit.mjs")).editOverrideCommand(k))
14857
+ handler: withClient(async (client) => (await import("./edit.mjs")).editOverrideCommand(client))
14836
14858
  }),
14837
14859
  remove: (0, import_cjs.command)({
14838
14860
  name: "remove",
@@ -14850,7 +14872,7 @@ const rootCli = (0, import_cjs.subcommands)({
14850
14872
  name: "browse",
14851
14873
  description: "Browse models",
14852
14874
  args: configOptionArgs,
14853
- handler: withApiKey(async (k) => (await import("./browse.mjs")).browseModelsCommand(k))
14875
+ handler: withClient(async (client) => (await import("./browse.mjs")).browseModelsCommand(client))
14854
14876
  }),
14855
14877
  validate: (0, import_cjs.command)({
14856
14878
  name: "validate",
@@ -14862,7 +14884,7 @@ const rootCli = (0, import_cjs.subcommands)({
14862
14884
  name: "menu",
14863
14885
  description: "Interactive configuration menu",
14864
14886
  args: configOptionArgs,
14865
- handler: withApiKey(async (k) => (await import("./config2.mjs")).runConfigMenu(k))
14887
+ handler: withClient(async (client) => (await import("./config2.mjs")).runConfigMenu(client))
14866
14888
  }),
14867
14889
  wizard: (0, import_cjs.command)({
14868
14890
  name: "wizard",
@@ -14896,6 +14918,6 @@ const handleError = async (err) => {
14896
14918
  if (argv.some((a) => !a.startsWith("-")) || isInfo) (0, import_cjs.run)(rootCli, argv).catch((err) => void handleError(err));
14897
14919
  else (0, import_cjs.run)(startCommand, argv).catch((err) => void handleError(err));
14898
14920
  //#endregion
14899
- export { __commonJSMin$1 as a, readConfigFile as i, findConfigFile as n, __require as o, getXdgConfigDir as r, logger as t };
14921
+ export { DEFAULTS as a, readConfigFile as i, findConfigFile as n, __commonJSMin$1 as o, getXdgConfigDir as r, __require as s, logger as t };
14900
14922
 
14901
14923
  //# sourceMappingURL=cli.mjs.map