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 +32 -0
- package/dist/add.mjs +2 -3
- package/dist/add.mjs.map +1 -1
- package/dist/browse.mjs +3 -4
- package/dist/browse.mjs.map +1 -1
- package/dist/cli.mjs +180 -58
- package/dist/cli.mjs.map +1 -1
- package/dist/config2.mjs +4 -4
- package/dist/config2.mjs.map +1 -1
- package/dist/dist.mjs +1 -1
- package/dist/dist2.mjs +1 -1
- package/dist/edit.mjs +4 -5
- package/dist/edit.mjs.map +1 -1
- package/dist/providers.mjs +9 -38
- package/dist/providers.mjs.map +1 -1
- package/dist/wizard.mjs +70 -36
- package/dist/wizard.mjs.map +1 -1
- package/package.json +1 -1
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,
|
|
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(
|
|
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":";;;;AAkBA,MAAM,iBAAiB;;AAGvB,eAAsB,mBAAmB,QAA+B;CACtE,GAAY,oBAAoB;CAEhC,MAAM,aAAa,kBAAkB;CACrC,MAAM,SAAS,IAAI,iBAAiB,MAAM;CAE1C,MAAM,SAAS,MAAM,sBAAsB,MAAM;CACjD,IAAI,CAAC,QAAQ;CAEb,MAAM,UAAU,MAAM,YAAY,MAAM;CACxC,IAAI,CAAC,SAAS;CAEd,IAAI,OAAO,YAAY,UAAU;CAEjC,IAAI,YAAY,gBAAgB;EAC9B,MAAM,UAAU,MAAM,aAAa,MAAM;EACzC,IAAI,CAAC,SAAS;EAGd,IADiB,kBAAkB,UACxB,EAAE,UAAU;GACrB,EAAU,KAAK,iBAAiB,QAAQ,oCAAoC;GAC5E;EACF;EAEA,MAAM,yBAAyB,YAAY,QAAQ,SAAS,IAAI;EAChE;CACF;CAEA,MAAM,WAAW,OAAO,MAAK,MAAK,EAAE,OAAO,OAAO;CAClD,IAAI,UAAU,iBAAiB,QAAQ;CAGvC,IADiB,kBAAkB,UACxB,EAAE,UAAU;EACrB,EAAU,KAAK,iBAAiB,QAAQ,oCAAoC;EAC5E;CACF;CAEA,MAAM,yBAAyB,YAAY,QAAQ,SAAS,KAAK;AACnE;AAEA,eAAe,sBACb,QACmC;CACnC,MAAM,IAAIA,GAAc;CACxB,EAAE,MAAM,mCAAmC;CAC3C,IAAI;EACF,MAAM,SAAS,MAAM,YAAY,MAAM;EACvC,EAAE,KAAK,GAAG,OAAO,OAAO,kBAAkB;EAC1C,OAAO;CACT,SAAS,OAAO;EACd,EAAE,KAAK,uBAAuB;EAC9B,EAAU,MAAM,OAAO,KAAK,CAAC;EAC7B,OAAO;CACT;AACF;AAEA,eAAe,YAAY,QAA4D;CACrF,MAAM,SAAS,MAAMC,GAAmB;EACtC,SAAS;EACT,aAAa;EACb,UAAU;EACV,UAAqC;GACnC,MAAM,QAAQ,KAAK,UAAU,KAAK,EAAE,YAAY;GAEhD,IAAI,CAAC,OACH,OAAO,CACL;IACE,OAAO;IACP,OAAO;GACT,CACF;GAeF,OAAO,CACL,GAbe,OACd,QAAO,MAAK;IAEX,OADa,GAAG,EAAE,GAAG,GAAG,EAAE,OAAO,YACvB,EAAE,SAAS,KAAK;GAC5B,CAAC,EACA,MAAM,GAAG,EAAE,EACX,KAAI,OAAM;IACT,OAAO,EAAE;IACT,OAAO,iBAAiB,CAAC;IACzB,MAAM,gBAAgB,CAAC;GACzB,EAGU,GACV;IAAE,OAAO;IAAgB,OAAO;GAA6C,CAC/E;EACF;EACA,SAAS,SAAiB,YAA+B;CAC3D,CAAC;CAED,IAAIC,IAAS,MAAM,GAAG,OAAO;CAC7B,OAAO;AACT;AAEA,eAAe,aAAa,QAAmD;CAC7E,MAAM,UAAU,MAAMC,GAAW;EAC/B,SAAS;EACT,aAAa;EACb,WAAU,MAAK;GACb,IAAI,CAAC,GAAG,KAAK,GAAG,OAAO;EAEzB;CACF,CAAC;CAED,IAAID,IAAS,OAAO,GAAG,OAAO;CAE9B,MAAM,MAAO,QAAmB,KAAK;CACrC,MAAM,UAAU,oBAAoB,KAAK,MAAM;CAC/C,IAAI,UAAU,GACZ,EAAU,KAAK,YAAY,IAAI,YAAY,QAAQ,UAAU;MAE7D,EAAU,KACR,YAAY,IAAI,6DAClB;CAGF,OAAO;AACT;AAEA,eAAe,yBACb,YACA,QACA,UACA,WACe;CACf,MAAM,OAAO,MAAM,kBAAkB,4BAA4B;CACjE,IAAIA,IAAS,IAAI,GAAG;CAEpB,IAAI,SAAS,QAAQ;EACnB,iBAAiB,YAAY,UAAU,CAAC,CAAC;EACzC,GAAY,gDAAgD;EAC5D;CACF;CAEA,MAAM,kBAAkB,MAAM,uBAAuB,QAAQ,UAAU,SAAS;CAChF,IAAI,CAAC,iBAAiB;CAEtB,MAAM,WAAW,MAAM,sBAAsB,MAAgB,eAAe;CAC5E,IAAI,CAAC,UAAU;CAEf,EAAU,KACR,yBAAyB,SAAS,SAAS,mBAAmB,QAAQ,GACxE;CAEA,MAAM,OAAO,MAAME,GAAc,EAAE,SAAS,kBAAkB,CAAC;CAC/D,IAAIF,IAAS,IAAI,KAAK,CAAC,MAAM;EAC3B,GAAY,WAAW;EACvB;CACF;CAEA,iBAAiB,YAAY,UAAU,QAAQ;CAC/C,GAAY,wBAAwB;AACtC;AAEA,SAAS,iBAAiB,OAA8B;CACtD,EAAU,KAAK,GAAG,MAAM,QAAQ,MAAM,IAAI;CAC1C,EAAU,KAAK,cAAc,oBAAoB,MAAM,cAAc,EAAE,QAAQ;CAC/E,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,uBACtB,EAAU,KACR,iBAAiB,oBAAoB,MAAM,aAAa,qBAAqB,EAAE,QACjF;CAEF,IAAI,MAAM,cAAc,UACtB,EAAU,KAAK,eAAe,MAAM,aAAa,UAAU;AAE/D;AAEA,SAAS,oBAAoB,SAAiB,QAAmC;CAC/E,IAAI,QAAQ,SAAS,GAAG,GAAG;EACzB,MAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;EAClC,OAAO,OAAO,QAAO,MAAK,EAAE,GAAG,WAAW,MAAM,CAAC,EAAE;CACrD;CACA,OAAO,OAAO,QAAO,MAAK,EAAE,OAAO,OAAO,EAAE;AAC9C;AAEA,SAAS,mBAAmB,UAA2C;CACrE,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS,YAAY,OAAO,SAAS,aAAa,UAAU;EAC9D,MAAM,IAAI,SAAS;EACnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,CAAC,GACzC,MAAM,KAAK,YAAY,IAAI,IAAI,KAAK,UAAU,KAAK,GAAG;CAE1D;CACA,OAAO,MAAM,KAAK,QAAQ,KAAK;AACjC"}
|
|
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,
|
|
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(
|
|
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(
|
|
80
|
+
await addOverrideCommand(client);
|
|
82
81
|
}
|
|
83
82
|
//#endregion
|
|
84
83
|
export { browseModelsCommand };
|
package/dist/browse.mjs.map
CHANGED
|
@@ -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 {
|
|
1
|
+
{"version":3,"file":"browse.mjs","names":["clack.spinner","clack.autocomplete","isCancel","clack.confirm"],"sources":["../src/commands/config/browse.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { OpenRouterDataClient } from '../../openrouter/data-client.js'\nimport { fetchModelEndpoints, getUniqueProviders } from '../../openrouter/endpoints.js'\nimport {\n fetchModels,\n formatPrice,\n parseModelAuthor,\n parseModelSlug,\n} from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\nimport { addOverrideCommand } from './add.js'\nimport {\n formatContextLength,\n formatLatency,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n formatThroughput,\n} from './format.js'\n\nfunction toOption(m: OpenRouterModel) {\n return { value: m.id, label: formatModelLabel(m), hint: formatModelHint(m) }\n}\n\nfunction displayModelDetails(model: OpenRouterModel): void {\n clack.log.success(`${model.name || model.id}`)\n if (model.description) {\n const desc =\n model.description.length > 200\n ? `${model.description.slice(0, 200)}...`\n : model.description\n clack.log.info(` ${desc}`)\n }\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`)\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n )\n }\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n )\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`)\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`)\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`)\n }\n if (model.supported_parameters?.length) {\n clack.log.info(` Parameters: ${model.supported_parameters.join(', ')}`)\n }\n}\n\nasync function displayProviders(\n client: OpenRouterDataClient,\n model: OpenRouterModel,\n): Promise<void> {\n const author = parseModelAuthor(model.id)\n const slug = parseModelSlug(model.id)\n const s = clack.spinner()\n s.start('Checking providers...')\n try {\n const endpoints = await fetchModelEndpoints(client, author, slug)\n const unique = getUniqueProviders(endpoints)\n s.stop(`${unique.length} providers available`)\n\n for (const p of unique) {\n const ep = endpoints.find(e => e.tag === p.tag)\n const latency = formatLatency(ep?.latency_last_30m?.p50 ?? null)\n const throughput = formatThroughput(ep?.throughput_last_30m?.p50 ?? null)\n clack.log.info(` ${p.providerName} (${p.tag}) — ${latency} · ${throughput}`)\n }\n } catch {\n s.stop('Could not fetch providers')\n }\n}\n\n/** Run the interactive \"Browse models\" flow. */\nexport async function browseModelsCommand(client: OpenRouterDataClient): Promise<void> {\n clack.intro('Browse Models')\n\n const s = clack.spinner()\n s.start('Loading models...')\n let models: OpenRouterModel[]\n try {\n models = await fetchModels(client)\n s.stop(`${models.length} models available`)\n } catch (error) {\n s.stop('Failed to load models')\n clack.log.error(String(error))\n return\n }\n\n const modelId = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search...',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase()\n if (!query) return models.slice(0, 15).map(toOption)\n\n return models\n .filter(m => `${m.id} ${m.name}`.toLowerCase().includes(query))\n .slice(0, 15)\n .map(toOption)\n },\n filter: (_search: string, _option: { value: string }) => true,\n })\n\n if (isCancel(modelId)) return\n\n const model = models.find(m => m.id === modelId)\n if (!model) return\n\n displayModelDetails(model)\n await displayProviders(client, model)\n\n const configure = await clack.confirm({\n message: `Configure routing for ${model.id}?`,\n })\n\n if (isCancel(configure) || !configure) {\n clack.outro('Bye!')\n return\n }\n\n await addOverrideCommand(client)\n}\n"],"mappings":"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"}
|
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
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
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()
|
|
10606
|
-
|
|
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
|
-
|
|
10742
|
+
let fileConfig = {};
|
|
10758
10743
|
if (!options.noConfig) {
|
|
10759
10744
|
const configPath = findConfigFile(options.configPath);
|
|
10760
|
-
if (configPath)
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
if (!
|
|
10771
|
-
|
|
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 =
|
|
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.
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
14918
|
+
const withClient = (fn) => async (args) => {
|
|
14812
14919
|
const apiKey = await resolveApiKey(args.config ?? void 0, args.openrouterKey ?? void 0);
|
|
14813
|
-
if (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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|