From d4956e6de23f4815657d9d870fa7a342443b3129 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 28 Jul 2026 03:35:46 +0000 Subject: [PATCH] fix(coding-agent): restore isRetryableAssistantError in legacy pi-ai shim Historical @earendil-works/pi-ai exports isRetryableAssistantError from its package root (utils/retry.ts), but OMP's legacy-pi-ai-shim re-exports @oh-my-pi/pi-ai, whose root never carried the symbol. Plugins importing it (e.g. @router-for-me/pi-cliproxyapi-provider >= 1.4.9) failed Bun's static named-export validation, rolling back install. Port the upstream transient-error classifier into the shim, preserving the provider-error wording tables so legacy retry semantics match. Fixes #6847 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/extensibility/legacy-pi-ai-shim.ts | 29 ++++++++++++++++++- .../legacy-pi-ai-type-remap.test.ts | 23 +++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8379caed6..4d9f7f02c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed legacy extension plugin validation failing with `Export named 'isRetryableAssistantError' not found in module '.../legacy-pi-ai-shim.ts'` when an extension imports `isRetryableAssistantError` from `@earendil-works/pi-ai` / `@oh-my-pi/pi-ai` (e.g. `@router-for-me/pi-cliproxyapi-provider` >= 1.4.9). Historical pi-ai exports this transient-error classifier from its package root, but OMP's legacy `pi-ai` root shim never bridged it; the shim now ships a compatibility implementation matching the upstream provider-error wording tables. ([#6847](https://github.com/can1357/oh-my-pi/issues/6847)) + ## [17.1.7] - 2026-07-27 ### Fixed diff --git a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts index f7bb39c5a..3a690647d 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts @@ -19,7 +19,7 @@ * `types.ts` via the `export *` below — pi-ai still exports both as types, * only the runtime `Type` builder and `StringEnum()` helper were removed. */ -import type { Api, Model } from "@oh-my-pi/pi-ai"; +import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai"; import type { Effort } from "@oh-my-pi/pi-catalog/effort"; import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking"; import { @@ -80,6 +80,33 @@ export function clampThinkingLevel(model: Model, level: return clampThinkingLevelForModel(model, level) ?? "off"; } +/** + * Provider-error classification patterns ported verbatim from historical pi-ai + * (`@earendil-works/pi-ai` `utils/retry.ts`). Legacy extensions call + * {@link isRetryableAssistantError} to decide whether to restart a failed + * assistant turn, so the wording tables must match the upstream semantics they + * were authored against rather than OMP's own `Error`-based classifiers. + */ +const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = + /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i; +const RETRYABLE_PROVIDER_ERROR_PATTERN = + /overloaded|rate.?limit|too many requests|429|500|502|503|504|524|service.?unavailable|server.?error|internal.?error|provider.?returned.?error|network.?error|connection.?error|connection.?refused|connection.?lost|other side closed|fetch failed|getaddrinfo|ENOTFOUND|EAI_AGAIN|upstream.?connect|reset before headers|socket hang up|socket connection was closed|timed? out|timeout|terminated|websocket.?closed|websocket.?error|ended without|stream ended before message_stop|stream ended before a terminal response event|http2 request did not get a response|retry delay|you can retry your request|try your request again|please retry your request|ResourceExhausted/i; + +/** + * Compatibility implementation of historical pi-ai's `isRetryableAssistantError`. + * + * Classifies whether a failed assistant message looks like a transient provider + * or transport error so legacy extensions can decide if the last assistant turn + * should be restarted. Account/quota limits are treated as non-retryable. This + * does not implement any retry policy; callers own budget, backoff, and reporting. + */ +export function isRetryableAssistantError(message: AssistantMessage): boolean { + if (message.stopReason !== "error" || !message.errorMessage) return false; + const errorMessage = message.errorMessage; + if (NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN.test(errorMessage)) return false; + return RETRYABLE_PROVIDER_ERROR_PATTERN.test(errorMessage); +} + export * from "@oh-my-pi/pi-ai"; /** * Compatibility re-exports for catalog symbols that pi-ai historically exposed diff --git a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts index 4eb64ddbd..dfc8fa984 100644 --- a/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts +++ b/packages/coding-agent/test/extensibility/legacy-pi-ai-type-remap.test.ts @@ -220,6 +220,29 @@ describe("legacy-pi @(scope)/pi-ai root `Type` remap (issue #1437)", () => { expect(loaded.schema.safeParse("blue").success).toBe(false); expect(loaded.schema.toJSON?.()?.description).toBe("primary colors"); }); + + it("exports isRetryableAssistantError for legacy retry classification (issue #6847)", async () => { + // `@earendil-works/pi-ai@0.82.x` exports isRetryableAssistantError from its + // package root (utils/retry.js). Plugins such as + // `@router-for-me/pi-cliproxyapi-provider` (>=1.4.9) import it, so a missing + // shim export surfaced as a plain + // `Export named 'isRetryableAssistantError' not found` at validation time. + const loaded = (await loadLegacyPiModule( + await writeFixtureExtension( + [ + 'import { isRetryableAssistantError } from "@earendil-works/pi-ai";', + 'const err = errorMessage => ({ role: "assistant", stopReason: "error", errorMessage });', + 'export const transient = isRetryableAssistantError(err("upstream connect error"));', + 'export const quota = isRetryableAssistantError(err("insufficient_quota"));', + 'export const ok = isRetryableAssistantError({ role: "assistant", stopReason: "stop" });', + ].join("\n"), + ), + )) as { transient: boolean; quota: boolean; ok: boolean }; + + expect(loaded.transient).toBe(true); + expect(loaded.quota).toBe(false); + expect(loaded.ok).toBe(false); + }); }); describe("legacy pi package root remaps (issue #1474)", () => {