@openhoo/hoopilot 0.9.3 → 1.0.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 CHANGED
@@ -1,34 +1,81 @@
1
- # hoopilot
1
+ # Hoopilot
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40openhoo%2Fhoopilot?label=npm)](https://www.npmjs.com/package/@openhoo/hoopilot)
4
4
  [![CI](https://github.com/openhoo/hoopilot/actions/workflows/ci.yml/badge.svg)](https://github.com/openhoo/hoopilot/actions/workflows/ci.yml)
5
5
 
6
- OpenAI- and Anthropic-compatible local proxy for GitHub Copilot accounts. It runs on Bun and exposes OpenAI-style `/v1/chat/completions`, `/v1/responses`, `/v1/completions`, and `/v1/models` routes plus Claude Code-compatible `/v1/messages` and `/v1/messages/count_tokens` routes.
6
+ Hoopilot is a local OpenAI- and Anthropic-compatible proxy for GitHub Copilot accounts. It runs on Bun and exposes OpenAI-style `/v1/chat/completions`, `/v1/responses`, `/v1/completions`, and `/v1/models` routes, plus Claude Code-compatible `/v1/messages` and `/v1/messages/count_tokens` routes.
7
7
 
8
- This project uses GitHub Copilot's service endpoints and is not an official GitHub product. The upstream API can change without notice. Use it only with accounts and usage patterns you are allowed to use.
8
+ This project uses GitHub Copilot service endpoints and is not an official GitHub product. Upstream behavior can change without notice. Use Hoopilot only with accounts and usage patterns you are allowed to use.
9
+
10
+ ## Highlights
11
+
12
+ - Browser-based GitHub Copilot OAuth login with a local credential store.
13
+ - OpenAI-compatible Chat Completions, Responses, legacy Completions, and model-list routes.
14
+ - Anthropic Messages compatibility for Claude Code and other Anthropic-style clients.
15
+ - Bundled `codexx` launcher that runs Codex against a local Hoopilot server with the right Responses API provider settings.
16
+ - Local API-key gate, loopback-safe defaults, structured logs, Prometheus metrics, and Copilot quota reporting.
17
+ - npm package, standalone binaries, Docker image, and self-update support for release binaries.
18
+
19
+ ## Quick start
20
+
21
+ Sign in once, then start the proxy on localhost:
22
+
23
+ ```sh
24
+ npx @openhoo/hoopilot login
25
+ npx @openhoo/hoopilot
26
+ ```
27
+
28
+ By default the server listens on `127.0.0.1:4141` and accepts local requests without authentication, so any placeholder works as the client key:
29
+
30
+ ```sh
31
+ export OPENAI_BASE_URL=http://127.0.0.1:4141/v1
32
+ export OPENAI_API_KEY=hoopilot
33
+ ```
34
+
35
+ PowerShell:
36
+
37
+ ```powershell
38
+ $env:OPENAI_BASE_URL = "http://127.0.0.1:4141/v1"
39
+ $env:OPENAI_API_KEY = "hoopilot"
40
+ ```
41
+
42
+ To require clients to authenticate — recommended whenever you expose the proxy beyond localhost — set `HOOPILOT_API_KEY` to a strong, unique secret and send that value as the client key:
43
+
44
+ ```sh
45
+ export HOOPILOT_API_KEY=$(openssl rand -hex 24)
46
+ npx @openhoo/hoopilot
47
+ ```
48
+
49
+ Run Codex through Hoopilot after the server is running:
50
+
51
+ ```sh
52
+ npx --package @openhoo/hoopilot codexx
53
+ ```
9
54
 
10
55
  ## Install
11
56
 
12
57
  ### npm
13
58
 
14
- ```powershell
59
+ Run without installing:
60
+
61
+ ```sh
15
62
  npx @openhoo/hoopilot
16
63
  ```
17
64
 
18
- Or install it globally:
65
+ Or install the package globally:
19
66
 
20
- ```powershell
67
+ ```sh
21
68
  npm install -g @openhoo/hoopilot
22
69
  bun add -g @openhoo/hoopilot
23
70
  ```
24
71
 
25
- ### Standalone Binary
72
+ ### Standalone binary
26
73
 
27
- When the npm registry is unreachable but GitHub is reachable, install a prebuilt self-contained binary from the latest GitHub release. No Node.js or Bun runtime is needed to run it.
74
+ When npm is unavailable but GitHub releases are reachable, install a prebuilt self-contained binary. Node.js and Bun are not required to run the binary.
28
75
 
29
- Linux / macOS from PowerShell:
76
+ Linux/macOS:
30
77
 
31
- ```powershell
78
+ ```sh
32
79
  curl -fsSL https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.sh | sh
33
80
  ```
34
81
 
@@ -38,9 +85,9 @@ Windows PowerShell:
38
85
  irm https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.ps1 | iex
39
86
  ```
40
87
 
41
- The installer detects your OS, CPU architecture, and libc, downloads the matching binary, verifies its SHA-256 checksum, and installs it to `~/.local/bin` on Linux/macOS or `%LOCALAPPDATA%\Programs\hoopilot` on Windows. Override the location with `HOOPILOT_INSTALL_DIR`, or pin a version:
88
+ The installer detects your OS, CPU architecture, and libc, downloads the matching binary, verifies its SHA-256 checksum, and installs it to `~/.local/bin` on Linux/macOS or `%LOCALAPPDATA%\Programs\hoopilot` on Windows. Override the install directory with `HOOPILOT_INSTALL_DIR`, or pin a version:
42
89
 
43
- ```powershell
90
+ ```sh
44
91
  curl -fsSL https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.sh | sh -s -- --version <version> --dir ~/bin
45
92
  ```
46
93
 
@@ -58,16 +105,23 @@ Run Hoopilot as a long-lived service from the published multi-arch image on the
58
105
  # 1. Sign in once; the OAuth credential is written to the persisted /data volume.
59
106
  docker run --rm -it -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login
60
107
 
61
- # 2. Run the proxy on localhost.
108
+ # 2. Run the proxy on localhost with a strong, unique API key.
109
+ export HOOPILOT_API_KEY=$(openssl rand -hex 24)
62
110
  docker run -d --name hoopilot --restart unless-stopped \
63
111
  -p 127.0.0.1:4141:4141 \
112
+ -e HOOPILOT_API_KEY \
64
113
  -v hoopilot-data:/data ghcr.io/openhoo/hoopilot
65
114
  ```
66
115
 
67
- Tags follow the release version (e.g. `ghcr.io/openhoo/hoopilot:0.8`, `:0.8.3`) plus `:latest`. The image listens on `0.0.0.0:4141`, runs as a non-root user, and stores its OAuth credential at `/data/auth.json` (override with `HOOPILOT_AUTH_FILE`). The Docker image allows unauthenticated local clients by default; set `HOOPILOT_API_KEY` if you publish the port beyond localhost. A `docker-compose.yml` is provided in the repository:
116
+ Tags follow the release version, for example `ghcr.io/openhoo/hoopilot:0.10`, `:0.10.0`, and `:latest`. The image listens on `0.0.0.0:4141` (required so Docker port publishing can reach it), runs as a non-root user, and stores its OAuth credential at `/data/auth.json` by default. Override that path with `HOOPILOT_AUTH_FILE`.
117
+
118
+ Because it binds a non-loopback interface, the image fails closed: it refuses to start unless you set `HOOPILOT_API_KEY` to a strong, unique secret (well-known demo keys are rejected). Clients then send that key as `Authorization: Bearer <key>` or `x-api-key: <key>`. To intentionally run without authentication — for example behind your own authenticating proxy — set `HOOPILOT_ALLOW_UNAUTHENTICATED=1`.
119
+
120
+ A `docker-compose.yml` is provided. Set `HOOPILOT_API_KEY` first; compose passes it through to the container:
68
121
 
69
122
  ```sh
70
- docker compose run --rm hoopilot login # one-time GitHub OAuth
123
+ docker compose run --rm hoopilot login
124
+ export HOOPILOT_API_KEY=$(openssl rand -hex 24)
71
125
  docker compose up -d
72
126
  ```
73
127
 
@@ -75,189 +129,231 @@ docker compose up -d
75
129
 
76
130
  Standalone binaries update themselves in place from the latest GitHub release:
77
131
 
78
- ```powershell
132
+ ```sh
79
133
  hoopilot update
80
134
  ```
81
135
 
82
136
  npm installs report when a newer version is available and print the right command. Hoopilot checks GitHub at most once a day in the background. Disable the check with `--no-update-check`, `HOOPILOT_NO_UPDATE_CHECK`, or `NO_UPDATE_NOTIFIER`.
83
137
 
84
- ## Run
138
+ ## Running the proxy
85
139
 
86
- First sign in with GitHub Copilot OAuth in your browser:
140
+ Login uses GitHub's browser/device flow, verifies that the returned OAuth token can reach the Copilot API, and stores it locally:
87
141
 
88
- ```powershell
89
- npx @openhoo/hoopilot login
142
+ ```sh
143
+ hoopilot login
90
144
  ```
91
145
 
92
- The login command prints a one-time code, opens `https://github.com/login/device` best-effort, verifies that the returned OAuth token can reach the Copilot API, and stores it in Hoopilot's auth file. Re-run `npx @openhoo/hoopilot login` after upgrading Hoopilot if Copilot reports a supported model as unavailable; older stored tokens can have a reduced model set.
146
+ Default credential paths:
93
147
 
94
- Then start the proxy:
148
+ - Linux/macOS: `$HOME/.config/hoopilot/auth.json`
149
+ - Windows: `%APPDATA%\hoopilot\auth.json`
95
150
 
96
- ```powershell
97
- npx @openhoo/hoopilot
151
+ Override the path with `HOOPILOT_AUTH_FILE` or `--auth-file`.
152
+
153
+ Start the server:
154
+
155
+ ```sh
156
+ hoopilot --port 4141
98
157
  ```
99
158
 
100
- By default Hoopilot listens on `127.0.0.1:4141` and reads the stored OAuth credential from:
159
+ By default Hoopilot listens on `127.0.0.1:4141`. If `HOOPILOT_API_KEY` is unset, local requests are accepted without client authentication. Binding to a non-loopback host requires either a strong, unique `HOOPILOT_API_KEY` or the explicit `--allow-unauthenticated` / `HOOPILOT_ALLOW_UNAUTHENTICATED=1` opt-in. Well-known demo keys are always rejected on a non-loopback host, even with the unauthenticated opt-in.
101
160
 
102
- - Linux/macOS: `$HOME/.config/hoopilot/auth.json`
103
- - Windows: `$env:APPDATA\hoopilot\auth.json`
161
+ When an API key is configured, clients may send it as either `Authorization: Bearer <key>` or `x-api-key: <key>`.
104
162
 
105
- Override the path with `HOOPILOT_AUTH_FILE` or `--auth-file`.
163
+ Cross-origin browser requests are always blocked, even when an API key is set, so a malicious web page cannot drive the local proxy. Requests from loopback origins are allowed; to permit specific web origins, list them in `HOOPILOT_ALLOWED_ORIGINS` (comma-separated).
106
164
 
107
- For a local API key:
165
+ ## Client setup
108
166
 
109
- ```powershell
110
- $env:HOOPILOT_API_KEY = "local-key"
111
- npx @openhoo/hoopilot --port 4141
167
+ ### OpenAI-compatible clients
168
+
169
+ ```sh
170
+ export OPENAI_BASE_URL=http://127.0.0.1:4141/v1
171
+ export OPENAI_API_KEY=hoopilot
112
172
  ```
113
173
 
114
- Point OpenAI-compatible clients at:
174
+ The client key value is arbitrary when the server runs without `HOOPILOT_API_KEY`; if you set one, use that value here instead.
115
175
 
116
- ```powershell
117
- $env:OPENAI_BASE_URL = "http://127.0.0.1:4141/v1"
118
- $env:OPENAI_API_KEY = "local-key"
176
+ Use any model returned by:
177
+
178
+ ```sh
179
+ hoopilot models
119
180
  ```
120
181
 
121
- Point Claude Code at the same server through its Anthropic base URL:
182
+ ### Claude Code and Anthropic-style clients
122
183
 
123
- ```powershell
124
- $env:ANTHROPIC_BASE_URL = "http://127.0.0.1:4141"
125
- $env:ANTHROPIC_AUTH_TOKEN = "local-key"
184
+ ```sh
185
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:4141
186
+ export ANTHROPIC_AUTH_TOKEN=hoopilot
126
187
  claude
127
188
  ```
128
189
 
129
- Hoopilot accepts the local key as either `Authorization: Bearer <key>` or
130
- `x-api-key: <key>`, so `ANTHROPIC_API_KEY` also works for clients that send
131
- Anthropic's `x-api-key` header.
190
+ Hoopilot accepts the local key as `x-api-key` too, so `ANTHROPIC_API_KEY` also works for clients that send Anthropic's standard API-key header.
132
191
 
133
- Use with Codex CLI after Hoopilot is running, via the bundled `codexx` command. It runs Codex against the local server with the right model provider — selecting `gpt-5.5` over Copilot's Responses API, which a plain `openai_base_url` override does not configure (see the note below):
192
+ ### Codex
134
193
 
135
- ```powershell
136
- $env:HOOPILOT_API_KEY = "local-key"
194
+ Use the bundled `codexx` command after Hoopilot is running:
195
+
196
+ ```sh
137
197
  codexx
138
198
  ```
139
199
 
140
- Without a global install, run it through npm:
200
+ Without a global install:
141
201
 
142
- ```powershell
143
- $env:HOOPILOT_API_KEY = "local-key"
202
+ ```sh
144
203
  npx --package @openhoo/hoopilot codexx
145
204
  ```
146
205
 
147
- `codexx` does not start Hoopilot and does not change your shell environment. It runs
148
- `codex` with a temporary `hoopilot` model provider pointed at
149
- `http://127.0.0.1:4141/v1`, disables Codex Responses WebSockets for that provider,
150
- maps `HOOPILOT_API_KEY` to `OPENAI_API_KEY` for that child process, passes
151
- `--disable network_proxy` to Codex, and removes standard proxy variables from the
152
- spawned Codex process so Codex talks directly to the local server. Override the local
153
- URL with `CODEXX_BASE_URL`, the local key with `CODEXX_API_KEY`, or the Codex
154
- executable with `CODEXX_CODEX_BIN`, the model with `CODEXX_MODEL`, or the reasoning
155
- effort with `CODEXX_MODEL_REASONING_EFFORT`.
156
-
157
- `codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Codex sends
158
- those requests through its Responses API provider, and Hoopilot forwards them to
159
- Copilot's Responses endpoint because `gpt-5.5` is not available through Copilot's
160
- chat-completions endpoint. Before starting Codex, `codexx` checks
161
- `http://127.0.0.1:4141/v1/models` and reports if the logged-in Copilot account does
162
- not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models,
163
- or log in with a Copilot account that has `gpt-5.5`.
164
-
165
- When Codex compacts a long session it POSTs to `/v1/responses/compact` — a server-side
166
- endpoint it expects from `OpenAI`- and Azure-named providers and for which it has no
167
- local fallback, so an unhandled route would abort compaction. Hoopilot handles it by
168
- running the supplied conversation through Copilot's Responses endpoint as a unary
169
- request and returning the resulting `{ "output": [...] }` summary, so compaction works
170
- whether Codex points at Hoopilot through `codexx` or through a plain `OPENAI_BASE_URL`
171
- override of the built-in `openai` provider.
172
-
173
- If no `HOOPILOT_API_KEY` is configured, Hoopilot accepts local requests without client authentication. Binding to a non-loopback host requires `HOOPILOT_API_KEY` unless `--allow-unauthenticated` is set.
206
+ If the server requires an API key, set `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in the `codexx` environment to match.
174
207
 
175
- ## Logging
208
+ `codexx` does not start Hoopilot and does not alter your shell environment. It starts `codex` with a temporary `hoopilot` model provider pointed at `http://127.0.0.1:4141/v1`, uses the Responses API wire format, disables Responses WebSockets for that provider, maps `HOOPILOT_API_KEY` (or a random throwaway key when none is set) to `OPENAI_API_KEY` for the child process, passes `--disable network_proxy`, and removes standard proxy variables from the spawned Codex process.
176
209
 
177
- Hoopilot uses Pino for structured logs. Server startup, request completion, upstream Copilot failures, model-list fallback, auth failures, and update-check diagnostics are logged with stable event names and request IDs. Logs never include request bodies, prompt text, completions, stream chunks, OAuth tokens, API keys, authorization headers, cookies, or auth-file contents.
210
+ `codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Before starting Codex, it checks `/v1/models` and reports if the logged-in Copilot account does not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models, or log in with a Copilot account that has access to the default model.
178
211
 
179
- Console logs default to pretty output at `info` level:
212
+ Codex compaction posts to `/v1/responses/compact` for OpenAI- and Azure-named providers. Hoopilot handles that route with a unary Copilot Responses request and returns the `{ "output": [...] }` summary Codex expects, so compaction works through either `codexx` or a direct OpenAI-compatible base URL override.
180
213
 
181
- ```powershell
182
- npx @openhoo/hoopilot --log-level info --log-format pretty
214
+ ## Authentication
215
+
216
+ Hoopilot supports one upstream credential flow: GitHub Copilot OAuth browser login.
217
+
218
+ ```sh
219
+ hoopilot login
220
+ hoopilot
183
221
  ```
184
222
 
185
- For machine-readable newline-delimited JSON:
223
+ Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-grained GitHub PATs are not supported.
224
+
225
+ Re-run `hoopilot login` after upgrading Hoopilot if Copilot reports a supported model as unavailable. Older stored tokens can have a reduced model set.
226
+
227
+ To print the verified OAuth token for another local tool, use `--print-key`. Login status goes to stderr, so stdout contains only the token.
228
+
229
+ ```sh
230
+ hoopilot login --print-key | sed 's/^/COPILOT_OAUTH_TOKEN=/' >> .env
231
+ ```
232
+
233
+ PowerShell:
186
234
 
187
235
  ```powershell
188
- npx @openhoo/hoopilot --log-level info --log-format json
236
+ hoopilot login --print-key |
237
+ ForEach-Object { "COPILOT_OAUTH_TOKEN=$_" } |
238
+ Add-Content -Encoding utf8 .env
189
239
  ```
190
240
 
191
- Equivalent environment variables:
241
+ Docker:
192
242
 
193
- - `HOOPILOT_LOG_LEVEL`: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent`. Default: `info`.
194
- - `HOOPILOT_LOG_FORMAT`: `json` or `pretty`. Default: `pretty`.
243
+ ```sh
244
+ docker run --rm -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login --print-key \
245
+ | sed 's/^/COPILOT_OAUTH_TOKEN=/' >> .env
246
+ ```
247
+
248
+ ## Logging
249
+
250
+ Hoopilot uses Pino for structured logs. Server startup, request completion, upstream Copilot failures, model-list fallback, auth failures, and update-check diagnostics are logged with stable event names and request IDs.
251
+
252
+ Logs never include request bodies, prompt text, completions, stream chunks, OAuth tokens, API keys, authorization headers, cookies, or auth-file contents.
253
+
254
+ Console logs default to pretty output at `info` level:
255
+
256
+ ```sh
257
+ hoopilot --log-level info --log-format pretty
258
+ ```
259
+
260
+ For newline-delimited JSON:
261
+
262
+ ```sh
263
+ hoopilot --log-level info --log-format json
264
+ ```
195
265
 
196
266
  Incoming `x-request-id` headers are preserved on responses. If a request has no ID, Hoopilot generates one and returns it as `x-request-id`.
197
267
 
198
268
  ## Metrics and usage
199
269
 
200
- Hoopilot tracks token usage, request counts, and latency in memory while the server runs, and can report your GitHub Copilot account quota (premium-request "credit" usage).
270
+ Hoopilot tracks token usage, request counts, and latency in memory while the server runs. It can also report your GitHub Copilot account quota and premium-request usage.
201
271
 
202
- - `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters (`hoopilot_requests_total`), upstream call counters (`hoopilot_upstream_requests_total`), token counters by model and type (`hoopilot_tokens_total{model,type}`), a request-duration histogram (`hoopilot_request_duration_seconds`), an in-flight gauge, and—once `/v1/usage` has been fetched at least once—Copilot quota gauges (`hoopilot_copilot_quota_remaining{category}`, `_entitlement`, `_used`, `_percent_remaining`, `_overage_count`, `_overage_entitlement`, `_unlimited`, `_overage_permitted`, `_has_quota`, `_token_based_billing`, and category reset/snapshot timestamps). Counters reset to zero on restart, which Prometheus handles natively.
203
- - `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub (cached for 60 seconds). If the quota cannot be read, `copilot` is `null` and `copilot_error` explains why, but the proxy metrics are still returned.
272
+ - `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters, upstream call counters, token counters by model and type, a request-duration histogram, an in-flight gauge, and Copilot quota gauges after `/v1/usage` has been fetched at least once. Counters reset to zero on restart, which Prometheus handles natively.
273
+ - `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub and cached for 60 seconds. If quota cannot be read, `copilot` is `null` and `copilot_error` explains why.
204
274
  - `hoopilot usage` prints your Copilot plan and quota from the command line.
205
275
 
206
- Token usage is read from the upstream `usage` object. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot never injects it, so streamed chat requests without that flag contribute request and latency metrics but not token counts. The Responses API always reports usage, so streamed Responses requests are fully accounted.
276
+ Token usage is read from the upstream `usage` object. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot does not inject that flag. Responses API streaming always reports usage, so streamed Responses requests are fully accounted.
207
277
 
208
278
  `/metrics` and `/v1/usage` are subject to the same `HOOPILOT_API_KEY` gate as the other routes.
209
279
 
210
- ## Authentication
280
+ ## Troubleshooting
211
281
 
212
- Hoopilot supports one credential flow: GitHub Copilot OAuth browser login.
282
+ ### Codex auth errors
213
283
 
214
- ```powershell
215
- npx @openhoo/hoopilot login
216
- npx @openhoo/hoopilot
284
+ Hoopilot does not return raw `403` responses to Codex for authentication or Copilot-entitlement failures. Local Hoopilot API-key problems return `401 invalid_api_key`; OAuth credential and upstream Copilot auth failures return `401 copilot_auth_error`.
285
+
286
+ Verify browser login and the local proxy before retrying Codex:
287
+
288
+ ```sh
289
+ hoopilot login
290
+ hoopilot --port 4141
217
291
  ```
218
292
 
219
- Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-grained GitHub PATs are not supported.
293
+ Then, in another shell:
220
294
 
221
- Supported authentication-related settings:
295
+ ```sh
296
+ curl http://127.0.0.1:4141/v1/models
297
+ codexx
298
+ ```
222
299
 
223
- - `HOOPILOT_AUTH_FILE`: OAuth credential store path.
224
- - `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override. The default uses the same GitHub Copilot OAuth app as opencode's Copilot provider.
225
- - `HOOPILOT_GITHUB_DOMAIN`: GitHub domain override. Default: `github.com`.
226
- - `COPILOT_API_BASE_URL`: upstream Copilot API base URL override. Default: `https://api.githubcopilot.com`.
227
- - `HOOPILOT_GITHUB_API_BASE_URL`: GitHub REST API base URL used for the Copilot quota lookup. Default: `https://api.github.com`.
228
- - `HOOPILOT_ALLOW_UNSAFE_UPSTREAM=1`: allow sending the stored OAuth token to nonstandard HTTPS Copilot/GitHub API hosts. Use only for trusted test or enterprise endpoints.
300
+ If you started the server with `HOOPILOT_API_KEY`, add `-H "Authorization: Bearer $HOOPILOT_API_KEY"` to the curl command and set the same `HOOPILOT_API_KEY` for `codexx`.
229
301
 
230
- ## Codex Auth Errors
302
+ If `/v1/models` returns `401 copilot_auth_error`, rerun `hoopilot login` and confirm that the GitHub account has active Copilot access.
231
303
 
232
- Hoopilot does not return raw `403` responses to Codex for authentication or Copilot-entitlement failures. Local Hoopilot API-key problems return `401 invalid_api_key`; OAuth credential and upstream Copilot auth failures return `401 copilot_auth_error`.
304
+ ## Configuration
233
305
 
234
- In PowerShell, verify the browser login and local proxy before retrying Codex:
306
+ Server and local-client settings:
235
307
 
236
- ```powershell
237
- npx @openhoo/hoopilot login
238
- $env:HOOPILOT_API_KEY = "local-key"
239
- npx @openhoo/hoopilot --port 4141
240
- ```
308
+ | Setting | Description |
309
+ | --- | --- |
310
+ | `HOST` / `--host` | Host to listen on. Default: `127.0.0.1` for local runs; Docker sets `0.0.0.0`. |
311
+ | `PORT` / `--port` | Port to listen on. Default: `4141`. |
312
+ | `HOOPILOT_API_KEY` / `--api-key` | Require clients to send `Authorization: Bearer <key>` or `x-api-key: <key>`. Must be a strong, unique secret on non-loopback binds; well-known demo keys are rejected. |
313
+ | `--api-key-file` | Read the local API key from a file instead of argv. |
314
+ | `HOOPILOT_ALLOWED_ORIGINS` | Comma-separated browser origins allowed to make cross-origin requests. Loopback origins are always allowed; every other origin is blocked. |
315
+ | `HOOPILOT_ALLOW_UNAUTHENTICATED` / `--allow-unauthenticated` | Allow non-loopback binds without a local API key. |
316
+ | `HOOPILOT_STREAM_MODE` / `--stream-mode` | `auto`, `live`, or `buffer`. `auto` buffers streams for Windows standalone binaries. |
241
317
 
242
- Then, in another PowerShell session:
318
+ Copilot and GitHub settings:
243
319
 
244
- ```powershell
245
- $env:OPENAI_API_KEY = "local-key"
246
- Invoke-RestMethod -Headers @{ Authorization = "Bearer $env:OPENAI_API_KEY" } `
247
- http://127.0.0.1:4141/v1/models
248
- codexx
249
- ```
320
+ | Setting | Description |
321
+ | --- | --- |
322
+ | `HOOPILOT_AUTH_FILE` / `--auth-file` | OAuth credential store path. |
323
+ | `HOOPILOT_GITHUB_CLIENT_ID` | GitHub OAuth app client ID override. |
324
+ | `HOOPILOT_GITHUB_DOMAIN` | GitHub domain override. Default: `github.com`. |
325
+ | `COPILOT_API_BASE_URL` / `--copilot-api-base-url` | Upstream Copilot API base URL. Default: `https://api.githubcopilot.com`. |
326
+ | `HOOPILOT_GITHUB_API_BASE_URL` | GitHub REST API base URL used for quota lookup. Default: `https://api.github.com`. |
327
+ | `HOOPILOT_ALLOW_UNSAFE_UPSTREAM=1` | Allow sending the stored OAuth token to nonstandard HTTPS Copilot/GitHub API hosts. Use only for trusted test or enterprise endpoints. |
250
328
 
251
- If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` and confirm the GitHub account has active Copilot access.
329
+ Logging and update settings:
252
330
 
253
- ## CLI
331
+ | Setting | Description |
332
+ | --- | --- |
333
+ | `HOOPILOT_LOG_LEVEL` / `--log-level` | `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent`. Default: `info`. |
334
+ | `HOOPILOT_LOG_FORMAT` / `--log-format` | `pretty` or `json`. Default: `pretty`. |
335
+ | `HOOPILOT_NO_UPDATE_CHECK` / `--no-update-check` | Disable background update checks. `NO_UPDATE_NOTIFIER` is also honored. |
254
336
 
255
- ```powershell
337
+ `codexx` settings:
338
+
339
+ | Setting | Description |
340
+ | --- | --- |
341
+ | `CODEXX_BASE_URL` | OpenAI-compatible Hoopilot base URL. Default: `http://127.0.0.1:4141/v1`. |
342
+ | `CODEXX_API_KEY` | API key sent to Hoopilot. Falls back to `HOOPILOT_API_KEY`, then a random per-run key for an unauthenticated local server. |
343
+ | `CODEXX_CODEX_BIN` | Codex executable to run. Default: `codex`. |
344
+ | `CODEXX_MODEL` | Codex model to use. Default: `gpt-5.5`. |
345
+ | `CODEXX_MODEL_REASONING_EFFORT` | Codex reasoning effort. Default: `xhigh`. |
346
+ | `CODEXX_SKIP_MODEL_PREFLIGHT=1` | Skip the `/v1/models` availability check before starting Codex. |
347
+
348
+ ## CLI reference
349
+
350
+ ```txt
256
351
  hoopilot [serve] [options]
257
352
  hoopilot codexx [codex options] [prompt]
258
353
  hoopilot login [options]
259
354
  hoopilot models [options]
260
355
  hoopilot usage [options]
356
+ hoopilot update
261
357
  ```
262
358
 
263
359
  Commands:
@@ -280,36 +376,41 @@ Options:
280
376
  --api-key-file <path> Read the local API key from a file instead of argv
281
377
  --auth-file <path> OAuth credential store path
282
378
  --copilot-api-base-url <url> Copilot API base URL override
379
+ --print-key Login: print the received OAuth token to stdout
283
380
  --log-level <level> trace, debug, info, warn, error, fatal, or silent
284
381
  --log-format <format> json or pretty. Default: pretty
382
+ --stream-mode <mode> auto, live, or buffer. Auto buffers Windows standalone streams.
285
383
  --no-update-check Do not check GitHub for a newer release
286
384
  --allow-unauthenticated Allow non-loopback bind without --api-key
385
+ -h, --help Show help
386
+ -v, --version Show version
287
387
  ```
288
388
 
289
389
  ## Endpoints
290
390
 
291
- - `GET /healthz`
391
+ - `GET /` and `GET /healthz`
292
392
  - `GET /metrics`
293
393
  - `GET /v1/models`
294
394
  - `GET /v1/usage`
295
- - `POST /v1/messages`
296
- - `POST /v1/messages/count_tokens`
297
395
  - `POST /v1/chat/completions`
298
396
  - `POST /v1/responses`
397
+ - `POST /v1/responses/compact`
299
398
  - `POST /v1/completions`
399
+ - `POST /v1/messages`
400
+ - `POST /v1/messages/count_tokens`
300
401
 
301
- `/v1/chat/completions` and `/v1/responses` are proxied to the matching Copilot endpoints as directly as possible. `/v1/messages` translates Anthropic Messages requests and responses to Copilot's Responses endpoint for Claude Code and other Anthropic-compatible clients. `/v1/messages/count_tokens` returns a local token estimate for Claude Code preflights because Copilot does not expose Anthropic's count-tokens route. `/v1/completions` translates legacy completion requests and responses to the closest chat completions equivalent. `GET /metrics` and `GET /v1/usage` report proxy metrics and Copilot quota (see [Metrics and usage](#metrics-and-usage)).
402
+ `/v1/chat/completions` and `/v1/responses` are proxied to the matching Copilot endpoints as directly as possible. `/v1/messages` translates Anthropic Messages requests and responses to Copilot's Responses endpoint. `/v1/messages/count_tokens` returns a local token estimate for Claude Code preflights because Copilot does not expose Anthropic's count-tokens route. `/v1/completions` translates legacy completion requests and responses to the closest chat-completions equivalent. `GET /v1/responses` returns an explicit unsupported-WebSocket response; `codexx` configures Codex to use HTTP Responses instead.
302
403
 
303
404
  ## Development
304
405
 
305
- ```powershell
406
+ ```sh
306
407
  bun install
307
408
  bun run check
308
409
  ```
309
410
 
310
411
  Useful scripts:
311
412
 
312
- ```powershell
413
+ ```sh
313
414
  bun run test
314
415
  bun run test:coverage
315
416
  bun run typecheck
@@ -319,7 +420,11 @@ bun run biome:fix
319
420
 
320
421
  ## Release
321
422
 
322
- Commits merged to `main` are evaluated by hooversion after CI passes. When a release is produced, the release workflow creates the release commit, tag, and GitHub release automatically, publishes the package through npm trusted publishing, then cross-compiles standalone binaries for every supported platform (`scripts/build-binaries.sh`) and attaches them plus a `SHA256SUMS` manifest to the GitHub release. Build all binaries locally with `bun run build:binaries`.
423
+ Commits merged to `main` are evaluated by hooversion after CI passes. When a release is produced, the release workflow creates the release commit, tag, and GitHub release automatically, publishes the package through npm trusted publishing, then cross-compiles standalone binaries for every supported platform with `scripts/build-binaries.sh` and attaches them plus a `SHA256SUMS` manifest to the GitHub release. Build all binaries locally with:
424
+
425
+ ```sh
426
+ bun run build:binaries
427
+ ```
323
428
 
324
429
  Configure npm trusted publishing for `@openhoo/hoopilot` on npmjs.com before relying on automatic publication. The workflow uses GitHub Actions OIDC with `npm publish --access public --provenance`.
325
430
 
@@ -52,7 +52,6 @@ function asRecord(value) {
52
52
 
53
53
  // src/codexx.ts
54
54
  var DEFAULT_BASE_URL = "http://127.0.0.1:4141/v1";
55
- var DEFAULT_API_KEY = "local-key";
56
55
  var DEFAULT_CODEX_BIN = "codex";
57
56
  var DEFAULT_MODEL = "gpt-5.5";
58
57
  var DEFAULT_REASONING_EFFORT = "xhigh";
@@ -68,7 +67,7 @@ var PROXY_ENV_KEYS = [
68
67
  ];
69
68
  function buildCodexxInvocation(argv, env = process.env) {
70
69
  const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;
71
- const apiKey = envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? DEFAULT_API_KEY;
70
+ const apiKey = envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();
72
71
  const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;
73
72
  const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;
74
73
  const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;
@@ -102,6 +101,9 @@ function buildCodexxInvocation(argv, env = process.env) {
102
101
  model
103
102
  };
104
103
  }
104
+ function generateEphemeralApiKey() {
105
+ return `codexx-${crypto.randomUUID()}`;
106
+ }
105
107
  function withoutProxyEnv(env) {
106
108
  const next = { ...env };
107
109
  for (const key of PROXY_ENV_KEYS) {
@@ -142,7 +144,7 @@ async function verifyCodexxModel(invocation, fetcher = fetch) {
142
144
  response = await fetcher(modelsUrl, {
143
145
  headers: {
144
146
  accept: "application/json",
145
- authorization: `Bearer ${invocation.env.OPENAI_API_KEY ?? DEFAULT_API_KEY}`
147
+ authorization: `Bearer ${invocation.env.OPENAI_API_KEY ?? generateEphemeralApiKey()}`
146
148
  },
147
149
  method: "GET"
148
150
  });
@@ -174,7 +176,9 @@ Usage:
174
176
  Environment:
175
177
  CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}
176
178
  CODEXX_API_KEY API key sent to the local Hoopilot server.
177
- HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset.
179
+ HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset. When
180
+ neither is set, a random throwaway key is generated for
181
+ an unauthenticated local server.
178
182
  CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}
179
183
  CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}
180
184
  CODEXX_MODEL_REASONING_EFFORT
@@ -218,4 +222,4 @@ export {
218
222
  main,
219
223
  verifyCodexxModel
220
224
  };
221
- //# sourceMappingURL=chunk-7GSQVYYT.js.map
225
+ //# sourceMappingURL=chunk-JU6F5L34.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codexx.ts","../src/util.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\nimport type { FetchLike } from \"./types\";\nimport { envValue } from \"./util\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst DEFAULT_MODEL = \"gpt-5.5\";\nconst DEFAULT_REASONING_EFFORT = \"xhigh\";\nconst PROXY_ENV_KEYS = [\n \"ALL_PROXY\",\n \"HTTPS_PROXY\",\n \"HTTP_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"https_proxy\",\n \"http_proxy\",\n \"no_proxy\",\n];\n\nexport interface CodexxInvocation {\n args: string[];\n baseUrl: string;\n command: string;\n env: NodeJS.ProcessEnv;\n model: string;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;\n // Never fall back to a public, predictable key: a shared constant like the old\n // \"local-key\" default is also a credential a malicious local/browser client\n // could guess. When no key is configured the local server is expected to run\n // unauthenticated, which accepts any value, so a random throwaway key is safe.\n const apiKey =\n envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();\n const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;\n const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;\n const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;\n const providerConfig = [\n '{ name = \"Hoopilot\"',\n `base_url = ${JSON.stringify(baseUrl)}`,\n 'env_key = \"OPENAI_API_KEY\"',\n 'wire_api = \"responses\"',\n \"supports_websockets = false }\",\n ].join(\", \");\n\n return {\n args: [\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n 'model_provider=\"hoopilot\"',\n \"-c\",\n `model_providers.hoopilot=${providerConfig}`,\n \"-m\",\n model,\n \"-c\",\n `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,\n ...argv,\n ],\n baseUrl,\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n model,\n };\n}\n\n// A random, non-guessable placeholder key for when neither CODEXX_API_KEY nor\n// HOOPILOT_API_KEY is set. An unauthenticated local Hoopilot accepts any value;\n// a keyed server rejects it with a 401, which the model preflight surfaces.\nfunction generateEphemeralApiKey(): string {\n return `codexx-${crypto.randomUUID()}`;\n}\n\nfunction withoutProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const next = { ...env };\n for (const key of PROXY_ENV_KEYS) {\n delete next[key];\n }\n return next;\n}\n\nexport async function main(argv = Bun.argv.slice(2), env = process.env): Promise<void> {\n if (argv.length === 1 && (argv[0] === \"--help\" || argv[0] === \"-h\")) {\n console.log(helpText());\n return;\n }\n\n const invocation = buildCodexxInvocation(argv, env);\n if (env.CODEXX_SKIP_MODEL_PREFLIGHT !== \"1\") {\n await verifyCodexxModel(invocation);\n }\n const child = spawn(invocation.command, invocation.args, {\n env: invocation.env,\n shell: process.platform === \"win32\",\n stdio: \"inherit\",\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (typeof code === \"number\") {\n resolve(code);\n return;\n }\n resolve(signal ? 128 + signalNumber(signal) : 1);\n });\n });\n\n process.exitCode = exitCode;\n}\n\nexport async function verifyCodexxModel(\n invocation: Pick<CodexxInvocation, \"baseUrl\" | \"env\" | \"model\">,\n fetcher: FetchLike = fetch,\n): Promise<void> {\n const modelsUrl = `${invocation.baseUrl.replace(/\\/+$/, \"\")}/models`;\n let response: Response;\n try {\n response = await fetcher(modelsUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${invocation.env.OPENAI_API_KEY ?? generateEphemeralApiKey()}`,\n },\n method: \"GET\",\n });\n } catch (error) {\n throw new Error(\n `Could not reach Hoopilot at ${modelsUrl}. Start Hoopilot first, or set CODEXX_SKIP_MODEL_PREFLIGHT=1 to skip this check. ${errorMessage(error)}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await shortResponseText(response)}`,\n );\n }\n\n const models = modelIds(await response.json().catch(() => undefined));\n if (models.length > 0 && !models.includes(invocation.model)) {\n throw new Error(\n `The logged-in Copilot account does not advertise model ${JSON.stringify(invocation.model)} at ${modelsUrl}. Available models: ${models.join(\", \")}. After upgrading Hoopilot, rerun \"hoopilot login\" to refresh the Copilot OAuth token, or set CODEXX_MODEL to one of the advertised model IDs.`,\n );\n }\n}\n\nfunction helpText(): string {\n return `codexx\n\nRun Codex against an already-running local Hoopilot server.\n\nUsage:\n codexx [codex options] [prompt]\n\nEnvironment:\n CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}\n CODEXX_API_KEY API key sent to the local Hoopilot server.\n HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset. When\n neither is set, a random throwaway key is generated for\n an unauthenticated local server.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}\n CODEXX_MODEL_REASONING_EFFORT\n Codex reasoning effort. Default: ${DEFAULT_REASONING_EFFORT}\n CODEXX_SKIP_MODEL_PREFLIGHT\n Set to 1 to skip checking /v1/models before starting Codex.\n\ncodexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's network_proxy feature, and removes proxy variables only from the spawned Codex process.`;\n}\n\nfunction signalNumber(signal: NodeJS.Signals): number {\n return osConstants.signals[signal] ?? 1;\n}\n\nfunction modelIds(value: unknown): string[] {\n const record = value && typeof value === \"object\" && !Array.isArray(value) ? value : {};\n const data = \"data\" in record && Array.isArray(record.data) ? record.data : [];\n return data\n .map((entry) =>\n entry && typeof entry === \"object\" && \"id\" in entry && typeof entry.id === \"string\"\n ? entry.id\n : undefined,\n )\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n}\n\nasync function shortResponseText(response: Response): Promise<string> {\n const text = await response.text();\n return text.slice(0, 500);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(errorMessage(error));\n process.exit(1);\n });\n}\n","import type { JsonObject } from \"./types\";\n\n/** Remove any trailing slashes from a URL or path string. */\nexport function trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\n/** Treat blank environment variables as unset while preserving nonblank values. */\nexport function envValue(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\n/** True for HTTPS URLs, or HTTP only on loopback hosts used by local tests/dev. */\nexport function isHttpsOrLoopbackUrl(rawUrl: string): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n return url.protocol === \"https:\" || isLoopbackHttpUrl(url);\n}\n\n/** Validate a base URL before sending a bearer/OAuth token to it. */\nexport function isTrustedTokenBaseUrl(\n rawUrl: string,\n allowedHttpsHosts: readonly string[],\n allowUnsafeHttps = false,\n): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n if (url.pathname !== \"\" && url.pathname !== \"/\") {\n return false;\n }\n if (isLoopbackHttpUrl(url)) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n const host = url.hostname.toLowerCase();\n return allowedHttpsHosts.includes(host) || allowUnsafeHttps;\n}\n\nfunction parseUrl(rawUrl: string): URL | undefined {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n return undefined;\n }\n return url;\n}\n\nfunction isLoopbackHttpUrl(url: URL): boolean {\n return (\n url.protocol === \"http:\" &&\n (url.hostname === \"127.0.0.1\" ||\n url.hostname === \"localhost\" ||\n url.hostname === \"::1\" ||\n url.hostname === \"[::1]\")\n );\n}\n\n/** Read a response body as text, truncated to keep error messages bounded. */\nexport async function truncatedResponseText(response: Response, max = 500): Promise<string> {\n const text = await response.text();\n return text.slice(0, max);\n}\n\n/** Narrow an unknown value to a plain object, returning {} for arrays/primitives/null. */\nexport function asRecord(value: unknown): JsonObject {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as JsonObject) : {};\n}\n"],"mappings":"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","names":[]}