@oh-my-pi/pi-ai 18.1.14 → 18.1.16

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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.16] - 2026-09-09
6
+
7
+ ### Fixed
8
+
9
+ - Codex SSE streams that end without a terminal completion event now retry when replay-safe and remain transient errors when partial output prevents replay ([#11349](https://github.com/can1357/oh-my-pi/issues/11349)).
10
+
11
+ ## [18.1.15] - 2026-09-08
12
+
13
+ ### Fixed
14
+
15
+ - GitHub Copilot sign-in keeps the GitHub-owned Copilot CLI OAuth app on Enterprise domains: private instances run their own OAuth registry and reject the github.com-registered OpenCode client, which is now used only for public github.com sign-ins ([#11285](https://github.com/can1357/oh-my-pi/pull/11285) by [@H4vC](https://github.com/H4vC))
16
+ - GitHub Copilot sign-in uses the minimal-grant OpenCode OAuth app again (`read:user` only): GitHub renders each app's existing per-user grant on the consent page, so Enterprise organizations that block the Copilot CLI app's broad historic grant can log in as on 18.1.4. API request identity still mimics the Copilot CLI, and tokens minted by either app keep working ([#11280](https://github.com/can1357/oh-my-pi/pull/11280) by [@H4vC](https://github.com/H4vC)).
17
+ - GitHub Copilot plan/model-policy 403s no longer count as credential failures for credential-lifetime decisions: the token is valid, so stored credentials are preserved instead of wiped ([#11280](https://github.com/can1357/oh-my-pi/pull/11280) by [@H4vC](https://github.com/H4vC)).
18
+ - Fixed custom `google-generative-ai` providers failing mid-turn model fallback when Gemini 3 tool calls are replayed without their original thought signature ([#11270](https://github.com/can1357/oh-my-pi/issues/11270)).
19
+ - Fixed Linux `zcode://` (Z.AI coding-plan) OAuth sign-in hanging at the paste prompt after a successful browser authorization: native scheme registration now runs `update-desktop-database`, so `xdg-desktop-portal-gtk` resolves the handler on the first attempt instead of reporting "No Apps available" and no longer routes the callback to a stale transaction directory ([#11279](https://github.com/can1357/oh-my-pi/issues/11279)).
20
+
5
21
  ## [18.1.14] - 2026-09-07
6
22
 
7
23
  ### Fixed
@@ -87,6 +87,15 @@ export declare function isFastModeUnsupported(error: unknown): boolean;
87
87
  * fail identically) nor surface as an auth failure.
88
88
  */
89
89
  export declare function isClinePassSurfaceGateMessage(errorMessage: string | undefined): boolean;
90
+ /**
91
+ * GitHub Copilot 403s are plan/model-policy/org denials against a valid token —
92
+ * never a revoked credential (those arrive as 401). Wiping stored credentials
93
+ * on them hides the whole provider from `/model` and forces a pointless
94
+ * re-login, so credential-lifetime decisions must exempt them even though
95
+ * they still classify as `Flag.AuthFailed` (issue #11275). Mirrors the 403
96
+ * contract in `rewriteCopilotError` (`utils/http-inspector.ts`).
97
+ */
98
+ export declare function isGitHubCopilotPolicyDenial(provider: string | undefined, status: number | undefined, errorMessage: string | undefined): boolean;
90
99
  export declare function classifyMessage(message: {
91
100
  api?: Api;
92
101
  provider?: string;
@@ -15,7 +15,7 @@ type GitHubCopilotLoginOptions = {
15
15
  };
16
16
  /**
17
17
  * Refresh GitHub Copilot token.
18
- * GitHub OAuth tokens from both the former OpenCode app and the Copilot CLI app
18
+ * GitHub OAuth tokens from both the OpenCode app and the Copilot CLI app
19
19
  * remain directly usable, so existing logins need no token exchange or migration.
20
20
  */
21
21
  export declare function refreshGitHubCopilotToken(refreshToken: string, enterpriseDomain?: string, apiEndpoint?: string): OAuthCredentials;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.1.14",
3
+ "version": "18.1.16",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -124,11 +124,11 @@
124
124
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
125
125
  },
126
126
  "dependencies": {
127
- "@oh-my-pi/omptype": "18.1.14",
128
- "@oh-my-pi/pi-catalog": "18.1.14",
129
- "@oh-my-pi/pi-natives": "18.1.14",
130
- "@oh-my-pi/pi-utils": "18.1.14",
131
- "@oh-my-pi/pi-wire": "18.1.14"
127
+ "@oh-my-pi/omptype": "18.1.16",
128
+ "@oh-my-pi/pi-catalog": "18.1.16",
129
+ "@oh-my-pi/pi-natives": "18.1.16",
130
+ "@oh-my-pi/pi-utils": "18.1.16",
131
+ "@oh-my-pi/pi-wire": "18.1.16"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/bun": "^1.3.14"
@@ -694,6 +694,26 @@ export function isClinePassSurfaceGateMessage(errorMessage: string | undefined):
694
694
  return errorMessage !== undefined && CLINE_PASS_SURFACE_GATE_PATTERN.test(errorMessage);
695
695
  }
696
696
 
697
+ const GITHUB_COPILOT_POLICY_DENIAL_PATTERN = /GitHub Copilot access denied \(HTTP 403\)/;
698
+
699
+ /**
700
+ * GitHub Copilot 403s are plan/model-policy/org denials against a valid token —
701
+ * never a revoked credential (those arrive as 401). Wiping stored credentials
702
+ * on them hides the whole provider from `/model` and forces a pointless
703
+ * re-login, so credential-lifetime decisions must exempt them even though
704
+ * they still classify as `Flag.AuthFailed` (issue #11275). Mirrors the 403
705
+ * contract in `rewriteCopilotError` (`utils/http-inspector.ts`).
706
+ */
707
+ export function isGitHubCopilotPolicyDenial(
708
+ provider: string | undefined,
709
+ status: number | undefined,
710
+ errorMessage: string | undefined,
711
+ ): boolean {
712
+ if (provider !== "github-copilot") return false;
713
+ if (status === 403) return true;
714
+ return errorMessage !== undefined && GITHUB_COPILOT_POLICY_DENIAL_PATTERN.test(errorMessage);
715
+ }
716
+
697
717
  export function classifyMessage(message: {
698
718
  api?: Api;
699
719
  provider?: string;
@@ -2147,6 +2147,17 @@ class CodexStreamProcessor {
2147
2147
  firstTokenTime = this.#handleStreamEvent(rawEvent, firstTokenTime);
2148
2148
  if (this.runtime.sawTerminalEvent) break;
2149
2149
  }
2150
+ if (!this.runtime.sawTerminalEvent) {
2151
+ CODEX_DEBUG &&
2152
+ logger.debug("[codex] codex stream ended unexpectedly", {
2153
+ transport: this.runtime.transport,
2154
+ terminalEventSeen: false,
2155
+ unexpectedStreamEnd: true,
2156
+ sentTurnStateHeader: Boolean(this.requestContext.turnState.value),
2157
+ sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag),
2158
+ });
2159
+ throw new CodexProviderStreamError("Codex stream ended before terminal completion event", true);
2160
+ }
2150
2161
  return { firstTokenTime };
2151
2162
  } catch (error) {
2152
2163
  const recovered = await this.#recoverStreamError(error);
@@ -2952,21 +2963,6 @@ class CodexStreamProcessor {
2952
2963
  if (this.options?.signal?.aborted) {
2953
2964
  throw new AIError.AbortError();
2954
2965
  }
2955
- if (!this.runtime.sawTerminalEvent) {
2956
- if (this.requestContext.websocketState) {
2957
- resetCodexWebSocketAppendState(this.requestContext.websocketState);
2958
- this.requestContext.websocketState.modelsEtag = undefined;
2959
- }
2960
- CODEX_DEBUG &&
2961
- logger.debug("[codex] codex stream ended unexpectedly", {
2962
- transport: this.runtime.transport,
2963
- terminalEventSeen: this.runtime.sawTerminalEvent,
2964
- unexpectedStreamEnd: true,
2965
- sentTurnStateHeader: Boolean(this.requestContext.turnState.value),
2966
- sentModelsEtagHeader: Boolean(this.requestContext.websocketState?.modelsEtag),
2967
- });
2968
- throw new CodexProviderStreamError("Codex stream ended before terminal completion event", false);
2969
- }
2970
2966
  if (output.stopReason === "aborted" || output.stopReason === "error") {
2971
2967
  throw new CodexProviderStreamError("Codex response failed", false);
2972
2968
  }
@@ -1,5 +1,15 @@
1
1
  /**
2
- * GitHub Copilot OAuth flow using the official Copilot CLI app.
2
+ * GitHub Copilot OAuth flow (OpenCode OAuth app, minimal read:user grant).
3
+ *
4
+ * The device flow intentionally uses the OpenCode app identity rather than
5
+ * the official Copilot CLI app: GitHub renders each app's existing per-user
6
+ * grant (Existing access) on the consent page, and Enterprise orgs that
7
+ * restrict OAuth apps block the CLI app's broad historic grant
8
+ * (repo/gist/codespace) no matter what scope this request asks for
9
+ * (issue #11275). API request identity still mimics the Copilot CLI via
10
+ * COPILOT_API_HEADERS. Enterprise domains keep the GitHub-owned Copilot CLI
11
+ * client: private instances run their own OAuth registry and reject the
12
+ * github.com-registered OpenCode client.
3
13
  */
4
14
  import { scheduler } from "node:timers/promises";
5
15
  import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
@@ -15,7 +25,18 @@ import * as AIError from "../../error";
15
25
  import type { FetchImpl } from "../../types";
16
26
  import type { OAuthController, OAuthCredentials } from "./types";
17
27
 
18
- const CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
28
+ const OPENCODE_CLIENT_ID = "Ov23li8tweQw6odWQebz";
29
+ const COPILOT_CLI_CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
30
+
31
+ /**
32
+ * OAuth client for the device flow. Public github.com uses the minimal-grant
33
+ * OpenCode app (narrow consent, issue #11275); private GitHub Enterprise
34
+ * instances run their own OAuth registry, which does not know that github.com
35
+ * registration, so they keep the GitHub-owned Copilot CLI client.
36
+ */
37
+ function resolveOAuthClientId(domain: string): string {
38
+ return isPublicGitHubHost(domain) ? OPENCODE_CLIENT_ID : COPILOT_CLI_CLIENT_ID;
39
+ }
19
40
  const OAUTH_SCOPE = "read:user";
20
41
  const OAUTH_HEADERS = {
21
42
  Accept: "application/json",
@@ -82,7 +103,7 @@ async function startDeviceFlow(domain: string, fetchImpl: FetchImpl): Promise<De
82
103
  method: "POST",
83
104
  headers: OAUTH_HEADERS,
84
105
  body: new URLSearchParams({
85
- client_id: CLIENT_ID,
106
+ client_id: resolveOAuthClientId(domain),
86
107
  scope: OAUTH_SCOPE,
87
108
  }),
88
109
  },
@@ -156,7 +177,7 @@ async function pollForGitHubAccessToken(
156
177
  method: "POST",
157
178
  headers: OAUTH_HEADERS,
158
179
  body: new URLSearchParams({
159
- client_id: CLIENT_ID,
180
+ client_id: resolveOAuthClientId(domain),
160
181
  device_code: deviceCode,
161
182
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
162
183
  }),
@@ -207,7 +228,7 @@ const FAR_FUTURE_MS = Date.now() + 10 * 365.25 * 24 * 60 * 60 * 1000;
207
228
 
208
229
  /**
209
230
  * Refresh GitHub Copilot token.
210
- * GitHub OAuth tokens from both the former OpenCode app and the Copilot CLI app
231
+ * GitHub OAuth tokens from both the OpenCode app and the Copilot CLI app
211
232
  * remain directly usable, so existing logins need no token exchange or migration.
212
233
  */
213
234
  export function refreshGitHubCopilotToken(
@@ -335,8 +356,8 @@ export async function loginGitHubCopilot(options: GitHubCopilotLoginOptions): Pr
335
356
 
336
357
  const apiEndpoint = await discoverGitHubCopilotApiEndpoint(githubAccessToken, fetchImpl);
337
358
 
338
- // Keep storing the GitHub token directly so credentials minted by the former
339
- // OpenCode OAuth app remain valid alongside new Copilot CLI app logins.
359
+ // Keep storing the GitHub token directly so credentials minted by the
360
+ // Copilot CLI OAuth app remain valid alongside new OpenCode app logins.
340
361
  const credentials: OAuthCredentials = {
341
362
  refresh: githubAccessToken,
342
363
  access: githubAccessToken,