proxitor 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/add.mjs.map +1 -1
- package/dist/browse.mjs.map +1 -1
- package/dist/cli.mjs +134 -21
- package/dist/cli.mjs.map +1 -1
- package/dist/config.mjs.map +1 -1
- package/dist/config2.mjs.map +1 -1
- package/dist/edit.mjs.map +1 -1
- package/dist/list.mjs.map +1 -1
- package/dist/providers.mjs +8 -8
- package/dist/providers.mjs.map +1 -1
- package/dist/remove.mjs.map +1 -1
- package/dist/validate.mjs.map +1 -1
- package/dist/wizard.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,23 @@ authType: oauth # "bearer" (default) or "oauth"
|
|
|
123
123
|
|
|
124
124
|
This changes the header to `Authorization: OAuth sk-...`.
|
|
125
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
|
+
|
|
126
143
|
### Provider routing
|
|
127
144
|
|
|
128
145
|
Control which provider handles your requests. All three options accept a string or an array:
|
|
@@ -258,6 +275,7 @@ The wizard asks for:
|
|
|
258
275
|
- **OpenRouter API key** — stored in config or set as `OPENROUTER_API_KEY` env var
|
|
259
276
|
- **Port** — default `8828` (avoids conflicts with common dev servers on 8080)
|
|
260
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
|
|
261
279
|
- **Authentication type** — `bearer` (default) or `oauth`; use `oauth` for custom proxy providers that pass tokens in the `Authorization: OAuth ...` header
|
|
262
280
|
- **Host** — all interfaces (`0.0.0.0`) or localhost only (`127.0.0.1`)
|
|
263
281
|
- **Save location** — project directory, `~/.config/proxitor/`, or `$XDG_CONFIG_HOME/proxitor/`
|
package/dist/add.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.mjs","names":["clack.spinner","clack.autocomplete","isCancel","clack.text","clack.confirm"],"sources":["../src/commands/config/add.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport type { 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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"}
|
|
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.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'
|
|
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
|
@@ -3800,7 +3800,7 @@ var require_main = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
3800
3800
|
const fs = __require("fs");
|
|
3801
3801
|
const path = __require("path");
|
|
3802
3802
|
const os$1 = __require("os");
|
|
3803
|
-
const crypto = __require("crypto");
|
|
3803
|
+
const crypto$1 = __require("crypto");
|
|
3804
3804
|
const TIPS = [
|
|
3805
3805
|
"◈ encrypted .env [www.dotenvx.com]",
|
|
3806
3806
|
"◈ secrets for agents [www.dotenvx.com]",
|
|
@@ -4005,7 +4005,7 @@ var require_main = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
|
|
|
4005
4005
|
const authTag = ciphertext.subarray(-16);
|
|
4006
4006
|
ciphertext = ciphertext.subarray(12, -16);
|
|
4007
4007
|
try {
|
|
4008
|
-
const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
4008
|
+
const aesgcm = crypto$1.createDecipheriv("aes-256-gcm", key, nonce);
|
|
4009
4009
|
aesgcm.setAuthTag(authTag);
|
|
4010
4010
|
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
4011
4011
|
} catch (error) {
|
|
@@ -10589,6 +10589,7 @@ const proxyConfigSchema = object({
|
|
|
10589
10589
|
port: number$1().int().min(1).max(65535).default(8828),
|
|
10590
10590
|
openrouterKey: string$1().default(""),
|
|
10591
10591
|
openrouterBaseUrl: string$1().url().default("https://openrouter.ai/api/v1"),
|
|
10592
|
+
openrouterDataUrl: string$1().url().optional(),
|
|
10592
10593
|
authType: _enum(["bearer", "oauth"]).default("bearer"),
|
|
10593
10594
|
verbose: boolean().default(false),
|
|
10594
10595
|
bodyLimit: string$1().min(1).default("50mb"),
|
|
@@ -11749,6 +11750,14 @@ function _getDefaultLogLevel() {
|
|
|
11749
11750
|
//#endregion
|
|
11750
11751
|
//#region src/logger.ts
|
|
11751
11752
|
const logger = createConsola().withTag("proxitor");
|
|
11753
|
+
/** Generate a short request ID (first 8 hex chars of a UUID) */
|
|
11754
|
+
function requestId() {
|
|
11755
|
+
return crypto.randomUUID().slice(0, 8);
|
|
11756
|
+
}
|
|
11757
|
+
/** Format a log message with a request ID prefix. */
|
|
11758
|
+
function withReq(id, message) {
|
|
11759
|
+
return `[${id}] ${message}`;
|
|
11760
|
+
}
|
|
11752
11761
|
//#endregion
|
|
11753
11762
|
//#region src/openrouter/client.ts
|
|
11754
11763
|
var OpenRouterClientError = class extends Error {
|
|
@@ -11764,14 +11773,22 @@ var OpenRouterClient = class {
|
|
|
11764
11773
|
apiKey;
|
|
11765
11774
|
baseUrl;
|
|
11766
11775
|
authType;
|
|
11767
|
-
constructor(
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11776
|
+
constructor(apiKeyOrUrl, baseUrl, authType) {
|
|
11777
|
+
if (baseUrl !== void 0) {
|
|
11778
|
+
this.apiKey = apiKeyOrUrl;
|
|
11779
|
+
this.baseUrl = baseUrl;
|
|
11780
|
+
this.authType = authType;
|
|
11781
|
+
} else {
|
|
11782
|
+
this.apiKey = void 0;
|
|
11783
|
+
this.baseUrl = apiKeyOrUrl;
|
|
11784
|
+
this.authType = void 0;
|
|
11785
|
+
}
|
|
11771
11786
|
}
|
|
11772
11787
|
async get(path) {
|
|
11773
11788
|
const url = `${this.baseUrl}${path}`;
|
|
11774
|
-
const
|
|
11789
|
+
const headers = {};
|
|
11790
|
+
if (this.apiKey !== void 0 && this.authType !== void 0) headers.Authorization = formatAuthHeader(this.apiKey, this.authType);
|
|
11791
|
+
const res = await fetch(url, { headers });
|
|
11775
11792
|
if (!res.ok) {
|
|
11776
11793
|
const body = await res.text().catch(() => "");
|
|
11777
11794
|
throw new OpenRouterClientError(res.status, body || res.statusText);
|
|
@@ -11780,6 +11797,88 @@ var OpenRouterClient = class {
|
|
|
11780
11797
|
}
|
|
11781
11798
|
};
|
|
11782
11799
|
//#endregion
|
|
11800
|
+
//#region src/openrouter/data-client.ts
|
|
11801
|
+
const OPENROUTER_FALLBACK_URL = "https://openrouter.ai/api/v1";
|
|
11802
|
+
function isValidProvidersResponse(data) {
|
|
11803
|
+
return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
|
|
11804
|
+
}
|
|
11805
|
+
function isValidModelsResponse(data) {
|
|
11806
|
+
return typeof data === "object" && data !== null && "data" in data && Array.isArray(data.data);
|
|
11807
|
+
}
|
|
11808
|
+
function isValidEndpointsResponse(data) {
|
|
11809
|
+
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);
|
|
11810
|
+
}
|
|
11811
|
+
/**
|
|
11812
|
+
* Client for fetching provider/model data with automatic fallback to OpenRouter.
|
|
11813
|
+
*
|
|
11814
|
+
* When the primary API (openrouterDataUrl or openrouterBaseUrl) doesn't support
|
|
11815
|
+
* OpenRouter-specific data endpoints, falls back to https://openrouter.ai/api/v1
|
|
11816
|
+
* which hosts public, unauthenticated endpoints for /providers, /models, etc.
|
|
11817
|
+
*/
|
|
11818
|
+
var OpenRouterDataClient = class {
|
|
11819
|
+
primaryClient;
|
|
11820
|
+
fallbackClient;
|
|
11821
|
+
skipFallback;
|
|
11822
|
+
onFallback;
|
|
11823
|
+
constructor(config) {
|
|
11824
|
+
const primaryUrl = config.openrouterDataUrl ?? config.openrouterBaseUrl;
|
|
11825
|
+
this.skipFallback = primaryUrl === OPENROUTER_FALLBACK_URL;
|
|
11826
|
+
this.primaryClient = new OpenRouterClient(config.apiKey, primaryUrl, config.authType);
|
|
11827
|
+
this.fallbackClient = new OpenRouterClient(OPENROUTER_FALLBACK_URL);
|
|
11828
|
+
this.onFallback = config.onFallback;
|
|
11829
|
+
}
|
|
11830
|
+
async fetchProviders() {
|
|
11831
|
+
return (await this.withFallback("/providers", () => this.primaryClient.get("/providers"), isValidProvidersResponse)).data.data;
|
|
11832
|
+
}
|
|
11833
|
+
async fetchModels() {
|
|
11834
|
+
return (await this.withFallback("/models", () => this.primaryClient.get("/models"), isValidModelsResponse)).data.data;
|
|
11835
|
+
}
|
|
11836
|
+
async fetchModelEndpoints(author, slug) {
|
|
11837
|
+
const path = `/models/${author}/${slug}/endpoints`;
|
|
11838
|
+
return (await this.withFallback(path, () => this.primaryClient.get(path), isValidEndpointsResponse)).data.data.endpoints ?? [];
|
|
11839
|
+
}
|
|
11840
|
+
/**
|
|
11841
|
+
* Try primary, validate response, fallback on failure.
|
|
11842
|
+
* Network errors get 1 retry before fallback.
|
|
11843
|
+
*/
|
|
11844
|
+
async withFallback(path, primaryFn, validate) {
|
|
11845
|
+
if (this.skipFallback) {
|
|
11846
|
+
const data = await primaryFn();
|
|
11847
|
+
if (!validate(data)) throw new Error(`Unexpected response format from primary API for ${path}`);
|
|
11848
|
+
return {
|
|
11849
|
+
data,
|
|
11850
|
+
usedFallback: false
|
|
11851
|
+
};
|
|
11852
|
+
}
|
|
11853
|
+
try {
|
|
11854
|
+
const data = await primaryFn();
|
|
11855
|
+
if (validate(data)) return {
|
|
11856
|
+
data,
|
|
11857
|
+
usedFallback: false
|
|
11858
|
+
};
|
|
11859
|
+
} catch (error) {
|
|
11860
|
+
if (error instanceof Error && isNetworkError(error)) try {
|
|
11861
|
+
const data = await primaryFn();
|
|
11862
|
+
if (validate(data)) return {
|
|
11863
|
+
data,
|
|
11864
|
+
usedFallback: false
|
|
11865
|
+
};
|
|
11866
|
+
} catch {}
|
|
11867
|
+
}
|
|
11868
|
+
this.onFallback?.(path);
|
|
11869
|
+
const data = await this.fallbackClient.get(path);
|
|
11870
|
+
if (!validate(data)) throw new Error(`Unexpected response format from OpenRouter fallback for ${path}`);
|
|
11871
|
+
return {
|
|
11872
|
+
data,
|
|
11873
|
+
usedFallback: true
|
|
11874
|
+
};
|
|
11875
|
+
}
|
|
11876
|
+
};
|
|
11877
|
+
function isNetworkError(error) {
|
|
11878
|
+
const message = error.message.toLowerCase();
|
|
11879
|
+
return message.includes("fetch") || message.includes("network") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("timeout") || message.includes("aborted") || error.name === "TypeError";
|
|
11880
|
+
}
|
|
11881
|
+
//#endregion
|
|
11783
11882
|
//#region node_modules/.pnpm/@hono+node-server@2.0.4_hono@4.12.23/node_modules/@hono/node-server/dist/index.mjs
|
|
11784
11883
|
var RequestError = class extends Error {
|
|
11785
11884
|
constructor(message, options) {
|
|
@@ -14606,7 +14705,7 @@ function buildUpstreamResponse(upstream, method) {
|
|
|
14606
14705
|
});
|
|
14607
14706
|
}
|
|
14608
14707
|
/** Read and process the request body, returning an error response on failure */
|
|
14609
|
-
async function readRawBody(request) {
|
|
14708
|
+
async function readRawBody(request, reqId) {
|
|
14610
14709
|
try {
|
|
14611
14710
|
return {
|
|
14612
14711
|
ok: true,
|
|
@@ -14614,7 +14713,7 @@ async function readRawBody(request) {
|
|
|
14614
14713
|
};
|
|
14615
14714
|
} catch (err) {
|
|
14616
14715
|
const message = err instanceof Error ? err.message : "Failed to read request body";
|
|
14617
|
-
logger.error(message);
|
|
14716
|
+
logger.error(withReq(reqId, message));
|
|
14618
14717
|
return {
|
|
14619
14718
|
ok: false,
|
|
14620
14719
|
response: Response.json({ error: {
|
|
@@ -14625,7 +14724,7 @@ async function readRawBody(request) {
|
|
|
14625
14724
|
}
|
|
14626
14725
|
}
|
|
14627
14726
|
/** Resolve per-request config: extract model, resolve overrides, build routing and body */
|
|
14628
|
-
function resolveRequest(rawBody, config, method, path) {
|
|
14727
|
+
function resolveRequest(rawBody, config, method, path, reqId) {
|
|
14629
14728
|
const modelName = extractModel(rawBody);
|
|
14630
14729
|
const resolved = resolveModelConfig(config, modelName);
|
|
14631
14730
|
const providerRouting = buildProviderRouting(resolved.provider);
|
|
@@ -14635,7 +14734,7 @@ function resolveRequest(rawBody, config, method, path) {
|
|
|
14635
14734
|
body = readRequestBody(method, rawBody, inject, providerRouting);
|
|
14636
14735
|
} catch (err) {
|
|
14637
14736
|
const message = err instanceof Error ? err.message : "Failed to process request body";
|
|
14638
|
-
logger.error(message);
|
|
14737
|
+
logger.error(withReq(reqId, message));
|
|
14639
14738
|
return {
|
|
14640
14739
|
inject,
|
|
14641
14740
|
body: void 0,
|
|
@@ -14658,22 +14757,22 @@ function resolveRequest(rawBody, config, method, path) {
|
|
|
14658
14757
|
};
|
|
14659
14758
|
}
|
|
14660
14759
|
/** Execute upstream fetch, returning appropriate error responses on failure */
|
|
14661
|
-
async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt) {
|
|
14760
|
+
async function executeUpstream(upstreamUrl, method, headers, body, signal, path, startedAt, reqId) {
|
|
14662
14761
|
let upstream;
|
|
14663
14762
|
try {
|
|
14664
14763
|
upstream = await fetchUpstream(upstreamUrl, method, headers, body, signal);
|
|
14665
14764
|
} catch (err) {
|
|
14666
14765
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
14667
|
-
logger.warn(`Aborted: ${method} ${path}`);
|
|
14766
|
+
logger.warn(withReq(reqId, `Aborted: ${method} ${path}`));
|
|
14668
14767
|
return new Response(null, { status: 499 });
|
|
14669
14768
|
}
|
|
14670
|
-
logger.error("Upstream fetch error:", err);
|
|
14769
|
+
logger.error(withReq(reqId, "Upstream fetch error:"), err);
|
|
14671
14770
|
return Response.json({ error: {
|
|
14672
14771
|
message: "Proxy failed to reach upstream",
|
|
14673
14772
|
type: "proxy_upstream_error"
|
|
14674
14773
|
} }, { status: 502 });
|
|
14675
14774
|
}
|
|
14676
|
-
logger.info(`${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`);
|
|
14775
|
+
logger.info(withReq(reqId, `${method} ${path} ← ${upstream.status} (${Date.now() - startedAt}ms)`));
|
|
14677
14776
|
return buildUpstreamResponse(upstream, method);
|
|
14678
14777
|
}
|
|
14679
14778
|
function createProxyServer(config, onReady) {
|
|
@@ -14692,16 +14791,18 @@ function createProxyServer(config, onReady) {
|
|
|
14692
14791
|
const path = new URL(c.req.url).pathname;
|
|
14693
14792
|
const upstreamUrl = buildUpstreamUrl(c.req.url, config);
|
|
14694
14793
|
const startedAt = Date.now();
|
|
14695
|
-
const
|
|
14794
|
+
const reqId = requestId();
|
|
14795
|
+
const raw = await readRawBody(c.req.raw, reqId);
|
|
14696
14796
|
if (!raw.ok) return raw.response;
|
|
14697
|
-
const resolved = resolveRequest(raw.body, config, method, path);
|
|
14797
|
+
const resolved = resolveRequest(raw.body, config, method, path, reqId);
|
|
14698
14798
|
if (resolved.error) return resolved.error;
|
|
14699
14799
|
const headers = buildRequestHeaders(c.req.raw.headers, config, resolved.inject, resolved.headers);
|
|
14700
14800
|
const controller = new AbortController();
|
|
14701
14801
|
c.req.raw.signal.addEventListener("abort", () => controller.abort());
|
|
14802
|
+
const upstreamShort = upstreamUrl.replace(/^https?:\/\//, "");
|
|
14702
14803
|
const modelLog = resolved.modelName ? ` model=${resolved.modelName}` : "";
|
|
14703
|
-
logger.info(`${method} ${path} → ${
|
|
14704
|
-
return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt);
|
|
14804
|
+
logger.info(withReq(reqId, `${method} ${path} → ${upstreamShort}${resolved.inject ? " [inject]" : ""}${modelLog}`));
|
|
14805
|
+
return executeUpstream(upstreamUrl, method, headers, resolved.body, controller.signal, path, startedAt, reqId);
|
|
14705
14806
|
});
|
|
14706
14807
|
return serve({
|
|
14707
14808
|
fetch: app.fetch,
|
|
@@ -14735,7 +14836,7 @@ function startProxyServer(config, onReady) {
|
|
|
14735
14836
|
}
|
|
14736
14837
|
//#endregion
|
|
14737
14838
|
//#region src/version.ts
|
|
14738
|
-
const version = "0.
|
|
14839
|
+
const version = "0.6.0";
|
|
14739
14840
|
//#endregion
|
|
14740
14841
|
//#region src/cli.ts
|
|
14741
14842
|
const argv = process.argv.slice(2);
|
|
@@ -14829,7 +14930,19 @@ const withClient = (fn) => async (args) => {
|
|
|
14829
14930
|
if (!apiKey) return;
|
|
14830
14931
|
try {
|
|
14831
14932
|
const cfg = await loadConfig({ configPath: args.config ?? void 0 });
|
|
14832
|
-
await fn(new
|
|
14933
|
+
await fn(new OpenRouterDataClient({
|
|
14934
|
+
openrouterBaseUrl: cfg.openrouterBaseUrl,
|
|
14935
|
+
openrouterDataUrl: cfg.openrouterDataUrl,
|
|
14936
|
+
apiKey,
|
|
14937
|
+
authType: cfg.authType,
|
|
14938
|
+
onFallback: (path) => {
|
|
14939
|
+
let endpoint;
|
|
14940
|
+
if (path === "/providers") endpoint = "providers";
|
|
14941
|
+
else if (path === "/models") endpoint = "models";
|
|
14942
|
+
else endpoint = "model providers";
|
|
14943
|
+
logger.warn(`Custom API did not return ${endpoint}, using OpenRouter data as fallback`);
|
|
14944
|
+
}
|
|
14945
|
+
}));
|
|
14833
14946
|
} catch (error) {
|
|
14834
14947
|
logger.error("Failed to load config:", error);
|
|
14835
14948
|
}
|