@openhoo/hoopilot 0.5.7 → 0.6.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
@@ -68,7 +68,7 @@ First sign in with GitHub Copilot OAuth in your browser:
68
68
  npx @openhoo/hoopilot login
69
69
  ```
70
70
 
71
- 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.
71
+ 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.
72
72
 
73
73
  Then start the proxy:
74
74
 
@@ -137,7 +137,10 @@ effort with `CODEXX_MODEL_REASONING_EFFORT`.
137
137
  `codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Codex sends
138
138
  those requests through its Responses API provider, and Hoopilot forwards them to
139
139
  Copilot's Responses endpoint because `gpt-5.5` is not available through Copilot's
140
- chat-completions endpoint.
140
+ chat-completions endpoint. Before starting Codex, `codexx` checks
141
+ `http://127.0.0.1:4141/v1/models` and reports if the logged-in Copilot account does
142
+ not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models,
143
+ or log in with a Copilot account that has `gpt-5.5`.
141
144
 
142
145
  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.
143
146
 
@@ -178,7 +181,7 @@ Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-g
178
181
  Supported authentication-related settings:
179
182
 
180
183
  - `HOOPILOT_AUTH_FILE`: OAuth credential store path.
181
- - `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override.
184
+ - `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override. The default uses the same GitHub Copilot OAuth app as opencode's Copilot provider.
182
185
  - `HOOPILOT_GITHUB_DOMAIN`: GitHub domain override. Default: `github.com`.
183
186
  - `COPILOT_API_BASE_URL`: upstream Copilot API base URL override. Default: `https://api.githubcopilot.com`.
184
187
 
@@ -210,6 +213,7 @@ If that returns `401 copilot_auth_error`, rerun `npx @openhoo/hoopilot login` an
210
213
  ```powershell
211
214
  hoopilot [serve] [options]
212
215
  hoopilot login [options]
216
+ hoopilot models [options]
213
217
  ```
214
218
 
215
219
  Commands:
@@ -217,6 +221,7 @@ Commands:
217
221
  ```txt
218
222
  serve Start the proxy server (default)
219
223
  login Sign in through GitHub OAuth in a browser and verify Copilot access
224
+ models List available GitHub Copilot model IDs
220
225
  update, upgrade Update hoopilot to the latest release
221
226
  ```
222
227
 
package/dist/cli.js CHANGED
@@ -101,9 +101,64 @@ function trimTrailingSlash(value) {
101
101
  return value.replace(/\/+$/, "");
102
102
  }
103
103
 
104
+ // src/copilot.ts
105
+ var CopilotClient = class {
106
+ #auth;
107
+ #fetch;
108
+ constructor(options = {}) {
109
+ this.#auth = new CopilotAuth(options);
110
+ this.#fetch = options.fetch ?? fetch;
111
+ }
112
+ async chatCompletions(body, signal) {
113
+ return this.fetchCopilot("/chat/completions", {
114
+ body: JSON.stringify(body),
115
+ headers: {
116
+ "content-type": "application/json"
117
+ },
118
+ method: "POST",
119
+ signal
120
+ });
121
+ }
122
+ async responses(body, signal) {
123
+ return this.fetchCopilot("/responses", {
124
+ body,
125
+ headers: {
126
+ "content-type": "application/json"
127
+ },
128
+ method: "POST",
129
+ signal
130
+ });
131
+ }
132
+ async models(signal) {
133
+ return this.fetchCopilot("/models", {
134
+ headers: {
135
+ accept: "application/json"
136
+ },
137
+ method: "GET",
138
+ signal
139
+ });
140
+ }
141
+ async fetchCopilot(path, init) {
142
+ const access = await this.#auth.getAccess();
143
+ const headers = new Headers(init.headers);
144
+ headers.set("accept", headers.get("accept") ?? "application/json");
145
+ headers.set("authorization", `Bearer ${access.token}`);
146
+ headers.set("copilot-integration-id", "vscode-chat");
147
+ headers.set("editor-plugin-version", "hoopilot/0.1.0");
148
+ headers.set("editor-version", "Hoopilot/0.1.0");
149
+ headers.set("openai-intent", "conversation-panel");
150
+ headers.set("user-agent", "hoopilot/0.1.0");
151
+ headers.set("x-github-api-version", "2026-06-01");
152
+ return this.#fetch(`${access.apiBaseUrl}${path}`, {
153
+ ...init,
154
+ headers
155
+ });
156
+ }
157
+ };
158
+
104
159
  // src/github-device.ts
105
160
  import { setTimeout as sleep } from "timers/promises";
106
- var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Iv23lijnNxm2e9UX3CF8";
161
+ var DEFAULT_GITHUB_COPILOT_CLIENT_ID = "Ov23li8tweQw6odWQebz";
107
162
  var DEFAULT_GITHUB_DOMAIN = "github.com";
108
163
  var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
109
164
  var POLLING_SAFETY_MARGIN_MS = 3e3;
@@ -321,61 +376,6 @@ function isLogLevel(value) {
321
376
  return LOG_LEVELS.includes(value);
322
377
  }
323
378
 
324
- // src/copilot.ts
325
- var CopilotClient = class {
326
- #auth;
327
- #fetch;
328
- constructor(options = {}) {
329
- this.#auth = new CopilotAuth(options);
330
- this.#fetch = options.fetch ?? fetch;
331
- }
332
- async chatCompletions(body, signal) {
333
- return this.fetchCopilot("/chat/completions", {
334
- body: JSON.stringify(body),
335
- headers: {
336
- "content-type": "application/json"
337
- },
338
- method: "POST",
339
- signal
340
- });
341
- }
342
- async responses(body, signal) {
343
- return this.fetchCopilot("/responses", {
344
- body,
345
- headers: {
346
- "content-type": "application/json"
347
- },
348
- method: "POST",
349
- signal
350
- });
351
- }
352
- async models(signal) {
353
- return this.fetchCopilot("/models", {
354
- headers: {
355
- accept: "application/json"
356
- },
357
- method: "GET",
358
- signal
359
- });
360
- }
361
- async fetchCopilot(path, init) {
362
- const access = await this.#auth.getAccess();
363
- const headers = new Headers(init.headers);
364
- headers.set("accept", headers.get("accept") ?? "application/json");
365
- headers.set("authorization", `Bearer ${access.token}`);
366
- headers.set("copilot-integration-id", "vscode-chat");
367
- headers.set("editor-plugin-version", "hoopilot/0.1.0");
368
- headers.set("editor-version", "Hoopilot/0.1.0");
369
- headers.set("openai-intent", "conversation-panel");
370
- headers.set("user-agent", "hoopilot/0.1.0");
371
- headers.set("x-github-api-version", "2026-06-01");
372
- return this.#fetch(`${access.apiBaseUrl}${path}`, {
373
- ...init,
374
- headers
375
- });
376
- }
377
- };
378
-
379
379
  // src/openai.ts
380
380
  var DEFAULT_MODEL = "gpt-4.1";
381
381
  function normalizeChatCompletionRequest(request) {
@@ -1460,6 +1460,20 @@ async function main(argv = Bun.argv.slice(2)) {
1460
1460
  await runLogin(args2);
1461
1461
  return;
1462
1462
  }
1463
+ if (command === "models") {
1464
+ const args2 = withRuntimeEnv(parseArgs(argv.slice(1)));
1465
+ if (args2.help) {
1466
+ console.log(helpText(await getVersion()));
1467
+ return;
1468
+ }
1469
+ args2.logger = createHoopilotLogger({
1470
+ env: args2.env,
1471
+ format: args2.logFormat,
1472
+ level: args2.logLevel
1473
+ }).child({ component: "cli", command: "models" });
1474
+ await runModels(args2);
1475
+ return;
1476
+ }
1463
1477
  const args = withRuntimeEnv(parseArgs(argv));
1464
1478
  if (args.help) {
1465
1479
  console.log(helpText(await getVersion()));
@@ -1585,6 +1599,30 @@ async function runLogin(options = {}) {
1585
1599
  console.log(`Copilot OAuth credential stored at ${path}`);
1586
1600
  console.log("Copilot authentication ready.");
1587
1601
  }
1602
+ async function runModels(options = {}) {
1603
+ const logger = options.logger?.child({ component: "models" }) ?? noopLogger;
1604
+ logger.debug({ event: "models.list.started" }, "fetching github copilot models");
1605
+ const response = await new CopilotClient(options).models();
1606
+ if (!response.ok) {
1607
+ const message = `GitHub Copilot API model list failed with ${response.status}: ${await safeResponseText2(response)}`;
1608
+ if (response.status === 401 || response.status === 403) {
1609
+ throw new CopilotAuthError(message);
1610
+ }
1611
+ throw new Error(message);
1612
+ }
1613
+ const ids = modelIdsFromResponse(await response.json().catch(() => void 0));
1614
+ if (ids.length === 0) {
1615
+ throw new Error("GitHub Copilot API returned no model IDs.");
1616
+ }
1617
+ logger.debug(
1618
+ { count: ids.length, event: "models.list.succeeded" },
1619
+ "github copilot models fetched"
1620
+ );
1621
+ for (const id of ids) {
1622
+ console.log(id);
1623
+ }
1624
+ return ids;
1625
+ }
1588
1626
  async function verifyCopilotOAuthToken(token, options = {}) {
1589
1627
  const apiBaseUrl = trimTrailingSlash2(
1590
1628
  options.copilotApiBaseUrl ?? options.env?.COPILOT_API_BASE_URL ?? DEFAULT_COPILOT_API_BASE_URL2
@@ -1640,6 +1678,24 @@ async function safeResponseText2(response) {
1640
1678
  function trimTrailingSlash2(value) {
1641
1679
  return value.replace(/\/+$/, "");
1642
1680
  }
1681
+ function modelIdsFromResponse(body) {
1682
+ const record = asRecord2(body);
1683
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];
1684
+ const seen = /* @__PURE__ */ new Set();
1685
+ const ids = [];
1686
+ for (const model of data) {
1687
+ const id = asRecord2(model).id;
1688
+ if (typeof id !== "string" || id.length === 0 || seen.has(id)) {
1689
+ continue;
1690
+ }
1691
+ seen.add(id);
1692
+ ids.push(id);
1693
+ }
1694
+ return ids;
1695
+ }
1696
+ function asRecord2(value) {
1697
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1698
+ }
1643
1699
  function withRuntimeEnv(args) {
1644
1700
  return { ...args, env: process.env };
1645
1701
  }
@@ -1651,12 +1707,14 @@ OpenAI-compatible proxy for GitHub Copilot.
1651
1707
  Usage:
1652
1708
  hoopilot [serve] [options]
1653
1709
  hoopilot login [options]
1710
+ hoopilot models [options]
1654
1711
  hoopilot update
1655
1712
  npx @openhoo/hoopilot [options]
1656
1713
 
1657
1714
  Commands:
1658
1715
  serve Start the proxy server (default)
1659
1716
  login Sign in through GitHub OAuth in a browser and verify Copilot access
1717
+ models List available GitHub Copilot model IDs
1660
1718
  update, upgrade Update hoopilot to the latest release
1661
1719
 
1662
1720
  Options:
@@ -1692,6 +1750,7 @@ if (import.meta.main) {
1692
1750
  export {
1693
1751
  main,
1694
1752
  parseArgs,
1753
+ runModels,
1695
1754
  verifyCopilotOAuthToken
1696
1755
  };
1697
1756
  //# sourceMappingURL=cli.js.map