ghc-proxy 0.10.2 → 0.10.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
@@ -110,6 +110,7 @@ ghc-proxy uses a subcommand structure:
110
110
  ```bash
111
111
  bunx --bun ghc-proxy@latest start # Start the proxy server
112
112
  bunx --bun ghc-proxy@latest auth # Run GitHub auth flow without starting the server
113
+ bunx --bun ghc-proxy@latest auth --account work # Create or replace a named account
113
114
  bunx --bun ghc-proxy@latest check-usage # Show your Copilot usage/quota in the terminal
114
115
  bunx --bun ghc-proxy@latest debug # Print diagnostic info (version, paths, token status)
115
116
  bunx --bun ghc-proxy@latest selfcheck # Probe tokenizer chunks and Bun/Node runtime contracts in the packaged bundle
@@ -191,17 +192,114 @@ The proxy normalizes and persists the GHE domain automatically after a successfu
191
192
 
192
193
  ## Configuration
193
194
 
194
- The proxy reads an optional JSON config file at:
195
+ The proxy reads an optional JSON config file at:
195
196
 
196
197
  ```
197
- ~/.local/share/ghc-proxy/config.json
198
- ```
199
-
200
- All fields are optional. The full schema:
198
+ ~/.local/share/ghc-proxy/config.json
199
+ ```
200
+
201
+ GitHub credentials are stored separately at
202
+ `~/.local/share/ghc-proxy/credentials.json`. The credential file is versioned,
203
+ selects one active account for legacy single-account mode, and can retain
204
+ multiple named accounts:
205
+
206
+ ```json
207
+ {
208
+ "version": 1,
209
+ "activeAccount": "default",
210
+ "accounts": {
211
+ "default": {
212
+ "githubToken": "<base64>",
213
+ "gheDomain": "company.ghe.com"
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ Base64 is low-cost obfuscation, not encryption; anyone who can read the file can
220
+ decode it. On first startup after upgrading, a legacy `config.json` token is
221
+ copied into this store only after a complete temporary config backup is created.
222
+ The legacy field and backup are removed after the stored credential succeeds at
223
+ both GitHub identity validation and Copilot token acquisition. A failed or
224
+ interrupted migration keeps the backup and reports its recovery path.
225
+
226
+ ### Named Account Routing
227
+
228
+ Authenticate each account under a stable name:
229
+
230
+ ```bash
231
+ bunx --bun ghc-proxy@latest auth --account default
232
+ bunx --bun ghc-proxy@latest auth --account account1
233
+
234
+ # A named GHE.com account keeps its own tenant beside its credential.
235
+ bunx --bun ghc-proxy@latest auth --account work --ghe-domain company.ghe.com
236
+ ```
237
+
238
+ Account names are case-sensitive, 1-64 characters, and may contain ASCII
239
+ letters, numbers, `.`, `_`, and `-`; the first character must be alphanumeric.
240
+
241
+ For an existing single-account installation, open the local Dashboard Accounts
242
+ view. The current legacy account remains the default, and the bootstrap form
243
+ suggests `defaultaccount.localhost` as its dedicated hostname. Edit that value
244
+ if needed, then enable routing. The change is persisted transactionally and
245
+ takes effect without restarting the process.
246
+
247
+ You can also configure hostname routing directly in `config.json`:
248
+
249
+ ```json
250
+ {
251
+ "accountRouting": {
252
+ "baseHostname": "localhost",
253
+ "defaultAccount": "default",
254
+ "hostnames": {
255
+ "default.localhost": "default",
256
+ "account1.localhost": "account1"
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ With that configuration, `http://localhost:4141`, `http://127.0.0.1:4141`, and
263
+ the stable dedicated hostname `http://default.localhost:4141` use `default`, while
264
+ `http://account1.localhost:4141` uses `account1`. Every routed account must have
265
+ exactly one dedicated hostname. The base hostname is an additional alias for
266
+ the currently selected default; `127.0.0.1` is a fixed loopback alias for that
267
+ same default. Switching the default never changes any dedicated hostname. DNS
268
+ hostname matching is case-insensitive, ignores the request port, and accepts a
269
+ trailing root dot. Any other hostname is rejected with HTTP `421` before a route handler or
270
+ upstream request runs. `Forwarded` and `X-Forwarded-Host` are not trusted for
271
+ account selection.
272
+
273
+ Each account has independent GitHub/Copilot tokens, refresh scheduling, model
274
+ cache, Responses emulator state, local rate limiter, and upstream queue/cooldown
275
+ state. A failure or capacity response on one account never switches, falls back,
276
+ rotates, or load-balances the request to another account. Process-wide policy
277
+ configuration remains shared.
278
+
279
+ `accountRouting` is opt-in so existing single-account installations retain their
280
+ current Host behavior until the local Dashboard bootstrap is explicitly
281
+ confirmed. The active legacy credential becomes the explicit default account;
282
+ the suggested `defaultaccount.localhost` hostname is editable before commit. In
283
+ routing mode every referenced account must already exist, and `defaultAccount`
284
+ must be explicit. Invalid or ambiguous routing config fails startup. Dashboard
285
+ bootstrap is unavailable while process-wide `start --github-token` or
286
+ `start --ghe-domain` overrides are active, and those overrides are rejected once
287
+ routing is enabled because they cannot identify one named account. Hostname
288
+ routing is a deterministic selector, not an authorization boundary: callers
289
+ that can reach the listener and choose a configured `Host` can select that
290
+ account unless access is enforced separately.
291
+
292
+ For Docker deployments, leave `GH_TOKEN` unset in routing mode and mount the
293
+ populated credential/config directory. The image healthcheck reads
294
+ `accountRouting.baseHostname`, applies the same DNS ASCII/case/root-dot
295
+ normalization, and sends it as `Host` while connecting over loopback, so health
296
+ checks continue to exercise the configured base hostname rather than the
297
+ `127.0.0.1` default alias.
298
+
299
+ All fields are optional. The full schema:
201
300
 
202
301
  | Field | Type | Default | Description |
203
302
  |-------|------|---------|-------------|
204
- | `githubToken` | `string` | unset | Persisted GitHub token. The device-code flow (`auth` or first startup) writes it automatically; `start --github-token` is runtime-only and does not write this field |
205
303
  | `modelRewrites` | `{ from, to }[]` | `[]` | Glob-pattern model substitution rules (see [Model Rewrites](#model-rewrites)) |
206
304
  | `modelFallback` | `object` | built-in family defaults | Override default model fallbacks (see [Customizing Fallbacks](#customizing-fallbacks)) |
207
305
  | `modelFallback.claudeOpus` | `string` | `claude-opus-5` | Fallback for `claude-opus-*` models |
@@ -224,8 +322,9 @@ All fields are optional. The full schema:
224
322
  | `upstreamRecoveryBudgetSeconds` | `number` | `60` | Shared recovery deadline after the first retryable outcome or active-cooldown encounter (`1..120` seconds) |
225
323
  | `overloadFallbacks` | `Record<string, string>` | `{}` (disabled) | Exact effective-model mappings for one opt-in fallback dispatch after terminal model `529` |
226
324
  | `upstreamQueueBaseDelaySeconds` | `number` | `2` | Base delay (seconds) for upstream retry backoff when `Retry-After` is absent |
227
- | `upstreamQueueMaxDelaySeconds` | `number` | `60` | Maximum computed backoff (seconds); does not clamp `Retry-After` |
325
+ | `upstreamQueueMaxDelaySeconds` | `number` | `60` | Maximum computed backoff (seconds); does not clamp `Retry-After` |
228
326
  | `gheDomain` | `string` | unset | GitHub Enterprise Cloud company domain (persisted automatically after GHE.com auth) |
327
+ | `accountRouting` | `{ baseHostname, defaultAccount, hostnames }` | unset | Opt-in exact DNS hostname to named-account routing; unknown hostnames are rejected |
229
328
 
230
329
  Example:
231
330
 
@@ -375,7 +474,7 @@ When the Copilot token response includes `endpoints.api`, `ghc-proxy` now prefer
375
474
 
376
475
  Incoming requests hit an [Elysia](https://elysiajs.com/) server. `chat/completions` requests are validated, normalized into the shared planning pipeline, and then forwarded to Copilot. `responses` requests use a native Responses path with explicit compatibility policies. `messages` requests are routed per-model and can use native Anthropic passthrough, the Responses translation path, or the existing chat-completions fallback. The translator tracks exact vs lossy vs unsupported behavior explicitly; see the [Messages Routing and Translation Guide](./docs/messages-routing-and-translation.md) and the [Anthropic Translation Matrix](./docs/anthropic-translation-matrix.md) for the current support surface.
377
476
 
378
- The built-in, read-only Dashboard projects process health, model routing, behavior, and recent request lifecycle metadata without storing request or response content. See [Dashboard Observability](./docs/design/dashboard-observability.md).
477
+ The built-in Dashboard projects process health, named account status, model routing, behavior, and recent request lifecycle metadata without storing request or response content. Its protected Accounts view can explicitly bootstrap a legacy account into named routing, authenticate a new account with a dedicated hostname, and switch the default account. See [Dashboard Observability](./docs/design/dashboard-observability.md).
379
478
 
380
479
  For Anthropic `search_result` blocks, an April 17, 2026 probe against `claude-opus-4.6` on Copilot native `/v1/messages` accepted top-level search results and pure search-result tool outputs, but rejected top-level `citations` and mixed text/search-result tool output arrays. The native path sanitizes those observed rejection cases, while translated paths flatten search results to text; re-run the probe before treating that dated upstream result as universal.
381
480
 
@@ -426,7 +525,7 @@ This keeps the existing chat pipeline stable while allowing newer Copilot models
426
525
  | `GET` | `/usage` | Copilot quota / usage monitoring |
427
526
  | `GET` | `/token` | Inspect the current Copilot token |
428
527
 
429
- **Local Dashboard (read-only):**
528
+ **Local Dashboard:**
430
529
 
431
530
  | Method | Path | Description |
432
531
  |--------|------|-------------|
@@ -437,8 +536,13 @@ This keeps the existing chat pipeline stable while allowing newer Copilot models
437
536
  | `GET` | `/dashboard/api/models` | Upstream model metadata and effective proxy capabilities |
438
537
  | `GET` | `/dashboard/api/behavior` | Active routing, compatibility policies, strategies, and effect counters |
439
538
  | `GET` | `/dashboard/api/requests` | Active requests and the most recent 256 completed request summaries |
539
+ | `GET` | `/dashboard/api/accounts` | Safe per-account identity, tenant, authentication, Copilot status, quota, hostname, and default marker |
540
+ | `POST` | `/dashboard/api/accounts/bootstrap` | Persist and enable named routing for the current legacy default account |
541
+ | `POST` | `/dashboard/api/accounts` | Start device authentication for a new named account and its dedicated hostname |
542
+ | `GET` | `/dashboard/api/account-auth/:id` | Read the safe state of an in-progress account authentication |
543
+ | `POST` | `/dashboard/api/accounts/default` | Persist and activate an explicit default account |
440
544
 
441
- Dashboard routes are restricted to local access and return `403` when the peer, request host, or supplied `Origin` fails the loopback/same-origin checks. They are excluded from request history and access logging. See [Dashboard Observability](./docs/design/dashboard-observability.md) for the projection and security contract.
545
+ Dashboard routes are restricted to local access and return `403` when the peer, request host, or supplied `Origin` fails the loopback/same-origin checks. A normal legacy single-account process exposes an explicit bootstrap action; adding accounts and changing the default remain unavailable until bootstrap succeeds. Processes using `start --github-token` or `start --ghe-domain` retain legacy behavior and return `409` for account management because those overrides cannot be assigned safely. Dashboard requests are excluded from request history and access logging. See [Dashboard Observability](./docs/design/dashboard-observability.md) for the projection, transaction, and security contract.
442
546
 
443
547
  > **Note:** The `/v1/` prefix is optional for OpenAI-compatible endpoints (`/chat/completions`, `/responses` and its resource routes, `/models`, `/embeddings`). Anthropic endpoints (`/v1/messages`, `/v1/messages/count_tokens`) require the `/v1` prefix. The utility and Dashboard endpoints are root-only and not exposed under `/v1`.
444
548
 
@@ -568,6 +672,9 @@ volumes:
568
672
  ## Running from Source
569
673
 
570
674
  Repository development uses Bun >= 1.4 even if you run the published package with Node.js.
675
+ Install dev dependencies when building from source: `gpt-tokenizer` is a
676
+ build-time dependency whose five encodings are still bundled into the published
677
+ runtime for the local count endpoints and packaged `selfcheck`.
571
678
 
572
679
  ```bash
573
680
  git clone https://github.com/wxxb789/ghc-proxy.git
@@ -580,8 +687,8 @@ bun run start
580
687
 
581
688
  ## Development
582
689
 
583
- ```bash
584
- bun install # Install dependencies
690
+ ```bash
691
+ bun install # Install dependencies, including build-time tokenizer sources
585
692
  bun run dev # Start with --watch
586
693
  bun run start # Start without --watch
587
694
  bun run build # Build with tsdown
@@ -0,0 +1,27 @@
1
+ # Third-Party Notices
2
+
3
+ ghc-proxy bundles portions of `gpt-tokenizer` in its published JavaScript.
4
+
5
+ ## gpt-tokenizer
6
+
7
+ MIT License
8
+
9
+ Copyright (c) 2023-2024 Bazyli Brzoska
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.