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