myapikey 0.19.1 → 0.19.3

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 CHANGED
@@ -2,311 +2,134 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/myapikey?logo=npm)](https://www.npmjs.com/package/myapikey)
4
4
  [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
5
- [![node](https://img.shields.io/badge/node.js-%3E%3D18-339933?logo=node.js&logoColor=white)](#quick-start)
5
+ [![node](https://img.shields.io/badge/node.js-%3E%3E18-339933?logo=node.js&logoColor=white)](#up-and-running-in-60-seconds)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](#development)
7
7
 
8
8
  English | [简体中文](README.zh-CN.md)
9
9
 
10
- **One address, one key, all your models without translation.**
10
+ **One address, one key, every AI tool you use.** A tiny self-hosted LLM gateway for your home server: keep your provider keys in one place, point every tool at the same URL with the same key, and fail over automatically when a backend breaks.
11
11
 
12
- MyAPIKey is a personal LLM gateway. Point every AI tool you use (Claude Code, OpenAI-compatible CLIs, editors, scripts) at a single local address with a single API key. Each call is forwarded **as-is** to a backend that speaks the same wire format, with automatic failover when a backend is down. Nothing is converted between OpenAI and Anthropic formats — what you send is exactly what reaches the backend.
12
+ ## Sound familiar?
13
13
 
14
- > Built for a home server / LAN. No database, no TLS by default, no translation layer just a thin, transparent forwarder you fully control.
14
+ - You use **Claude Code, a few OpenAI-compatible CLIs, editor plugins, scripts** and each one needs its own base URL + API key pasted in.
15
+ - Your **real keys** (OpenAI, Anthropic, OpenRouter, a local Ollama…) are scattered across a dozen config files. Rotating one means remembering everywhere it's pasted.
16
+ - Your **main provider 429s or goes down** mid-task and the tool just dies. Switching to a backup means re-editing configs.
17
+ - You tried **one-api / new-api-style gateways**, which *translate* between formats — and drop fields, mangle streams, or hold new upstream parameters hostage until someone updates the gateway.
15
18
 
16
- ---
19
+ MyAPIKey is built to end exactly this.
17
20
 
18
- ## Table of contents
21
+ ## What you get
19
22
 
20
- - [What is this, and who is it for?](#what-is-this-and-who-is-it-for)
21
- - [Quick start](#quick-start)
22
- - [Add your first backend](#add-your-first-backend)
23
- - [Connect a tool](#connect-a-tool)
24
- - [How routing works](#how-routing-works)
25
- - [Model name mapping](#model-name-mapping)
26
- - [Web UI](#web-ui)
27
- - [CLI reference](#cli-reference)
28
- - [API surface](#api-surface)
29
- - [Storage](#storage)
30
- - [Development](#development)
31
- - [Notes and non-goals](#notes-and-non-goals)
23
+ - **One address, one key.** Every tool points at `http://<your-server>:7800` with a single `sk-myapikey-…` key. Real provider keys live only inside the gateway — rotate one, swap a backend, and no tool config ever changes again.
24
+ - **Automatic failover + circuit breaker.** Give a model an ordered chain of backends. On `429` / `5xx` / timeout the next one takes over; a source that keeps failing cools down (30 s → 5 min) so later calls skip it. Your agent never notices.
25
+ - **Pure passthrough, zero translation.** OpenAI-format calls reach OpenAI-speaking backends, Anthropic-format calls reach Anthropic-speaking backends — bodies forwarded byte-for-byte, streaming straight through. New upstream fields and parameters work without the gateway "supporting" them first.
26
+ - **Rate limiting that queues instead of 429ing.** Cap each backend's rpm, or give a model an even per-minute pace; excess calls wait in the gateway instead of coming straight back as `429`.
27
+ - **Model name mapping.** Expose a friendly name to your tools (`claude-sonnet-4`); each backend maps it to its own real id (`claude-sonnet-4-20250514` here, something else there).
28
+ - **A default thinking level per slot.** Pin a reasoning effort / token budget on a route; it overrides whatever the request carried — handy for taming Claude Code.
29
+ - **See everything.** Web UI (English / 简体中文): live call log, success rate, p50/p95 latency, per-model / per-backend / per-day stats. The CLI can do all of it too.
30
+ - **Featherweight.** No database — one `data.json` plus a `logs.jsonl`. Runs with `npx`. Built for LAN / home-server use.
32
31
 
33
- ---
32
+ ## Up and running in 60 seconds
34
33
 
35
- ## What is this, and who is it for?
36
-
37
- If any of these sound familiar, MyAPIKey is for you:
38
-
39
- - You juggle **several API keys** across OpenAI, Anthropic, OpenRouter, a local model, etc., and you're tired of pasting a different one into every tool.
40
- - You want **one stable address** for your agents, even when the provider behind it changes.
41
- - You want **automatic fallback** — if your primary backend rate-limits or goes down, the next one takes over.
42
- - You want to **rename or swap a model** without editing every tool's config.
43
- - You'd like a **log of every call** (latency, status, which backend served it).
44
-
45
- MyAPIKey gives you all of that. You keep your real provider keys inside the gateway; your tools only ever know `http://<host>:7800` and one `sk-myapikey-…` key.
46
-
47
- **The one thing to know up front:** the gateway **forwards in your tool's native format and never translates**. A call to the OpenAI endpoint reaches an OpenAI-speaking backend; a call to the Anthropic endpoint reaches an Anthropic-speaking backend. This keeps your requests and responses lossless. (See [How routing works](#how-routing-works) once you're set up.)
48
-
49
- ---
50
-
51
- ## Quick start
52
-
53
- Requires **Node.js 18+**. No install needed — run it straight from npm:
34
+ Requires Node.js 18+.
54
35
 
55
36
  ```bash
56
37
  npx myapikey serve # → http://localhost:7800
57
38
  ```
58
39
 
59
- Or install it globally:
60
-
61
- ```bash
62
- npm install -g myapikey
63
- myapikey serve
64
- ```
65
-
66
- On first run it prints generated credentials **and saves them to a file** so you can't lose them:
67
-
68
- ```
69
- First run — here are your credentials (save them):
70
- username : <random> (web login)
71
- password : <random> (web login)
72
- api key : sk-myapikey-… (put this in the tool's "api key" field)
73
- ↳ also written to ~/.myapikey/credentials.txt
74
- ```
75
-
76
- Open `http://localhost:7800` in a browser and sign in with the **username / password** to reach the web UI. The **API key** is separate — that's what your AI tools use later.
77
-
78
- > Forgot the credentials? `cat ~/.myapikey/credentials.txt` (regenerated on every start). Override the data location with `--data-dir <path>` or the `MYAPIKEY_DATA_DIR` env var.
40
+ First run prints your credentials and also saves them to `~/.myapikey/credentials.txt`:
79
41
 
80
- That's the gateway running. Next, give it a backend to forward to.
81
-
82
- ---
83
-
84
- ## Add your first backend
85
-
86
- A **backend** (provider) is one upstream API: OpenAI, Anthropic, OpenRouter, a local Ollama, a vendor like Volcengine Ark, etc. You add it once with its real key, then enable models through it.
87
-
88
- ### Easiest: the web UI
89
-
90
- 1. Open the **Models** tab → **Add backend**.
91
- 2. Enter a name, the backend's base URL(s), and its API key.
92
- 3. Pick the wire format(s) it speaks: `openai`, `anthropic` (or both).
93
- 4. Save, then click **discover** to pull its model list.
94
- 5. **Enable** the models you want, per format slot.
95
-
96
- ### Or, the CLI
97
-
98
- ```bash
99
- # An OpenAI-compatible backend. The OpenAI base includes the version segment
100
- # (e.g. /v1, or a vendor's own like Ark's /api/v3).
101
- myapikey provider add openai-direct \
102
- --base-url-openai https://api.openai.com/v1 \
103
- --key sk-... \
104
- --formats openai
105
-
106
- # An Anthropic backend. The Anthropic base EXCLUDES /v1
107
- # (https://api.anthropic.com, or Ark's /api/plan).
108
- myapikey provider add anthropic-direct \
109
- --base-url-anthropic https://api.anthropic.com \
110
- --key sk-ant-... \
111
- --formats anthropic
112
-
113
- # See what each offers, then enable a model on a slot:
114
- myapikey provider models openai-direct
115
- myapikey model enable gpt-4o-mini --format openai --via openai-direct
116
- ```
117
-
118
- That model is now reachable through the gateway. To add a **fallback** for the same model (used when the first one fails), add another backend and chain it:
42
+ 1. Open `http://localhost:7800` and sign in with the printed **username / password** (web login).
43
+ 2. **Models** tab → **Add backend**: paste the backend's base URL + its real API key, tick the formats it speaks (`openai` / `anthropic`), then **discover** → **enable** the models you want.
44
+ 3. Point your tools at the gateway — every tool's "API key" field gets the **gateway key**, not your web password:
119
45
 
120
46
  ```bash
121
- myapikey provider add backup --base-url-openai https://api.openrouter.ai/api/v1 --key sk-or-... --formats openai
122
- myapikey model add-provider gpt-4o-mini backup --format openai
123
- myapikey model prioritize gpt-4o-mini openai-direct backup --format openai # left = primary
124
- myapikey model list # see the routing table
125
- ```
126
-
127
- > `--format` selects the routing **slot**: `openai` (for `/openai/v1/chat/completions`), `anthropic` (for `/anthropic/v1/messages`), or `responses` (for `/openai/v1/responses`). The `responses` slot only accepts backends you've marked **supportsResponses** — set that toggle in the web UI (the CLI doesn't expose it yet).
128
-
129
- ---
130
-
131
- ## Connect a tool
132
-
133
- Once a model is enabled, point any agent at the gateway. Get your connection info:
134
-
135
- ```bash
136
- myapikey whoami # prints base url + api key + ready-to-paste env lines
137
- ```
138
-
139
- For an **OpenAI-compatible** tool (covers `/chat/completions` *and* `/responses`):
47
+ # Claude Code / anything Anthropic
48
+ export ANTHROPIC_BASE_URL=http://localhost:7800/anthropic
49
+ export ANTHROPIC_API_KEY=sk-myapikey-…
140
50
 
141
- ```bash
51
+ # OpenAI-compatible tools (Codex, CLIs, plugins)
142
52
  export OPENAI_BASE_URL=http://localhost:7800/openai/v1
143
- export OPENAI_API_KEY=<gateway api key>
144
- ```
145
-
146
- For **Anthropic / Claude Code**:
147
-
148
- ```bash
149
- export ANTHROPIC_BASE_URL=http://localhost:7800/anthropic
150
- export ANTHROPIC_API_KEY=<gateway api key>
53
+ export OPENAI_API_KEY=sk-myapikey-…
151
54
  ```
152
55
 
153
- > Whatever a tool calls its "API key" field, put the **gateway API key** there *not* your web login password. The login password is only for the web UI and CLI admin commands.
56
+ > Why `…/openai/v1` but `…/anthropic`? Each SDK appends its own paths (OpenAI adds `/chat/completions`, Anthropic adds `/v1/messages`), so the gateway follows each ecosystem's own convention. Each surface also has its own `/models`, listing only models enabled for that format. `myapikey whoami` prints all of this ready to paste, anytime.
154
57
 
155
- Quick smoke test without any tool:
58
+ 4. Smoke test without any tool:
156
59
 
157
60
  ```bash
158
61
  myapikey call gpt-4o-mini "Say hello in one sentence."
159
62
  ```
160
63
 
161
- **Why two base URLs?** The gateway exposes two separate agent surfaces — `/openai/v1` and `/anthropic/v1` each with its own `GET /models` (an OpenAI client discovers only openai-enabled models, an Anthropic client only anthropic-enabled ones). Each ecosystem's SDK appends its own paths, so the OpenAI SDK points at `…/openai/v1` (it appends `/chat/completions`, `/responses`, `/models`) and the Anthropic SDK / Claude Code points at `…/anthropic` (it appends `/v1/messages`, `/v1/models`).
162
-
163
- **Raw HTTP** (no SDK) — hit either surface directly with the gateway API key as a `Bearer` token:
164
-
165
- ```bash
166
- # OpenAI family
167
- curl http://localhost:7800/openai/v1/chat/completions \
168
- -H "Authorization: Bearer <gateway api key>" -H "Content-Type: application/json" \
169
- -d '{"model":"<model>","messages":[{"role":"user","content":"hi"}]}'
170
-
171
- # Anthropic family
172
- curl http://localhost:7800/anthropic/v1/messages \
173
- -H "Authorization: Bearer <gateway api key>" -H "Content-Type: application/json" \
174
- -d '{"model":"<model>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
175
- ```
176
-
177
- Whichever endpoint you point a tool at, the gateway forwards in that format and never translates — so make sure each model you call is backed by at least one source on the matching slot ([see below](#how-routing-works)).
178
-
179
- ---
64
+ Prefer the terminal? `provider add` `provider models` `model enable` does the same job cheat sheet below.
180
65
 
181
- ## How routing works
66
+ ## How routing works (the whole story)
182
67
 
183
- The gateway is a **directional forwarder, not a translator**. Four rules explain everything:
68
+ - The gateway has two entrances: `/openai/v1/*` (chat completions, responses, models) and `/anthropic/v1/*` (messages, models). A request is forwarded in the entrance's format, **never translated**.
69
+ - Each model has an independent, ordered backend chain **per format**. Within a chain, `429` / `5xx` / timeouts fail over to the next backend; other `4xx` errors come back as-is (they're the caller's fault, not the backend's). No failover once streaming has started. If no backend serves that model on that format, you get a `404` — a clear failure beats a silent translation.
70
+ - Anything that speaks either format works: OpenAI, Anthropic, OpenRouter, Ollama / vLLM, Volcengine Ark, your company's internal gateway…
184
71
 
185
- 1. **The endpoint picks the slot.** `/openai/v1/chat/completions` → the **openai** slot. `/openai/v1/responses` → the **responses** slot. `/anthropic/v1/messages` → the **anthropic** slot. Each is a distinct wire format, and the body is forwarded verbatim — nothing is converted.
72
+ ## Why no translation?
186
73
 
187
- 2. **Each model has three independent slots.** For one model you can enable `openai`, `responses`, and `anthropic` separately, and each has its own ordered source chain. Enable a model only on the slots its backends actually speak.
74
+ one-api / new-api convert OpenAI Anthropic shapes. Convenient, until a field gets dropped, a stream gets mangled, or a new upstream parameter needs a gateway update before you can send it. OpenRouter is hosted and takes a cut per call. MyAPIKey's answer is to not have a translation layer at all:
188
75
 
189
- 3. **A source qualifies per slot.** The `openai` and `anthropic` slots accept sources carrying that wire format; the `responses` slot only accepts sources marked `supportsResponses` (most OpenAI-compatible backends don't implement the Responses API). A `supportsResponses` source can sit in the **openai** chain *and* the **responses** chain at once.
76
+ | | MyAPIKey | one-api / new-api | OpenRouter |
77
+ |---|---|---|---|
78
+ | Deploy | self-hosted, one `npx` | self-hosted, heavier | hosted service |
79
+ | Cost | free | free | markup per call |
80
+ | Formats | passthrough, byte-for-byte | translated | translated |
81
+ | New upstream params | work immediately | wait for gateway support | wait for platform support |
82
+ | Provider keys | on your machine | on your machine | on their platform |
190
83
 
191
- 4. **Priority + failover, within a slot.** Candidates are tried in your priority order. On `429` / `5xx` / timeout, the gateway tries the next one — and the failing source enters a brief **circuit-breaker cooldown** (exponential, 30 s → 5 min) so later calls skip it until it recovers; a success closes the circuit. Other `4xx` errors are returned as-is (they're the caller's fault, not the backend's). Once streaming has started, there's no failover.
84
+ The trade-off, stated plainly: an Anthropic-format backend cannot be called through the OpenAI entrance. If what you want is format conversion, this isn't your tool (yet).
192
85
 
193
- **The trade-off this buys you:** call an endpoint no source serves for that model and you get a `404` — not a silent translation. That's the price of zero-loss passthrough. (Translation could be layered on later without changing the agent-facing endpoints.)
86
+ ## Managing it
194
87
 
195
- This is the core difference from aggregators like OpenRouter or one-api, which convert between OpenAI and Anthropic request/response shapes.
88
+ **Web UI** `http://localhost:7800`, English + 简体中文. Five tabs: **Connect** (copy-paste-ready env lines), **Models** (backends, discovery, per-format enable, drag-to-order chains, name mapping, test call), **Logs** (live timeline), **Stats** (counts, success rate, p50/p95 by model / backend / day), **Settings** (rotate API key, change password, circuit-breaker state, storage paths).
196
89
 
197
- ---
90
+ **CLI cheat sheet** (same admin API as the web UI — the two never drift apart):
198
91
 
199
- ## Model name mapping
200
-
201
- Sometimes the name a tool asks for isn't the name the backend expects. MyAPIKey can **rewrite the model name per source**, on the way through — still pure passthrough, no format translation.
202
-
203
- Set it in the web UI (**Models** tab): when adding a source, there's an **upstream model** field; or edit it inline on any chain member. (Not exposed in the CLI yet.)
204
-
205
- Use it to:
206
-
207
- - **Alias a friendly name to a versioned id** — expose `claude-sonnet-4` to your tools, while the gateway sends `claude-sonnet-4-20250514` upstream.
208
- - **Point one public name at different real models per source** — e.g. your `gpt-4` slot's primary source sends `gpt-4o`, its fallback sends `gpt-4-turbo`.
209
- - **Bridge naming quirks** between two backends that implement the "same" model under different ids.
210
-
211
- If no mapping is set for a source, the public model name is sent unchanged.
212
-
213
- ---
214
-
215
- ## Web UI
216
-
217
- Visit `http://localhost:7800`, sign in with your username/password. Ships in **English and Chinese** (toggle in the top bar). Five tabs:
218
-
219
- - **Connect** — copy-paste base URL / API key / login, plus ready-to-use SDK env lines.
220
- - **Models** — full CRUD: add sources, discover & enable models per slot, drag-order priority, toggle each slot, set [model name mapping](#model-name-mapping), and run an end-to-end **test** call against any model + slot.
221
- - **Logs** — live recent-calls timeline (latency, status, source, format), with circuit-breaker cooldown events shown alongside the failures that triggered them.
222
- - **Stats** — aggregate call counts / success rate / latency (p50, p95) by model, source, format, and day, over a selectable range (24h / 7d / 30d / 90d / all).
223
- - **Settings** — rotate the API key, change the account password, view/reset circuit-breaker state, and see where `data.json` + `logs.jsonl` live.
92
+ | I want to… | Command |
93
+ |---|---|
94
+ | run the gateway | `myapikey serve [--port 7800] [--data-dir <dir>]` |
95
+ | print what to paste into tools | `myapikey whoami` |
96
+ | add a backend | `myapikey provider add <name> --base-url-openai https://api.openai.com/v1 --key sk-… --formats openai` |
97
+ | see a backend's models | `myapikey provider models <name>` |
98
+ | enable a model | `myapikey model enable <model> --format openai --via <backend>` |
99
+ | add a fallback + set order | `myapikey model add-provider <model> <backend> --format openai` · `myapikey model prioritize <model> <primary> <backup> --format openai` |
100
+ | see the routing table | `myapikey model list` |
101
+ | pace a model | `myapikey model pace <model> <rpm\|0>` |
102
+ | pin a slot's thinking level | `myapikey model thinking <model> <index> [value] --format <fmt>` |
103
+ | quick test | `myapikey call <model> "hi"` |
224
104
 
225
- It's the same admin API the CLI uses configure either way.
105
+ > OpenAI base URLs **include** the version segment (`/v1`, Ark's `/api/v3`); Anthropic base URLs **exclude** it (`https://api.anthropic.com`). The `responses` format only accepts backends you've marked `supportsResponses` (web UI toggle).
106
+ > CLI global flags: `-u/--url`, `--user`, `--pass`, `--api-key`, or env `MYAPIKEY_URL` / `MYAPIKEY_USER` / `MYAPIKEY_PASS` / `MYAPIKEY_API_KEY`.
226
107
 
227
- ---
108
+ ## Where your data lives
228
109
 
229
- ## CLI reference
110
+ One directory (default `~/.myapikey`, override with `--data-dir` or `MYAPIKEY_DATA_DIR`):
230
111
 
231
- | Command | |
112
+ | File | What |
232
113
  |---|---|
233
- | `serve [--port 7800] [--data-dir <dir>] [--web-dir <path>]` | run the gateway |
234
- | `whoami` | print connection info for agents (base url, api key, login, example env) |
235
- | `provider add <name> --base-url-openai URL [--base-url-anthropic URL] --key KEY --formats openai,anthropic` | add a backend (set `supportsResponses` in the web UI) |
236
- | `provider list` | list backends |
237
- | `provider models <ref>` | discover models offered by a backend |
238
- | `provider remove <ref>` | remove a backend (id or name) |
239
- | `model enable <name> --format <fmt> [--via <ref>]` | enable a model on a slot (openai / anthropic / responses) |
240
- | `model disable <name> --format <fmt>` | disable one slot (keeps config) |
241
- | `model list` | show the per-slot routing table |
242
- | `model add-provider <name> <ref> --format <fmt>` | add a fallback source for a slot |
243
- | `model remove-provider <name> <ref> --format <fmt>` | remove a source from a slot |
244
- | `model prioritize <name> <ref>... --format <fmt>` | set source order for a slot (left = primary) |
245
- | `model remove <name>` | remove a model entirely (all slots) |
246
- | `call <model> [prompt...]` | quick test through the gateway (OpenAI path) |
247
-
248
- `<ref>` is a provider id or name. Global flags: `-u/--url`, `--user`, `--pass`, `--api-key` (or `MYAPIKEY_URL` / `MYAPIKEY_USER` / `MYAPIKEY_PASS` / `MYAPIKEY_API_KEY` env vars).
249
-
250
- ---
251
-
252
- ## API surface
253
-
254
- **Agent-facing (two surfaces, one API key — `Authorization: Bearer` or `x-api-key`):**
255
-
256
- - `POST /openai/v1/chat/completions` — OpenAI-format proxy
257
- - `POST /openai/v1/responses` — OpenAI Responses API (only sources marked `supportsResponses`)
258
- - `GET /openai/v1/models` — models enabled on the **openai** slot, OpenAI list shape (public — no key needed)
259
- - `POST /anthropic/v1/messages` — Anthropic-format proxy
260
- - `GET /anthropic/v1/models` — models enabled on the **anthropic** slot, Anthropic list shape (public — no key needed)
261
- - `GET /health` — public liveness check
262
-
263
- **Admin (`/admin`, account password — HTTP Basic):**
264
-
265
- - `/admin/account` `GET` / `PUT` — read or change username/password (either field optional)
266
- - `/admin/api-key` `GET`, `/admin/api-key/rotate` `POST` — the key agents use
267
- - `/admin/connection` `GET` — detected LAN IP (for pointing other machines at the gateway)
268
- - `/admin/providers`, `/admin/providers/:id` `POST`/`PUT`/`DELETE`, `/admin/providers/:id/discover` `POST` — backend CRUD + model discovery
269
- - `/admin/models`, `/admin/models/:name/{providers,priority,map,disable,test}` — routing-table CRUD, [per-source name mapping](#model-name-mapping), and end-to-end test
270
- - `/admin/logs` `GET` — recent calls (tail of the on-disk log)
271
- - `/admin/stats?range=24h|7d|30d|90d|all` `GET` — aggregate stats over retained history
272
- - `/admin/storage` `GET` — where `data.json` + `logs.jsonl` live
273
- - `/admin/circuit` `GET`, `/admin/circuit/:id/reset` `POST` — circuit-breaker snapshot + manual reset
274
-
275
- ---
276
-
277
- ## Storage
278
-
279
- Everything lives in one data directory (default `~/.myapikey`):
280
-
281
- | File | Contents |
282
- |---|---|
283
- | `data.json` | all config — providers, the per-model routing table, account, API key |
284
- | `logs.jsonl` | call history, one JSON object per line (retained ~90 days / 1 M lines) |
285
- | `credentials.txt` | human-readable web login + API key, regenerated on every startup |
286
- | `client.json` | CLI client profile (base url + login + API key) |
287
-
288
- Override the directory with `--data-dir <path>` or `MYAPIKEY_DATA_DIR`. Call logs are never held in memory: the Logs timeline tail-reads the newest ~200 entries, and stats stream over the full retained file on demand.
289
-
290
- ---
114
+ | `data.json` | all config: backends, routing table, account, API key |
115
+ | `logs.jsonl` | call history (kept ~90 days / 1 M lines) |
116
+ | `credentials.txt` | your login + API key in plain text, rewritten every start |
117
+ | `client.json` | the CLI's saved connection profile |
291
118
 
292
119
  ## Development
293
120
 
294
121
  ```bash
295
122
  npm install
296
- npm run build:web # build the Vue UI into packages/web/dist (one time, and after UI changes)
297
- npm start # tsx ... serve (gateway on :7800)
298
- npm run dev # gateway, with watch reload
299
- npm run dev:web # vite dev server (proxies /openai, /anthropic, and /admin to :7800)
300
- npm run typecheck # tsc (core) + vue-tsc (web)
123
+ npm run build:web # build the Vue UI into packages/web/dist
124
+ npm run dev # gateway with watch reload
125
+ npm run dev:web # vite dev server (proxies API calls to :7800)
126
+ npm test # vitest unit + integration
127
+ npm run test:e2e # playwright against a real gateway process
128
+ npm run typecheck # tsc + vue-tsc
301
129
  ```
302
130
 
303
- Testing the gateway by hand: spin up a mock upstream and run `serve` on a scratch port with `--data-dir /tmp/myapikey`.
304
-
305
- ---
306
-
307
- ## Notes and non-goals
131
+ ## Honest limits
308
132
 
309
- - **No OpenAI↔Anthropic translation.** If a backend doesn't speak the format an agent used, that call fails (`404`). Addable later without changing the agent contract.
310
- - **No database.** One `data.json` for config; `logs.jsonl` for retained call history.
311
- - **No TLS.** Intended for LAN / home-server use on your own network. Put it behind a reverse proxy with TLS if you expose it beyond that.
312
- - **The login password and the API key are separate.** The API key is what agents use; rotate it (`POST /admin/api-key/rotate` or the web UI) if a tool's config leaks. Don't hand either to untrusted code.
133
+ - **No OpenAI↔Anthropic translation.** Wrong entrance for a model `404`.
134
+ - **Single user, no TLS.** Meant for your own network; put a reverse proxy in front if you expose it.
135
+ - **Web password ≠ API key.** The password administers the UI/CLI; agents use the `sk-myapikey-…` key. If a tool config leaks, rotate the key (Settings, or `POST /admin/api-key/rotate`) and nothing else changes.
package/README.zh-CN.md CHANGED
@@ -2,311 +2,134 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/myapikey?logo=npm)](https://www.npmjs.com/package/myapikey)
4
4
  [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
5
- [![node](https://img.shields.io/badge/node.js-%3E%3D18-339933?logo=node.js&logoColor=white)](#快速开始)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](#开发指南)
5
+ [![node](https://img.shields.io/badge/node.js-%3E%3E18-339933?logo=node.js&logoColor=white)](#60-秒跑起来)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](#开发)
7
7
 
8
8
  [English](README.md) | 简体中文
9
9
 
10
- **一个地址、一把密钥,接入你所有的模型——且绝不翻译。**
10
+ **一个地址、一把 key,接上你所有的 AI 工具。** 一个跑在自己家用服务器上的轻量 LLM 网关:供应商的 key 收进网关统一保管,所有工具只认同一个地址 + 同一把 key,后端挂了自动切换。
11
11
 
12
- MyAPIKey 是一个个人 LLM 网关。把你用到的每个 AI 工具(Claude Code、兼容 OpenAI 的命令行、编辑器、脚本)都指向同一个本地地址、用同一把 API Key。每一次调用都会**原样转发**给一个说相同线材格式(wire format)的后端,并在某个后端掉线时自动故障转移。OpenAI 与 Anthropic 格式之间不做任何转换——你发出去什么,后端就收到什么。
12
+ ## 是不是在说你
13
13
 
14
- > 面向家用服务器 / 局域网设计。默认无数据库、无 TLS、不做格式转换——只是一个你能完全掌控的、轻量透明的转发层。
14
+ - 你用着 **Claude Code、好几个 OpenAI 兼容 CLI、编辑器插件、脚本**——每个都得单独填一遍 base URL + API key。
15
+ - 你的**真实 key**(OpenAI、Anthropic、OpenRouter、本地 Ollama……)散落在十几个配置文件里。想换一把 key?先想清楚它粘在了哪儿。
16
+ - **主力供应商一限流或宕机**,活儿干到一半就断。切备用?又得改一轮配置。
17
+ - 你试过 **one-api / new-api 这类网关**,它们在格式之间做*翻译*——然后你遇到丢字段、流式错乱、新参数要等网关适配才能用。
15
18
 
16
- ---
19
+ MyAPIKey 就是为治这些而生的。
17
20
 
18
- ## 目录
21
+ ## 你能得到什么
19
22
 
20
- - [这是什么,适合谁用?](#这是什么适合谁用)
21
- - [快速开始](#快速开始)
22
- - [添加第一个后端](#添加第一个后端)
23
- - [连接你的工具](#连接你的工具)
24
- - [路由是怎么工作的](#路由是怎么工作的)
25
- - [模型名映射](#模型名映射)
26
- - [网页界面](#网页界面)
27
- - [命令行参考](#命令行参考)
28
- - [接口一览](#接口一览)
29
- - [存储说明](#存储说明)
30
- - [开发指南](#开发指南)
31
- - [说明与非目标](#说明与非目标)
23
+ - **一个地址 + 一把 key。** 所有工具都指向 `http://你的机器:7800`,只用一把 `sk-myapikey-…`。真实供应商 key 只存在网关里——轮换、换后端,都不用再碰任何工具的配置。
24
+ - **自动故障转移 + 熔断。** 给同一个模型配一条有序的后端链:429 / 5xx / 超时自动切下一条;连续失败的后端进入冷却(30 秒 → 5 分钟),后续请求先跳过它。你的 agent 毫无感知。
25
+ - **纯透传,零翻译。** OpenAI 格式的调用只发给说 OpenAI 格式的后端,Anthropic 同理——请求/响应逐字节转发,流式直通。上游新出的字段、新参数,不需要网关"适配"就能用。
26
+ - **限流靠排队,不甩 429。** 给每个后端设 rpm 上限,或给模型设匀速节奏;超出的调用先在网关排队,而不是直接把 429 甩回工具。
27
+ - **模型名映射。** 对工具暴露一个好记的名字(`claude-sonnet-4`);每个后端各自映射到真实 id(这里发 `claude-sonnet-4-20250514`,那边发别的)。
28
+ - **每个槽位可固定默认 thinking 档位。** 在路由上钉死一个推理档位 / 预算,覆盖请求里带的设置——想管住 Claude Code 的思考行为,网关说了算。
29
+ - **全程看得见。** Web 界面(中/英):实时调用日志、成功率、p50/p95 延迟,按模型 / 来源 / 日期的统计;CLI 功能完全对等。
30
+ - **轻得没负担。** 无数据库——一个 `data.json` 加一个 `logs.jsonl`;`npx` 一行起跑;为局域网 / 家用服务器设计。
32
31
 
33
- ---
32
+ ## 60 秒跑起来
34
33
 
35
- ## 这是什么,适合谁用?
36
-
37
- 如果下面几条说到你心坎里,那 MyAPIKey 就适合你:
38
-
39
- - 你在 OpenAI、Anthropic、OpenRouter、本地模型之间来回切换**好几把 API Key**,受够了每个工具都要粘一份不同的密钥。
40
- - 你想让 agent 始终指向**同一个稳定地址**,哪怕背后的供应商变了。
41
- - 你想要**自动故障转移**——主后端被限流或掉线时,下一个顶上。
42
- - 你想**在不改动任何工具配置**的前提下,重命名或替换某个模型。
43
- - 你想要**每一次调用的记录**(延迟、状态、由哪个后端服务)。
44
-
45
- MyAPIKey 一次性给你这些。你把真实的供应商密钥留在网关内部;你的工具只认识 `http://<主机>:7800` 和一把 `sk-myapikey-…` 密钥。
46
-
47
- **先知道一件事:网关按你工具的原生格式转发,绝不翻译。** 调用 OpenAI 端点的请求只会到达支持 OpenAI 格式的后端;调用 Anthropic 端点的请求只会到达支持 Anthropic 格式的后端。这保证了请求与响应零损耗。(配好之后,可看 [路由是怎么工作的](#路由是怎么工作的)。)
48
-
49
- ---
50
-
51
- ## 快速开始
52
-
53
- 需要 **Node.js 18+**。无需安装,直接用 npm 跑:
34
+ 需要 Node.js 18+。
54
35
 
55
36
  ```bash
56
37
  npx myapikey serve # → http://localhost:7800
57
38
  ```
58
39
 
59
- 或全局安装:
60
-
61
- ```bash
62
- npm install -g myapikey
63
- myapikey serve
64
- ```
65
-
66
- 首次运行会打印生成的凭据,**并同时写入文件**,这样你就不会弄丢:
67
-
68
- ```
69
- First run — here are your credentials (save them):
70
- username : <随机> (网页登录用)
71
- password : <随机> (网页登录用)
72
- api key : sk-myapikey-… (填进工具的 "api key" 字段)
73
- ↳ also written to ~/.myapikey/credentials.txt
74
- ```
75
-
76
- 在浏览器打开 `http://localhost:7800`,用**用户名 / 密码**登录进入网页界面。**API Key** 是分开的另一把——那是你的 AI 工具稍后用的。
77
-
78
- > 忘了凭据?`cat ~/.myapikey/credentials.txt`(每次启动都会重新生成)。用 `--data-dir <路径>` 或环境变量 `MYAPIKEY_DATA_DIR` 可改数据存放位置。
79
-
80
- 网关已经跑起来了。下一步,给它一个可以转发的后端。
81
-
82
- ---
83
-
84
- ## 添加第一个后端
85
-
86
- 一个**后端**(provider / 来源)就是一个上游 API:OpenAI、Anthropic、OpenRouter、本地 Ollama、火山引擎 Ark 之类的厂商等。你只需用它的真实密钥加一次,然后通过它启用模型。
87
-
88
- ### 最简单:网页界面
89
-
90
- 1. 打开**模型**标签 → **添加模型来源**。
91
- 2. 填名称、后端的 Base URL、它的 API Key。
92
- 3. 勾选它支持的线材格式:`openai`、`anthropic`(或两者都勾)。
93
- 4. 保存,然后点**刷新模型**拉取它的模型列表。
94
- 5. 在对应的格式槽位上**启用**你想要的模型。
95
-
96
- ### 或者:命令行
97
-
98
- ```bash
99
- # 一个 OpenAI 兼容后端。OpenAI 的 Base URL 含版本段
100
- # (例如 /v1,或厂商自己的,如火山 Ark 的 /api/v3)。
101
- myapikey provider add openai-direct \
102
- --base-url-openai https://api.openai.com/v1 \
103
- --key sk-... \
104
- --formats openai
105
-
106
- # 一个 Anthropic 后端。Anthropic 的 Base URL 不含 /v1
107
- # (https://api.anthropic.com,或火山 Ark 的 /api/plan)。
108
- myapikey provider add anthropic-direct \
109
- --base-url-anthropic https://api.anthropic.com \
110
- --key sk-ant-... \
111
- --formats anthropic
112
-
113
- # 看看各自提供哪些模型,然后在某个槽位上启用一个:
114
- myapikey provider models openai-direct
115
- myapikey model enable gpt-4o-mini --format openai --via openai-direct
116
- ```
117
-
118
- 这个模型现在就能通过网关访问了。要为同一个模型加**备用来源**(在第一个失败时顶上),再加一个后端并把它串进链里:
119
-
120
- ```bash
121
- myapikey provider add backup --base-url-openai https://api.openrouter.ai/api/v1 --key sk-or-... --formats openai
122
- myapikey model add-provider gpt-4o-mini backup --format openai
123
- myapikey model prioritize gpt-4o-mini openai-direct backup --format openai # 左边 = 主
124
- myapikey model list # 查看路由表
125
- ```
126
-
127
- > `--format` 选择路由**槽位**:`openai`(对应 `/openai/v1/chat/completions`)、`anthropic`(对应 `/anthropic/v1/messages`)、或 `responses`(对应 `/openai/v1/responses`)。`responses` 槽位只接受你标记了 **supportsResponses** 的后端——在网页界面里勾选那个开关(CLI 暂未暴露它)。
128
-
129
- ---
130
-
131
- ## 连接你的工具
132
-
133
- 一旦启用某个模型,就可以把任意 agent 指向网关。先取出连接信息:
134
-
135
- ```bash
136
- myapikey whoami # 打印 base url + api key + 可直接粘贴的环境变量
137
- ```
138
-
139
- **兼容 OpenAI** 的工具(覆盖 `/chat/completions` *和* `/responses`):
140
-
141
- ```bash
142
- export OPENAI_BASE_URL=http://localhost:7800/openai/v1
143
- export OPENAI_API_KEY=<网关 API Key>
144
- ```
40
+ 首次启动会打印凭据,并同时存到 `~/.myapikey/credentials.txt`:
145
41
 
146
- **Anthropic / Claude Code**:
42
+ 1. 浏览器打开 `http://localhost:7800`,用打印出来的**用户名 / 密码**登录(网页管理用)。
43
+ 2. **模型**页 → **添加后端**:填后端的 base URL + 它的真实 key,勾选它说的格式(`openai` / `anthropic`),然后**刷新模型** → 在对应格式上**启用**你想要的。
44
+ 3. 把工具指过来——所有工具里的 "API key" 字段都填**网关 key**,不是网页登录密码:
147
45
 
148
46
  ```bash
47
+ # Claude Code / Anthropic 系
149
48
  export ANTHROPIC_BASE_URL=http://localhost:7800/anthropic
150
- export ANTHROPIC_API_KEY=<网关 API Key>
151
- ```
152
-
153
- > 不管某个工具把它叫什么 "API key" 字段,都填**网关 API Key**——*不是*你的网页登录密码。登录密码只用于网页界面和 CLI 管理命令。
49
+ export ANTHROPIC_API_KEY=sk-myapikey-…
154
50
 
155
- 不用任何工具,也能快速冒烟测试:
156
-
157
- ```bash
158
- myapikey call gpt-4o-mini "用一句话打个招呼。"
51
+ # OpenAI 兼容工具(Codex、各种 CLI / 插件)
52
+ export OPENAI_BASE_URL=http://localhost:7800/openai/v1
53
+ export OPENAI_API_KEY=sk-myapikey-…
159
54
  ```
160
55
 
161
- **为什么有两个 Base URL?** 网关暴露了两个互相独立的 agent 面——`/openai/v1` `/anthropic/v1`,各自带自己的 `GET /models`(OpenAI 客户端只发现 openai 槽位启用的模型,Anthropic 客户端只发现 anthropic 槽位启用的模型)。两套生态的 SDK 各自拼接自己的路径:OpenAI SDK 指向 `…/openai/v1`(它自己补 `/chat/completions`、`/responses`、`/models`),Anthropic SDK / Claude Code 指向 `…/anthropic`(它自己补 `/v1/messages`、`/v1/models`)。
56
+ > 为什么一个是 `…/openai/v1` 一个是 `…/anthropic`?各生态 SDK 自己补路径(OpenAI `/chat/completions`,Anthropic `/v1/messages`),网关按各自的约定来。两边各有独立的 `/models`,只列出各自格式启用的模型。`myapikey whoami` 随时打印这些可直接粘贴的配置。
162
57
 
163
- **直接用 HTTP**(不走 SDK)——带上网关 API Key(`Bearer`)直接打这两个面之一:
58
+ 4. 不开工具也能冒烟测试:
164
59
 
165
60
  ```bash
166
- # OpenAI
167
- curl http://localhost:7800/openai/v1/chat/completions \
168
- -H "Authorization: Bearer <网关 API Key>" -H "Content-Type: application/json" \
169
- -d '{"model":"<模型名>","messages":[{"role":"user","content":"hi"}]}'
170
-
171
- # Anthropic 系
172
- curl http://localhost:7800/anthropic/v1/messages \
173
- -H "Authorization: Bearer <网关 API Key>" -H "Content-Type: application/json" \
174
- -d '{"model":"<模型名>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
61
+ myapikey call gpt-4o-mini "用一句话介绍你自己"
175
62
  ```
176
63
 
177
- 不管你把工具指向哪个端点,网关都按那个格式转发、绝不翻译——所以确保你调用的每个模型,在对应的槽位上至少有一个来源在服务它([见下文](#路由是怎么工作的))。
178
-
179
- ---
64
+ 喜欢全程命令行?`provider add` → `provider models` → `model enable` 干的是同一件事,速查表在下面。
180
65
 
181
- ## 路由是怎么工作的
66
+ ## 路由是怎么回事(就这么多)
182
67
 
183
- 网关是一个**定向转发器,而非翻译器**。四条规则解释一切:
68
+ - 网关开两个入口:`/openai/v1/*`(chat completions、responses、models)和 `/anthropic/v1/*`(messages、models)。请求从哪个口进,就按哪个口的格式转发,**绝不翻译**。
69
+ - 每个模型在每个格式上有一条独立、有序的后端链。链内按序故障转移:429 / 5xx / 超时切下一条;其他 4xx 原样返回(那是调用方的错);开始流式后不再切换。该格式下没有任何后端能服务这个模型,就返回 `404`——宁可失败得明明白白,也不悄悄翻译。
70
+ - 只要"会说"这两种格式的都能接:OpenAI、Anthropic、OpenRouter、Ollama / vLLM、火山 Ark、公司内部网关……
184
71
 
185
- 1. **端点决定槽位。** `/openai/v1/chat/completions` → **openai** 槽位;`/openai/v1/responses` → **responses** 槽位;`/anthropic/v1/messages` → **anthropic** 槽位。每种都是独立的线材格式,请求体原样转发,不做任何转换。
72
+ ## 为什么不做翻译?
186
73
 
187
- 2. **每个模型有三个互相独立的槽位。** 对同一个模型,你可以分别启用 `openai`、`responses`、`anthropic`,每个槽位都有自己的、按优先级排序的来源链。只在它的后端真正支持的槽位上启用即可。
74
+ one-api / new-api 在 OpenAI ↔ Anthropic 之间做格式转换,方便是方便,但字段会丢、流式会乱,上游出新参数还得等网关更新;OpenRouter 是托管服务,每次调用抽成。MyAPIKey 的答案是干脆没有翻译层:
188
75
 
189
- 3. **来源按槽位资质匹配。** `openai` `anthropic` 槽位接受携带对应线材格式的来源;`responses` 槽位只接受你标记了 `supportsResponses` 的来源(大多数 OpenAI 兼容后端并没有实现 Responses API)。一个 `supportsResponses` 来源可以**同时**位于 **openai** 链 *和* **responses** 链中。
76
+ | | MyAPIKey | one-api / new-api | OpenRouter |
77
+ |---|---|---|---|
78
+ | 部署 | 自托管,`npx` 一行 | 自托管,较重 | 托管服务 |
79
+ | 费用 | 免费 | 免费 | 按次加价 |
80
+ | 格式 | 逐字节透传 | 翻译 | 翻译 |
81
+ | 上游新参数 | 立即可用 | 等网关适配 | 等平台适配 |
82
+ | 供应商 key | 在你自己机器上 | 在你自己机器上 | 在平台手里 |
190
83
 
191
- 4. **槽位内按优先级 + 故障转移。** 候选来源按你的优先级顺序依次尝试。遇到 `429` / `5xx` / 超时,网关就试下一个——并且触发失败的来源会进入短暂的**熔断冷却**(指数退避,30 秒 → 5 分钟),让后续调用先跳过它、等它恢复;成功一次就闭合熔断。其他 `4xx` 错误原样返回(那是调用方的错,不是后端的)。一旦开始流式输出,就不再故障转移。
84
+ 代价也直说:Anthropic 格式的后端没法从 OpenAI 入口调。如果你要的就是格式互转,这个工具暂时不适合你。
192
85
 
193
- **由此带来的取舍:** 如果某个端点没有任何来源能为该模型服务,你会收到 `404`——而不是被悄悄翻译。这是零损耗透传的代价。(将来可以在不改变 agent 端接口的前提下叠加翻译层。)
86
+ ## 怎么管
194
87
 
195
- 这也是它与 OpenRouter、one-api 等聚合器的根本区别——后者会在 OpenAI Anthropic 的请求/响应结构之间互相转换。
88
+ **Web 界面** `http://localhost:7800`,中英双语。五个页:**使用方式**(可直接粘贴的连接配置)、**模型**(后端、发现、按格式启用、拖拽排序、名称映射、测试调用)、**最近调用**(实时时间线)、**统计**(调用数 / 成功率 / p50 / p95,按模型 / 来源 / 日期)、**设置**(轮换 key、改密码、熔断状态、存储位置)。
196
89
 
197
- ---
90
+ **CLI 速查**(和网页同一套 admin API,两边永远一致):
198
91
 
199
- ## 模型名映射
200
-
201
- 有时候,工具请求的模型名并不是后端期望的名字。MyAPIKey 可以**按来源改写模型名**,在转发过程中替换——依然是纯透传,不做格式翻译。
202
-
203
- 在网页界面(**模型**标签)设置:添加来源时有一个**上游模型**字段;也可以在链中任意成员上就地编辑。(CLI 暂未暴露。)
204
-
205
- 它的用途:
206
-
207
- - **把友好名别名到带版本的 id**——对工具暴露 `claude-sonnet-4`,而网关向上游发送 `claude-sonnet-4-20250514`。
208
- - **让同一个公开名在不同来源指向不同真实模型**——例如你的 `gpt-4` 槽位,主来源发 `gpt-4o`,备用来源发 `gpt-4-turbo`。
209
- - **桥接两个后端的命名差异**——同一个模型在不同后端用不同 id。
210
-
211
- 若某来源未设映射,则原样发送公开模型名。
212
-
213
- ---
214
-
215
- ## 网页界面
216
-
217
- 浏览器打开 `http://localhost:7800`,用用户名 / 密码登录。内置**中英文**,顶栏可切换。五个标签:
218
-
219
- - **使用方式** — 可直接复制粘贴的 Base URL / API Key / 登录信息,以及现成的 SDK 环境变量。
220
- - **模型** — 完整增删改查:添加来源、按槽位发现并启用模型、拖拽排序优先级、切换每个槽位、设置[模型名映射](#模型名映射),还能对任意「模型 + 槽位」跑一次端到端**测试**。
221
- - **最近调用** — 实时的近期调用时间线(延迟、状态、来源、格式),熔断冷却事件会和触发它的失败记录并排显示。
222
- - **统计** — 按模型 / 来源 / 格式 / 日期汇总调用次数、成功率、延迟(p50、p95),可选时间范围(24 小时 / 7 天 / 30 天 / 90 天 / 全部)。
223
- - **设置** — 轮换 API Key、修改账号密码、查看 / 重置熔断状态、查看 `data.json` 和 `logs.jsonl` 的位置。
224
-
225
- 它和 CLI 用的是同一套 admin API——想用哪种配置都行。
92
+ | 我想…… | 命令 |
93
+ |---|---|
94
+ | 起网关 | `myapikey serve [--port 7800] [--data-dir <目录>]` |
95
+ | 打印要粘给工具的配置 | `myapikey whoami` |
96
+ | 加后端 | `myapikey provider add <名> --base-url-openai https://api.openai.com/v1 --key sk-… --formats openai` |
97
+ | 看后端有哪些模型 | `myapikey provider models <名>` |
98
+ | 启用模型 | `myapikey model enable <模型> --format openai --via <后端>` |
99
+ | 加备用 + 排序 | `myapikey model add-provider <模型> <后端> --format openai` · `myapikey model prioritize <模型> <主> <备> --format openai` |
100
+ | 看路由表 | `myapikey model list` |
101
+ | 给模型设匀速限速 | `myapikey model pace <模型> <rpm\|0>` |
102
+ | 钉死槽位 thinking 档位 | `myapikey model thinking <模型> <链上位次> [档位] --format <格式>` |
103
+ | 快速试一把 | `myapikey call <模型> "你好"` |
226
104
 
227
- ---
105
+ > OpenAI 的 base URL **含**版本段(`/v1`、火山 Ark 的 `/api/v3`);Anthropic 的 base URL **不含**(`https://api.anthropic.com`)。`responses` 格式只接受你标记了 `supportsResponses` 的后端(网页里勾选)。
106
+ > CLI 全局参数:`-u/--url`、`--user`、`--pass`、`--api-key`,或环境变量 `MYAPIKEY_URL` / `MYAPIKEY_USER` / `MYAPIKEY_PASS` / `MYAPIKEY_API_KEY`。
228
107
 
229
- ## 命令行参考
108
+ ## 数据都在哪
230
109
 
231
- | 命令 | |
232
- |---|---|
233
- | `serve [--port 7800] [--data-dir <目录>] [--web-dir <路径>]` | 运行网关 |
234
- | `whoami` | 打印给 agent 用的连接信息(base url、api key、登录信息、示例环境变量) |
235
- | `provider add <名称> --base-url-openai URL [--base-url-anthropic URL] --key KEY --formats openai,anthropic` | 添加后端(`supportsResponses` 在网页界面设置) |
236
- | `provider list` | 列出后端 |
237
- | `provider models <引用>` | 发现某个后端提供的模型 |
238
- | `provider remove <引用>` | 移除后端(用 id 或名称) |
239
- | `model enable <名称> --format <格式> [--via <引用>]` | 在某个槽位启用模型(openai / anthropic / responses) |
240
- | `model disable <名称> --format <格式>` | 禁用某个槽位(保留配置) |
241
- | `model list` | 查看按槽位的路由表 |
242
- | `model add-provider <名称> <引用> --format <格式>` | 为某个槽位添加备用来源 |
243
- | `model remove-provider <名称> <引用> --format <格式>` | 从某个槽位移除来源 |
244
- | `model prioritize <名称> <引用>... --format <格式>` | 设置某个槽位的来源顺序(左 = 主) |
245
- | `model remove <名称>` | 完全移除一个模型(所有槽位) |
246
- | `call <模型> [提示词...]` | 通过网关快速测试(走 OpenAI 路径) |
247
-
248
- `<引用>` 是来源的 id 或名称。全局参数:`-u/--url`、`--user`、`--pass`、`--api-key`(或 `MYAPIKEY_URL` / `MYAPIKEY_USER` / `MYAPIKEY_PASS` / `MYAPIKEY_API_KEY` 环境变量)。
249
-
250
- ---
251
-
252
- ## 接口一览
253
-
254
- **面向 agent(两个面,共用一把 API Key —— `Authorization: Bearer` 或 `x-api-key`):**
255
-
256
- - `POST /openai/v1/chat/completions` — OpenAI 格式代理
257
- - `POST /openai/v1/responses` — OpenAI Responses API(仅限标记了 `supportsResponses` 的来源)
258
- - `GET /openai/v1/models` — 启用在 **openai** 槽位的模型,OpenAI 列表格式(公开——无需 Key)
259
- - `POST /anthropic/v1/messages` — Anthropic 格式代理
260
- - `GET /anthropic/v1/models` — 启用在 **anthropic** 槽位的模型,Anthropic 列表格式(公开——无需 Key)
261
- - `GET /health` — 公开存活检查
262
-
263
- **管理(`/admin`,账号密码 —— HTTP Basic):**
264
-
265
- - `/admin/account` `GET` / `PUT` — 读取或修改用户名 / 密码(任一字段可选)
266
- - `/admin/api-key` `GET`、`/admin/api-key/rotate` `POST` — agent 用的那把密钥
267
- - `/admin/connection` `GET` — 探测到的局域网 IP(用于把其他机器指向网关)
268
- - `/admin/providers`、`/admin/providers/:id` `POST`/`PUT`/`DELETE`、`/admin/providers/:id/discover` `POST` — 后端增删改查 + 模型发现
269
- - `/admin/models`、`/admin/models/:name/{providers,priority,map,disable,test}` — 路由表增删改查、[按来源的模型名映射](#模型名映射)、端到端测试
270
- - `/admin/logs` `GET` — 近期调用(落盘日志的尾部)
271
- - `/admin/stats?range=24h|7d|30d|90d|all` `GET` — 基于保留历史的聚合统计
272
- - `/admin/storage` `GET` — 查看 `data.json` 和 `logs.jsonl` 的位置
273
- - `/admin/circuit` `GET`、`/admin/circuit/:id/reset` `POST` — 熔断快照 + 手动重置
274
-
275
- ---
276
-
277
- ## 存储说明
278
-
279
- 所有东西都放在一个数据目录里(默认 `~/.myapikey`):
110
+ 一个目录(默认 `~/.myapikey`,可用 `--data-dir` 或 `MYAPIKEY_DATA_DIR` 改):
280
111
 
281
112
  | 文件 | 内容 |
282
113
  |---|---|
283
- | `data.json` | 全部配置 —— 后端、按模型的路由表、账号、API Key |
284
- | `logs.jsonl` | 调用历史,每行一个 JSON 对象(保留约 90 天 / 100 万行) |
285
- | `credentials.txt` | 人可读的网页登录信息 + API Key,每次启动重新生成 |
286
- | `client.json` | CLI 客户端配置(base url + 登录信息 + API Key) |
114
+ | `data.json` | 全部配置:后端、路由表、账号、API key |
115
+ | `logs.jsonl` | 调用历史(保留约 90 天 / 100 万行) |
116
+ | `credentials.txt` | 明文的登录信息 + API key,每次启动重写 |
117
+ | `client.json` | CLI 自己存的连接配置 |
287
118
 
288
- `--data-dir <路径>` 或 `MYAPIKEY_DATA_DIR` 覆盖该目录。调用日志从不放进内存:「最近调用」时间线是尾部读取最新的约 200 条,统计则是按需流式扫描整个保留文件。
289
-
290
- ---
291
-
292
- ## 开发指南
119
+ ## 开发
293
120
 
294
121
  ```bash
295
122
  npm install
296
- npm run build:web # 把 Vue 界面构建到 packages/web/dist(首次,以及每次改完界面后)
297
- npm start # tsx ... serve(网关在 :7800)
123
+ npm run build:web # 把 Vue 界面构建到 packages/web/dist
298
124
  npm run dev # 网关,带 watch 热重载
299
- npm run dev:web # vite 开发服务器( /openai、/anthropic、/admin 代理到 :7800)
300
- npm run typecheck # tsc(core)+ vue-tsc(web)
125
+ npm run dev:web # vite 开发服务器(API 代理到 :7800)
126
+ npm test # vitest 单测 + 集成
127
+ npm run test:e2e # playwright 打真实网关进程
128
+ npm run typecheck # tsc + vue-tsc
301
129
  ```
302
130
 
303
- 手动测试网关:起一个 mock 上游,然后在临时端口上用 `serve --data-dir /tmp/myapikey` 跑。
304
-
305
- ---
306
-
307
- ## 说明与非目标
131
+ ## 丑话说在前面
308
132
 
309
- - **不做 OpenAI ↔ Anthropic 翻译。** 如果某个后端不会说 agent 使用的格式,那次调用就会失败(`404`)。将来可以在不改变 agent 端接口的前提下叠加。
310
- - **没有数据库。** 配置放一个 `data.json`,保留的调用历史放 `logs.jsonl`。
311
- - **没有 TLS。** 面向局域网 / 家用服务器,在你自己的网络里用。若要暴露到外网,请放到带 TLS 的反向代理后面。
312
- - **登录密码和 API Key 是分开的两把。** API Key 是 agent 用的;若某个工具的配置泄露了,就轮换它(`POST /admin/api-key/rotate` 或网页界面)。两把都不要交给不可信的代码路径。
133
+ - **不做 OpenAI ↔ Anthropic 翻译。** 入口和模型格式对不上 `404`。
134
+ - **单用户、无 TLS。** 给你自己的内网用的;要暴露到外网请自己套反代。
135
+ - **网页密码 ≠ API key。** 密码只管 UI/CLI,agent 只认 `sk-myapikey-…`。工具配置泄露了,去设置页轮换 key,其他什么都不用动。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myapikey",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "type": "module",
5
5
  "description": "Personal LLM API gateway & proxy — one address + one API key for all your models. Forwards OpenAI & Anthropic calls to your backends with failover and a circuit breaker. Pure passthrough, no format translation. Self-hosted (CLI + web UI).",
6
6
  "keywords": [
@@ -70,10 +70,19 @@ type UsageFields = Partial<Pick<Usage, "input" | "output" | "cacheRead" | "cache
70
70
  * `message_delta.usage.output_tokens` (output); a non-streaming Message
71
71
  * carries `usage` directly (both).
72
72
  * - openai chat: only the final chunk (with stream_options.include_usage) has
73
- * `usage` (prompt/completion tokens); the non-streaming body has it too.
73
+ * `usage` (prompt/completion tokens + `prompt_tokens_details.cached_tokens`
74
+ * / DeepSeek's `prompt_cache_hit_tokens` for cache hits); the non-streaming
75
+ * body has it too.
74
76
  * - responses: `response.usage` on response.completed/in-progress, or top-level
75
77
  * `usage` on a non-streaming Response. Accepts both the `*_tokens` and bare
76
- * `input`/`output` spellings the API has used over time. */
78
+ * `input`/`output` spellings the API has used over time, plus
79
+ * `input_tokens_details.cached_tokens` for cache hits.
80
+ *
81
+ * Cache-hit fields (`cacheRead` etc.) are captured on every wire that reports
82
+ * them. On the OpenAI-family wires the reported `input`/`prompt_tokens`
83
+ * INCLUDES the cached subset, so it is subtracted here — `Usage.input` means
84
+ * UNCACHED prompt tokens on every wire (Anthropic's native semantics), which
85
+ * is what the store's cache-hit-rate formula assumes. */
77
86
  function extractUsage(obj: any, key: RouteKey): UsageFields | null {
78
87
  if (!obj || typeof obj !== "object") return null;
79
88
 
@@ -94,13 +103,31 @@ function extractUsage(obj: any, key: RouteKey): UsageFields | null {
94
103
  if (u) {
95
104
  const input = num(u.input_tokens) ?? num(u.input);
96
105
  const output = num(u.output_tokens) ?? num(u.output);
97
- if (typeof input === "number" || typeof output === "number") return { input, output };
106
+ // OpenAI Responses reports cache hits inside `input_tokens_details` a
107
+ // SUBSET of input_tokens (like chat/completions, unlike Anthropic where
108
+ // input_tokens excludes cache), so subtract to keep Usage.input = uncached.
109
+ const details = u.input_tokens_details as { cached_tokens?: unknown } | undefined;
110
+ const cached = num(details?.cached_tokens) ?? 0;
111
+ if (typeof input === "number" || typeof output === "number") {
112
+ return { input: input === undefined ? undefined : input - cached, output, cacheRead: cached > 0 ? cached : undefined };
113
+ }
98
114
  }
99
115
  return null;
100
116
  }
101
- // openai /chat/completions
117
+ // openai /chat/completions (also its OpenAI-compatible clones)
102
118
  if (obj.usage) {
103
- return { input: num(obj.usage.prompt_tokens), output: num(obj.usage.completion_tokens) };
119
+ const input = num(obj.usage.prompt_tokens);
120
+ const output = num(obj.usage.completion_tokens);
121
+ // Prompt-cache hits on this wire: `prompt_tokens_details.cached_tokens`
122
+ // (OpenAI, Ark, Qwen, GLM, …) or DeepSeek's `prompt_cache_hit_tokens`.
123
+ // Both spell the cached tokens as a SUBSET of prompt_tokens (cached +
124
+ // uncached = prompt), so — unlike Anthropic's separate counters — the hit
125
+ // part is subtracted from input. That keeps Usage.input = UNCACHED prompt
126
+ // tokens on every wire, which is what the stats hit-rate formula
127
+ // (cacheRead / (input + cacheRead + cacheCreation)) assumes.
128
+ const details = obj.usage.prompt_tokens_details as { cached_tokens?: unknown } | undefined;
129
+ const cached = num(details?.cached_tokens) ?? num(obj.usage.prompt_cache_hit_tokens) ?? 0;
130
+ return { input: input === undefined ? undefined : input - cached, output, cacheRead: cached > 0 ? cached : undefined };
104
131
  }
105
132
  return null;
106
133
  }
@@ -118,8 +118,13 @@ export interface GateConfig {
118
118
  * streams where the upstream omits usage (most agents don't set
119
119
  * stream_options.include_usage), `estimated` is set and input/output come from
120
120
  * a local tokenizer approximation (gpt-tokenizer, o200k_base) instead — the UI
121
- * renders those with a ≈ marker. cacheRead/cacheCreation (prompt-caching hits,
122
- * Anthropic-only) are surfaced separately from `input`. */
121
+ * renders those with a ≈ marker. cacheRead/cacheCreation (prompt-cache hit /
122
+ * write tokens) are surfaced separately from `input`, which always means
123
+ * UNCACHED prompt tokens: Anthropic reports them that way natively, while on
124
+ * the OpenAI-family wires the cached subset (chat `prompt_tokens_details.
125
+ * cached_tokens`, DeepSeek `prompt_cache_hit_tokens`, /responses
126
+ * `input_tokens_details.cached_tokens`) arrives INCLUDED in prompt_tokens /
127
+ * input_tokens and is subtracted at capture time to match. */
123
128
  export interface Usage {
124
129
  input: number;
125
130
  output: number;