proxitor 0.2.0 → 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.
- package/README.md +221 -81
- package/dist/add.cjs +139 -0
- package/dist/add.cjs.map +1 -0
- package/dist/add.mjs +138 -0
- package/dist/add.mjs.map +1 -0
- package/dist/browse.cjs +88 -0
- package/dist/browse.cjs.map +1 -0
- package/dist/browse.mjs +87 -0
- package/dist/browse.mjs.map +1 -0
- package/dist/cli.cjs +148 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +149 -26
- package/dist/cli.mjs.map +1 -1
- package/dist/config.cjs +68 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.mjs +45 -0
- package/dist/config.mjs.map +1 -0
- package/dist/config2.cjs +75 -0
- package/dist/config2.cjs.map +1 -0
- package/dist/config2.mjs +74 -0
- package/dist/config2.mjs.map +1 -0
- package/dist/edit.cjs +82 -0
- package/dist/edit.cjs.map +1 -0
- package/dist/edit.mjs +81 -0
- package/dist/edit.mjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +223 -53
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +223 -53
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/list.cjs +33 -0
- package/dist/list.cjs.map +1 -0
- package/dist/list.mjs +31 -0
- package/dist/list.mjs.map +1 -0
- package/dist/providers.cjs +376 -0
- package/dist/providers.cjs.map +1 -0
- package/dist/providers.mjs +279 -0
- package/dist/providers.mjs.map +1 -0
- package/dist/proxy.cjs +128 -8
- package/dist/proxy.cjs.map +1 -1
- package/dist/proxy.mjs +99 -9
- package/dist/proxy.mjs.map +1 -1
- package/dist/remove.cjs +38 -0
- package/dist/remove.cjs.map +1 -0
- package/dist/remove.mjs +37 -0
- package/dist/remove.mjs.map +1 -0
- package/dist/validate.cjs +26 -0
- package/dist/validate.cjs.map +1 -0
- package/dist/validate.mjs +25 -0
- package/dist/validate.mjs.map +1 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,44 +1,92 @@
|
|
|
1
1
|
# proxitor
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
#
|
|
72
|
+
# no install needed
|
|
19
73
|
npx proxitor
|
|
20
74
|
```
|
|
21
75
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
### Start the proxy
|
|
76
|
+
---
|
|
25
77
|
|
|
26
|
-
|
|
27
|
-
# With env var
|
|
28
|
-
OPENROUTER_API_KEY=sk-... proxitor
|
|
78
|
+
## Quick Start
|
|
29
79
|
|
|
30
|
-
|
|
31
|
-
proxitor --openrouter-key sk-...
|
|
80
|
+
**1. Start the proxy**
|
|
32
81
|
|
|
33
|
-
|
|
34
|
-
|
|
82
|
+
```sh
|
|
83
|
+
OPENROUTER_API_KEY=sk-or-... proxitor
|
|
84
|
+
# Listening on http://0.0.0.0:8080
|
|
35
85
|
```
|
|
36
86
|
|
|
37
|
-
|
|
87
|
+
**2. Point your tools at it**
|
|
38
88
|
|
|
39
|
-
|
|
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
|
-
|
|
97
|
+
That's it. Requests flow through proxitor to OpenRouter, SSE streams pass through unchanged.
|
|
50
98
|
|
|
51
|
-
|
|
99
|
+
---
|
|
52
100
|
|
|
53
|
-
|
|
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
|
-
|
|
103
|
+
Proxitor looks for a config file in this order:
|
|
61
104
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
116
|
+
Control which provider handles your requests. All three options accept a string or an array:
|
|
69
117
|
|
|
70
118
|
```yaml
|
|
71
|
-
#
|
|
119
|
+
# Strict lock — only this provider, no fallbacks
|
|
72
120
|
provider:
|
|
73
|
-
only: "
|
|
121
|
+
only: "anthropic"
|
|
74
122
|
|
|
75
|
-
#
|
|
123
|
+
# Restricted pool — load balance between these providers only
|
|
76
124
|
provider:
|
|
77
125
|
only:
|
|
78
|
-
- "
|
|
79
|
-
- "
|
|
126
|
+
- "anthropic"
|
|
127
|
+
- "deepinfra"
|
|
80
128
|
|
|
81
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
187
|
+
**Match priority:** exact name > longer prefix > shorter prefix.
|
|
129
188
|
|
|
130
189
|
### Custom headers
|
|
131
190
|
|
|
132
|
-
Add
|
|
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
|
-
```
|
|
226
|
+
```sh
|
|
151
227
|
curl http://localhost:8080/health
|
|
152
228
|
```
|
|
153
229
|
|
|
154
|
-
|
|
230
|
+
---
|
|
155
231
|
|
|
156
|
-
|
|
157
|
-
proxitor [options]
|
|
232
|
+
## Interactive Config Manager
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
# Install dependencies
|
|
173
|
-
pnpm install
|
|
269
|
+
**"Use specific providers only" / "Ignore specific providers"** — multiselect, pick all that apply:
|
|
174
270
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
#
|
|
182
|
-
|
|
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
|
-
#
|
|
185
|
-
|
|
292
|
+
◇ Select provider #3 (or cancel to finish)
|
|
293
|
+
│ ● ✓ Done
|
|
186
294
|
|
|
187
|
-
|
|
188
|
-
pnpm run lint:fix
|
|
189
|
-
pnpm run format
|
|
295
|
+
◇ Allow fallbacks to other providers? Yes
|
|
190
296
|
|
|
191
|
-
|
|
192
|
-
pnpm run build
|
|
297
|
+
◇ Save to config? Yes
|
|
193
298
|
|
|
194
|
-
|
|
195
|
-
|
|
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
|
package/dist/add.cjs.map
ADDED
|
@@ -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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"}
|