proxitor 0.2.1 → 0.3.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.
Files changed (52) hide show
  1. package/README.md +221 -81
  2. package/dist/add.cjs +139 -0
  3. package/dist/add.cjs.map +1 -0
  4. package/dist/add.mjs +138 -0
  5. package/dist/add.mjs.map +1 -0
  6. package/dist/browse.cjs +88 -0
  7. package/dist/browse.cjs.map +1 -0
  8. package/dist/browse.mjs +87 -0
  9. package/dist/browse.mjs.map +1 -0
  10. package/dist/cli.cjs +148 -25
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.mjs +149 -26
  13. package/dist/cli.mjs.map +1 -1
  14. package/dist/config.cjs +68 -0
  15. package/dist/config.cjs.map +1 -0
  16. package/dist/config.mjs +45 -0
  17. package/dist/config.mjs.map +1 -0
  18. package/dist/config2.cjs +75 -0
  19. package/dist/config2.cjs.map +1 -0
  20. package/dist/config2.mjs +74 -0
  21. package/dist/config2.mjs.map +1 -0
  22. package/dist/edit.cjs +82 -0
  23. package/dist/edit.cjs.map +1 -0
  24. package/dist/edit.mjs +81 -0
  25. package/dist/edit.mjs.map +1 -0
  26. package/dist/index.cjs +2 -0
  27. package/dist/index.d.cts +223 -53
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +223 -53
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +2 -2
  32. package/dist/list.cjs +33 -0
  33. package/dist/list.cjs.map +1 -0
  34. package/dist/list.mjs +31 -0
  35. package/dist/list.mjs.map +1 -0
  36. package/dist/providers.cjs +376 -0
  37. package/dist/providers.cjs.map +1 -0
  38. package/dist/providers.mjs +279 -0
  39. package/dist/providers.mjs.map +1 -0
  40. package/dist/proxy.cjs +121 -2
  41. package/dist/proxy.cjs.map +1 -1
  42. package/dist/proxy.mjs +92 -3
  43. package/dist/proxy.mjs.map +1 -1
  44. package/dist/remove.cjs +38 -0
  45. package/dist/remove.cjs.map +1 -0
  46. package/dist/remove.mjs +37 -0
  47. package/dist/remove.mjs.map +1 -0
  48. package/dist/validate.cjs +26 -0
  49. package/dist/validate.cjs.map +1 -0
  50. package/dist/validate.mjs +25 -0
  51. package/dist/validate.mjs.map +1 -0
  52. package/package.json +6 -3
package/README.md CHANGED
@@ -1,44 +1,92 @@
1
1
  # proxitor
2
2
 
3
- > Lightweight proxy for routing CLI requests (claude-code, codex) to [OpenRouter](https://openrouter.ai)
3
+ <p align="center">
4
+ <strong>A transparent proxy between your AI CLI tools and OpenRouter.</strong><br/>
5
+ Route by provider. Control costs. Keep streaming. Zero config changes in Claude Code.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/proxitor"><img src="https://img.shields.io/npm/v/proxitor?color=6366f1&labelColor=1e2327&label=npm" alt="npm version"></a>
10
+ <a href="https://github.com/neiromaster/proxitor/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-22c55e?labelColor=1e2327" alt="MIT License"></a>
11
+ <img src="https://img.shields.io/badge/node-%3E%3D22-3b82f6?labelColor=1e2327" alt="Node.js ≥ 22">
12
+ <img src="https://img.shields.io/badge/built_with-TypeScript-3178c6?labelColor=1e2327" alt="TypeScript">
13
+ </p>
14
+
15
+ ---
16
+
17
+ ```
18
+ Claude Code / Codex
19
+
20
+ │ ANTHROPIC_BASE_URL=http://localhost:8080/v1
21
+
22
+ ┌───────────────┐
23
+ │ proxitor │ ← injects provider routing
24
+ │ :8080 │ ← streams SSE back unchanged
25
+ └───────────────┘
26
+
27
+ │ + X-OpenRouter-* headers
28
+
29
+ OpenRouter
30
+ ┌──────┬──────┐
31
+ Anthropic DeepInfra Azure ...
32
+ ```
33
+
34
+ ---
4
35
 
5
36
  ## Why
6
37
 
7
- When using AI CLI tools like Claude Code or Codex, you may want to route requests through OpenRouter for model selection, cost control, or unified API access. Proxitor sits between your CLI tools and OpenRouter, injecting [provider routing](https://openrouter.ai/docs/api/reference/streaming) into requests and streaming responses back unchanged — including SSE streams from LLM models.
38
+ ### The prompt cache problem
39
+
40
+ OpenRouter is convenient — one key, every model. But by default it load-balances across multiple provider instances for the same model. Each request can land on a different provider, and **prompt caching is provider-scoped**: a cache entry built on Anthropic's infrastructure doesn't help when the next request goes to DeepInfra.
41
+
42
+ Claude Code sends a large system prompt on every single request. Without a pinned provider, you pay full token price every time. With proxitor locking `claude-*` to `anthropic`, that system prompt gets cached after the first hit and subsequent requests cost a fraction.
43
+
44
+ ```yaml
45
+ # pin all Claude models to Anthropic — prompt cache works reliably
46
+ modelOverrides:
47
+ "claude-*":
48
+ provider:
49
+ only: "anthropic"
50
+ ```
51
+
52
+ ### Other reasons to use it
53
+
54
+ - **Cost control** — route specific models to cheaper providers when caching isn't the priority
55
+ - **Automatic fallbacks** — if Anthropic is degraded, fall back to DeepInfra without touching your tools
56
+ - **Mixed routing** — `claude-*` on Anthropic, `gpt-*` on Azure, different rules per model
57
+ - **Data privacy** — enforce `dataCollection: deny` or ZDR across all requests
58
+
59
+ Proxitor sits between your CLI tools and OpenRouter, injecting all of this transparently. Your tools don't know anything changed.
60
+
61
+ ---
8
62
 
9
63
  ## Install
10
64
 
11
- ```bash
65
+ ```sh
12
66
  # npm
13
67
  npm install -g proxitor
14
68
 
15
69
  # bun
16
70
  bun install -g proxitor
17
71
 
18
- # npx (no install)
72
+ # no install needed
19
73
  npx proxitor
20
74
  ```
21
75
 
22
- ## Usage
23
-
24
- ### Start the proxy
76
+ ---
25
77
 
26
- ```bash
27
- # With env var
28
- OPENROUTER_API_KEY=sk-... proxitor
78
+ ## Quick Start
29
79
 
30
- # With CLI flag
31
- proxitor --openrouter-key sk-...
80
+ **1. Start the proxy**
32
81
 
33
- # With config file
34
- proxitor --config ./proxitor.config.yaml
82
+ ```sh
83
+ OPENROUTER_API_KEY=sk-or-... proxitor
84
+ # Listening on http://0.0.0.0:8080
35
85
  ```
36
86
 
37
- ### Configure CLI tools
87
+ **2. Point your tools at it**
38
88
 
39
- Point your AI CLI tools at the proxy:
40
-
41
- ```bash
89
+ ```sh
42
90
  # Claude Code
43
91
  ANTHROPIC_BASE_URL=http://localhost:8080/v1 claude
44
92
 
@@ -46,76 +94,87 @@ ANTHROPIC_BASE_URL=http://localhost:8080/v1 claude
46
94
  OPENAI_BASE_URL=http://localhost:8080/v1 codex
47
95
  ```
48
96
 
49
- ## Configuration
97
+ That's it. Requests flow through proxitor to OpenRouter, SSE streams pass through unchanged.
50
98
 
51
- Proxitor looks for config files in this order:
99
+ ---
52
100
 
53
- 1. `proxitor.config.yaml`
54
- 2. `proxitor.config.yml`
55
- 3. `proxitor.config.json`
56
- 4. `.proxitor.yaml`
57
- 5. `.proxitor.yml`
58
- 6. `.proxitor.json`
101
+ ## Configuration
59
102
 
60
- See [`proxitor.config.example.yaml`](./proxitor.config.example.yaml) for a full example.
103
+ Proxitor looks for a config file in this order:
61
104
 
62
- ### Priority
105
+ ```
106
+ proxitor.config.yaml → proxitor.config.yml → proxitor.config.json
107
+ .proxitor.yaml → .proxitor.yml → .proxitor.json
108
+ ```
109
+
110
+ **Priority:** CLI flags > config file > environment variables > defaults
63
111
 
64
- CLI flags > config file > environment variables > defaults
112
+ See [`proxitor.config.example.yaml`](./proxitor.config.example.yaml) for the complete reference.
65
113
 
66
114
  ### Provider routing
67
115
 
68
- Control which upstream provider handles your requests. Both `only` and `order` accept a single string or an array:
116
+ Control which provider handles your requests. All three options accept a string or an array:
69
117
 
70
118
  ```yaml
71
- # Use a single provider exclusively (no fallbacks)
119
+ # Strict lock only this provider, no fallbacks
72
120
  provider:
73
- only: "deepinfra"
121
+ only: "anthropic"
74
122
 
75
- # Use multiple providers exclusively
123
+ # Restricted pool — load balance between these providers only
76
124
  provider:
77
125
  only:
78
- - "openai"
79
- - "azure"
126
+ - "anthropic"
127
+ - "deepinfra"
80
128
 
81
- # Prefer a provider, allow fallbacks
129
+ # Priority order — try Anthropic first, fall back to others if unavailable
82
130
  provider:
83
131
  order: "anthropic"
84
132
  allowFallbacks: true
85
133
 
86
- # Try providers in order
134
+ # Strict order — try in sequence, no fallbacks outside the list
87
135
  provider:
88
136
  order:
89
137
  - "anthropic"
90
138
  - "deepinfra"
91
139
  allowFallbacks: false
140
+
141
+ # Blacklist — never use these providers
142
+ provider:
143
+ ignore: "azure"
92
144
  ```
93
145
 
94
- Without `provider` configured, the proxy forwards requests unchanged.
95
- See the [OpenRouter provider routing docs](https://openrouter.ai/docs/guides/routing/provider-selection) for the full list of supported providers.
146
+ | Option | Behavior |
147
+ |---|---|
148
+ | `only` | Restrict to the listed provider(s). Load balances by price within the list. Never routes outside it — if all are unavailable, the request fails. |
149
+ | `order` | Try providers in the specified priority order. If none work, falls back to other available providers (unless `allowFallbacks: false`). |
150
+ | `ignore` | Never route to the listed provider(s). |
151
+
152
+ Without `provider` set, requests are forwarded unchanged.
153
+
154
+ See [OpenRouter's provider routing docs](https://openrouter.ai/docs/guides/routing/provider-selection) for the full list of supported providers and options.
96
155
 
97
156
  ### Per-model overrides
98
157
 
99
- Route different models to different providers using `modelOverrides`. Keys are exact model names or prefix patterns (e.g. `claude-*`). Overrides layer on top of global settings — `provider` replaces the global value, `headers` merge:
158
+ Route different models differently. Keys are exact names or prefix wildcards. More specific matches win.
100
159
 
101
160
  ```yaml
102
161
  provider:
103
- order: "deepinfra"
162
+ order: "deepinfra" # global default
104
163
 
105
164
  modelOverrides:
106
- # Exact match — force Anthropic models to Anthropic's own infrastructure
165
+ # Exact match — force this model to Anthropic
107
166
  "claude-sonnet-4-6":
108
167
  provider:
109
168
  only: "anthropic"
110
169
 
111
- # Wildcard — all Claude models prefer Anthropic with fallback
170
+ # Wildcard — all claude-* models prefer Anthropic with fallback
112
171
  "claude-*":
113
172
  provider:
114
173
  order:
115
174
  - "anthropic"
116
175
  - "deepinfra"
117
176
 
118
- # Wildcard — GPT models to OpenAI/Azure, plus a custom header
177
+ # GPT models to OpenAI/Azure, plus a custom header
119
178
  "gpt-*":
120
179
  provider:
121
180
  only:
@@ -125,76 +184,157 @@ modelOverrides:
125
184
  X-Model-Family: "gpt"
126
185
  ```
127
186
 
128
- When a model name matches multiple patterns, the most specific match wins (exact name > longer prefix > shorter prefix).
187
+ **Match priority:** exact name > longer prefix > shorter prefix.
129
188
 
130
189
  ### Custom headers
131
190
 
132
- Add custom headers to all proxied requests, or per-model via `modelOverrides`:
191
+ Add headers to all proxied requests, or per-model (merged on top of global):
133
192
 
134
193
  ```yaml
135
- # Global custom headers
136
194
  headers:
137
195
  X-Custom-Header: "my-value"
138
196
  X-Environment: "production"
139
197
 
140
- # Per-model headers (merged on top of global)
141
198
  modelOverrides:
142
199
  "claude-*":
143
200
  headers:
144
- X-Custom-Header: "claude-override" # overrides global value
201
+ X-Custom-Header: "claude-override" # overrides the global value
145
202
  X-Extra: "only-for-claude" # added only for this model
146
203
  ```
147
204
 
205
+ ### Advanced provider options
206
+
207
+ ```yaml
208
+ provider:
209
+ sort: "throughput" # sort by: price | throughput | latency
210
+ quantizations:
211
+ - "fp8" # filter by quantization level
212
+ maxPrice:
213
+ prompt: 1 # $/M tokens
214
+ completion: 2
215
+ requireParameters: true # only use providers that support all request params
216
+ dataCollection: "deny" # "allow" | "deny"
217
+ zdr: true # Zero Data Retention enforcement
218
+ preferredMinThroughput:
219
+ p90: 50 # tokens/sec (soft threshold)
220
+ preferredMaxLatency:
221
+ p90: 3 # seconds (soft threshold)
222
+ ```
223
+
148
224
  ### Health check
149
225
 
150
- ```bash
226
+ ```sh
151
227
  curl http://localhost:8080/health
152
228
  ```
153
229
 
154
- ## CLI Options
230
+ ---
155
231
 
156
- ```text
157
- proxitor [options]
232
+ ## Interactive Config Manager
158
233
 
159
- Options:
160
- -p, --port <port> Proxy server port (default: 8080)
161
- -h, --host <host> Proxy server host (default: 0.0.0.0)
162
- -c, --config <path> Path to config file
163
- --openrouter-key <key> OpenRouter API key
164
- --verbose Enable verbose logging
165
- -v, --version Display version
166
- --help Display help
234
+ Proxitor includes an interactive CLI for managing model overrides — search models, pick providers, and write to config without editing YAML by hand.
235
+
236
+ ```sh
237
+ proxitor config menu # interactive menu
238
+ proxitor config add # add a model override
239
+ proxitor config edit # edit existing override
240
+ proxitor config remove # remove override(s)
241
+ proxitor config list # show current overrides
242
+ proxitor config browse # explore models with pricing info
243
+ proxitor config validate # validate config file
167
244
  ```
168
245
 
169
- ## Development
246
+ ### Add override walkthrough
247
+
248
+ ```sh
249
+ $ proxitor config add
250
+
251
+ ┌──────────────────────────────────┐
252
+ │ Add Model Override │
253
+ ╰──────────────────────────────────╯
254
+
255
+ ◇ Search for a model
256
+ │ claude
257
+ (23 matches)
258
+ ● anthropic/claude-sonnet-4-6 · $3.00/$15.00 · 200k
259
+ ○ anthropic/claude-opus-4-8 · $15.00/$75.00 · 200k
260
+ ...
261
+
262
+ ◇ Configure provider routing
263
+ │ ○ Use specific providers only
264
+ ○ Set provider priority order
265
+ ○ Ignore specific providers
266
+ ○ Skip provider routing
267
+ ```
170
268
 
171
- ```bash
172
- # Install dependencies
173
- pnpm install
269
+ **"Use specific providers only" / "Ignore specific providers"** — multiselect, pick all that apply:
174
270
 
175
- # Run in dev mode with watch
176
- pnpm run dev
271
+ ```text
272
+ Select providers
273
+ ◼ anthropic (anthropic) · 1.0s · 40 t/s
274
+ ◻ google-vertex/global · 1.1s · 39 t/s
275
+ ◻ amazon-bedrock · 1.2s · 40 t/s
276
+ ```
177
277
 
178
- # Run tests
179
- pnpm run test
278
+ **"Set provider priority order"** — pick providers one at a time, then select **✓ Done** at the bottom to finish:
279
+
280
+ ```text
281
+ ◇ Select provider #1 (or cancel to finish)
282
+ │ ● anthropic (anthropic) · 1.0s · 40 t/s
283
+ ○ google-vertex/global · 1.1s · 39 t/s
284
+ ○ amazon-bedrock · 1.2s · 40 t/s
285
+ ○ ✓ Done
180
286
 
181
- # Type check
182
- pnpm run typecheck
287
+ ◇ Select provider #2 (or cancel to finish)
288
+ google-vertex/global · 1.1s · 39 t/s
289
+ ○ amazon-bedrock · 1.2s · 40 t/s
290
+ ○ ✓ Done
183
291
 
184
- # Lint + format (Biome)
185
- pnpm run check:biome
292
+ ◇ Select provider #3 (or cancel to finish)
293
+ ✓ Done
186
294
 
187
- # Auto-fix lint + format issues
188
- pnpm run lint:fix
189
- pnpm run format
295
+ Allow fallbacks to other providers? Yes
190
296
 
191
- # Build
192
- pnpm run build
297
+ Save to config? Yes
193
298
 
194
- # Full check (typecheck + biome + test)
195
- pnpm run check
299
+ ╭──────────────────────────────────╮
300
+ Model override saved │
301
+ ╰──────────────────────────────────╯
196
302
  ```
197
303
 
304
+ The interface uses live data from the OpenRouter API — model search with type-ahead, real provider availability and pricing for each model.
305
+
306
+ ---
307
+
308
+ ## CLI Options
309
+
310
+ | Flag | Default | Description |
311
+ |---|---|---|
312
+ | `-p, --port <port>` | `8080` | Server port |
313
+ | `-h, --host <host>` | `0.0.0.0` | Server host |
314
+ | `-c, --config <path>` | auto-discovered | Path to config file |
315
+ | `--openrouter-key <key>` | `$OPENROUTER_API_KEY` | OpenRouter API key |
316
+ | `--verbose` | `false` | Enable verbose logging |
317
+ | `-v, --version` | | Print version |
318
+ | `--help` | | Print help |
319
+
320
+ ---
321
+
322
+ ## Development
323
+
324
+ ```sh
325
+ pnpm install # install dependencies
326
+ pnpm dev # build + watch
327
+ pnpm test # run tests
328
+ pnpm test:e2e # end-to-end tests
329
+ pnpm typecheck # TypeScript check
330
+ pnpm check:biome # lint + format check
331
+ pnpm lint:fix # auto-fix lint issues
332
+ pnpm build # production build
333
+ pnpm check # typecheck + biome + test (full CI)
334
+ ```
335
+
336
+ ---
337
+
198
338
  ## License
199
339
 
200
- [MIT](./LICENSE)
340
+ [MIT](./LICENSE)
package/dist/add.cjs ADDED
@@ -0,0 +1,139 @@
1
+ const require_proxy = require("./proxy.cjs");
2
+ const require_providers = require("./providers.cjs");
3
+ const require_config = require("./config.cjs");
4
+ let _clack_prompts = require("@clack/prompts");
5
+ _clack_prompts = require_proxy.__toESM(_clack_prompts, 1);
6
+ //#region src/commands/config/add.ts
7
+ const CUSTOM_PATTERN = "__custom_pattern__";
8
+ /** Run the interactive "Add model override" flow. */
9
+ async function addOverrideCommand(apiKey) {
10
+ _clack_prompts.intro("Add Model Override");
11
+ const configPath = require_config.requireConfigPath();
12
+ const client = new require_providers.OpenRouterClient(apiKey);
13
+ const models = await loadModelsWithSpinner(client);
14
+ if (!models) return;
15
+ const modelId = await searchModel(models);
16
+ if (!modelId) return;
17
+ if (typeof modelId !== "string") return;
18
+ if (modelId === CUSTOM_PATTERN) {
19
+ const pattern = await enterPattern(models);
20
+ if (!pattern) return;
21
+ if (require_config.getModelOverrides(configPath)[pattern]) {
22
+ _clack_prompts.log.warn(`Override for "${pattern}" already exists. Use Edit instead.`);
23
+ return;
24
+ }
25
+ await configureProviderAndSave(configPath, client, pattern, true);
26
+ return;
27
+ }
28
+ const selected = models.find((m) => m.id === modelId);
29
+ if (selected) displayModelInfo(selected);
30
+ if (require_config.getModelOverrides(configPath)[modelId]) {
31
+ _clack_prompts.log.warn(`Override for "${modelId}" already exists. Use Edit instead.`);
32
+ return;
33
+ }
34
+ await configureProviderAndSave(configPath, client, modelId, false);
35
+ }
36
+ async function loadModelsWithSpinner(client) {
37
+ const s = _clack_prompts.spinner();
38
+ s.start("Loading models from OpenRouter...");
39
+ try {
40
+ const models = await require_providers.fetchModels(client);
41
+ s.stop(`${models.length} models available`);
42
+ return models;
43
+ } catch (error) {
44
+ s.stop("Failed to load models");
45
+ _clack_prompts.log.error(String(error));
46
+ return null;
47
+ }
48
+ }
49
+ async function searchModel(models) {
50
+ const result = await _clack_prompts.autocomplete({
51
+ message: "Search for a model",
52
+ placeholder: "Type to search (e.g. \"claude\", \"gpt-4o\", \"qwen\")",
53
+ maxItems: 15,
54
+ options() {
55
+ const query = this.userInput.trim().toLowerCase();
56
+ if (!query) return [{
57
+ value: CUSTOM_PATTERN,
58
+ label: "✏️ Enter custom pattern (e.g. \"claude-*\")"
59
+ }];
60
+ return [...models.filter((m) => {
61
+ return `${m.id} ${m.name}`.toLowerCase().includes(query);
62
+ }).slice(0, 14).map((m) => ({
63
+ value: m.id,
64
+ label: require_providers.formatModelLabel(m),
65
+ hint: require_providers.formatModelHint(m)
66
+ })), {
67
+ value: CUSTOM_PATTERN,
68
+ label: "✏️ Enter custom pattern (e.g. \"claude-*\")"
69
+ }];
70
+ },
71
+ filter: (_search, _option) => true
72
+ });
73
+ if ((0, _clack_prompts.isCancel)(result)) return null;
74
+ return result;
75
+ }
76
+ async function enterPattern(models) {
77
+ const pattern = await _clack_prompts.text({
78
+ message: "Enter model pattern",
79
+ placeholder: "e.g. claude-*, gpt-4*, anthropic/*",
80
+ validate: (v) => {
81
+ if (!v?.trim()) return "Pattern cannot be empty";
82
+ }
83
+ });
84
+ if ((0, _clack_prompts.isCancel)(pattern)) return null;
85
+ const pat = pattern.trim();
86
+ const matches = countPatternMatches(pat, models);
87
+ if (matches > 0) _clack_prompts.log.info(`Pattern "${pat}" matches ${matches} model(s)`);
88
+ else _clack_prompts.log.warn(`Pattern "${pat}" does not match any current models — it will still be saved`);
89
+ return pat;
90
+ }
91
+ async function configureProviderAndSave(configPath, client, modelKey, isPattern) {
92
+ const mode = await require_providers.selectRoutingMode("Configure provider routing");
93
+ if ((0, _clack_prompts.isCancel)(mode)) return;
94
+ if (mode === "skip") {
95
+ require_config.setModelOverride(configPath, modelKey, {});
96
+ _clack_prompts.outro("Done — override saved without provider routing");
97
+ return;
98
+ }
99
+ const providerOptions = await require_providers.fetchProvidersForModel(client, modelKey, isPattern);
100
+ if (!providerOptions) return;
101
+ const override = await require_providers.selectProvidersByMode(mode, providerOptions);
102
+ if (!override) return;
103
+ _clack_prompts.log.info(`Proposed override:\n ${modelKey}:\n ${formatOverrideYaml(override)}`);
104
+ const save = await _clack_prompts.confirm({ message: "Save to config?" });
105
+ if ((0, _clack_prompts.isCancel)(save) || !save) {
106
+ _clack_prompts.outro("Cancelled");
107
+ return;
108
+ }
109
+ require_config.setModelOverride(configPath, modelKey, override);
110
+ _clack_prompts.outro("✓ Model override saved");
111
+ }
112
+ function displayModelInfo(model) {
113
+ _clack_prompts.log.info(`${model.name || model.id}`);
114
+ _clack_prompts.log.info(` Context: ${require_providers.formatContextLength(model.context_length)} tokens`);
115
+ _clack_prompts.log.info(` Pricing: ${require_providers.formatPricing(model.pricing.prompt, model.pricing.completion)}`);
116
+ if (model.pricing.input_cache_read && model.pricing.input_cache_read !== "0") _clack_prompts.log.info(` Cache read: ${require_providers.formatPrice(model.pricing.input_cache_read)}`);
117
+ if (model.pricing.input_cache_write && model.pricing.input_cache_write !== "0") _clack_prompts.log.info(` Cache write: ${require_providers.formatPrice(model.pricing.input_cache_write)}`);
118
+ if (model.top_provider?.max_completion_tokens) _clack_prompts.log.info(` Max output: ${require_providers.formatContextLength(model.top_provider.max_completion_tokens)} tokens`);
119
+ if (model.architecture?.modality) _clack_prompts.log.info(` Modality: ${model.architecture.modality}`);
120
+ }
121
+ function countPatternMatches(pattern, models) {
122
+ if (pattern.endsWith("*")) {
123
+ const prefix = pattern.slice(0, -1);
124
+ return models.filter((m) => m.id.startsWith(prefix)).length;
125
+ }
126
+ return models.filter((m) => m.id === pattern).length;
127
+ }
128
+ function formatOverrideYaml(override) {
129
+ const parts = [];
130
+ if (override.provider && typeof override.provider === "object") {
131
+ const p = override.provider;
132
+ for (const [key, value] of Object.entries(p)) parts.push(`provider.${key}: ${JSON.stringify(value)}`);
133
+ }
134
+ return parts.join("\n ") || "(empty)";
135
+ }
136
+ //#endregion
137
+ exports.addOverrideCommand = addOverrideCommand;
138
+
139
+ //# sourceMappingURL=add.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.cjs","names":["requireConfigPath","OpenRouterClient","getModelOverrides","clack","fetchModels","formatModelLabel","formatModelHint","selectRoutingMode","fetchProvidersForModel","selectProvidersByMode","formatContextLength","formatPricing","formatPrice"],"sources":["../src/commands/config/add.ts"],"sourcesContent":["import * as clack from '@clack/prompts'\nimport { isCancel } from '@clack/prompts'\nimport { OpenRouterClient } from '../../openrouter/client.js'\nimport { fetchModels, formatPrice } from '../../openrouter/models.js'\nimport type { OpenRouterModel } from '../../openrouter/types.js'\nimport { getModelOverrides, requireConfigPath, setModelOverride } from './config.js'\nimport {\n formatContextLength,\n formatModelHint,\n formatModelLabel,\n formatPricing,\n} from './format.js'\nimport {\n fetchProvidersForModel,\n selectProvidersByMode,\n selectRoutingMode,\n} from './providers.js'\n\nconst CUSTOM_PATTERN = '__custom_pattern__'\n\n/** Run the interactive \"Add model override\" flow. */\nexport async function addOverrideCommand(apiKey: string): Promise<void> {\n clack.intro('Add Model Override')\n\n const configPath = requireConfigPath()\n const client = new OpenRouterClient(apiKey)\n\n const models = await loadModelsWithSpinner(client)\n if (!models) return\n\n const modelId = await searchModel(models)\n if (!modelId) return\n\n if (typeof modelId !== 'string') return\n\n if (modelId === CUSTOM_PATTERN) {\n const pattern = await enterPattern(models)\n if (!pattern) return\n\n const existing = getModelOverrides(configPath)\n if (existing[pattern]) {\n clack.log.warn(`Override for \"${pattern}\" already exists. Use Edit instead.`)\n return\n }\n\n await configureProviderAndSave(configPath, client, pattern, true)\n return\n }\n\n const selected = models.find(m => m.id === modelId)\n if (selected) displayModelInfo(selected)\n\n const existing = getModelOverrides(configPath)\n if (existing[modelId]) {\n clack.log.warn(`Override for \"${modelId}\" already exists. Use Edit instead.`)\n return\n }\n\n await configureProviderAndSave(configPath, client, modelId, false)\n}\n\nasync function loadModelsWithSpinner(\n client: OpenRouterClient,\n): Promise<OpenRouterModel[] | null> {\n const s = clack.spinner()\n s.start('Loading models from OpenRouter...')\n try {\n const models = await fetchModels(client)\n s.stop(`${models.length} models available`)\n return models\n } catch (error) {\n s.stop('Failed to load models')\n clack.log.error(String(error))\n return null\n }\n}\n\nasync function searchModel(models: OpenRouterModel[]): Promise<string | symbol | null> {\n const result = await clack.autocomplete({\n message: 'Search for a model',\n placeholder: 'Type to search (e.g. \"claude\", \"gpt-4o\", \"qwen\")',\n maxItems: 15,\n options(this: { userInput: string }) {\n const query = this.userInput.trim().toLowerCase()\n\n if (!query) {\n return [\n {\n value: CUSTOM_PATTERN,\n label: '✏️ Enter custom pattern (e.g. \"claude-*\")',\n },\n ]\n }\n\n const filtered = models\n .filter(m => {\n const text = `${m.id} ${m.name}`.toLowerCase()\n return text.includes(query)\n })\n .slice(0, 14)\n .map(m => ({\n value: m.id,\n label: formatModelLabel(m),\n hint: formatModelHint(m),\n }))\n\n return [\n ...filtered,\n { value: CUSTOM_PATTERN, label: '✏️ Enter custom pattern (e.g. \"claude-*\")' },\n ]\n },\n filter: (_search: string, _option: { value: string }) => true,\n })\n\n if (isCancel(result)) return null\n return result as string\n}\n\nasync function enterPattern(models: OpenRouterModel[]): Promise<string | null> {\n const pattern = await clack.text({\n message: 'Enter model pattern',\n placeholder: 'e.g. claude-*, gpt-4*, anthropic/*',\n validate: v => {\n if (!v?.trim()) return 'Pattern cannot be empty'\n return undefined\n },\n })\n\n if (isCancel(pattern)) return null\n\n const pat = (pattern as string).trim()\n const matches = countPatternMatches(pat, models)\n if (matches > 0) {\n clack.log.info(`Pattern \"${pat}\" matches ${matches} model(s)`)\n } else {\n clack.log.warn(\n `Pattern \"${pat}\" does not match any current models — it will still be saved`,\n )\n }\n\n return pat\n}\n\nasync function configureProviderAndSave(\n configPath: string,\n client: OpenRouterClient,\n modelKey: string,\n isPattern: boolean,\n): Promise<void> {\n const mode = await selectRoutingMode('Configure provider routing')\n if (isCancel(mode)) return\n\n if (mode === 'skip') {\n setModelOverride(configPath, modelKey, {})\n clack.outro('Done — override saved without provider routing')\n return\n }\n\n const providerOptions = await fetchProvidersForModel(client, modelKey, isPattern)\n if (!providerOptions) return\n\n const override = await selectProvidersByMode(mode as string, providerOptions)\n if (!override) return\n\n clack.log.info(\n `Proposed override:\\n ${modelKey}:\\n ${formatOverrideYaml(override)}`,\n )\n\n const save = await clack.confirm({ message: 'Save to config?' })\n if (isCancel(save) || !save) {\n clack.outro('Cancelled')\n return\n }\n\n setModelOverride(configPath, modelKey, override)\n clack.outro('✓ Model override saved')\n}\n\nfunction displayModelInfo(model: OpenRouterModel): void {\n clack.log.info(`${model.name || model.id}`)\n clack.log.info(` Context: ${formatContextLength(model.context_length)} tokens`)\n clack.log.info(\n ` Pricing: ${formatPricing(model.pricing.prompt, model.pricing.completion)}`,\n )\n if (model.pricing.input_cache_read && model.pricing.input_cache_read !== '0') {\n clack.log.info(` Cache read: ${formatPrice(model.pricing.input_cache_read)}`)\n }\n if (model.pricing.input_cache_write && model.pricing.input_cache_write !== '0') {\n clack.log.info(` Cache write: ${formatPrice(model.pricing.input_cache_write)}`)\n }\n if (model.top_provider?.max_completion_tokens) {\n clack.log.info(\n ` Max output: ${formatContextLength(model.top_provider.max_completion_tokens)} tokens`,\n )\n }\n if (model.architecture?.modality) {\n clack.log.info(` Modality: ${model.architecture.modality}`)\n }\n}\n\nfunction countPatternMatches(pattern: string, models: OpenRouterModel[]): number {\n if (pattern.endsWith('*')) {\n const prefix = pattern.slice(0, -1)\n return models.filter(m => m.id.startsWith(prefix)).length\n }\n return models.filter(m => m.id === pattern).length\n}\n\nfunction formatOverrideYaml(override: Record<string, unknown>): string {\n const parts: string[] = []\n if (override.provider && typeof override.provider === 'object') {\n const p = override.provider as Record<string, unknown>\n for (const [key, value] of Object.entries(p)) {\n parts.push(`provider.${key}: ${JSON.stringify(value)}`)\n }\n }\n return parts.join('\\n ') || '(empty)'\n}\n"],"mappings":"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"}