Merge PR #6848: fix(coding-agent): restore isRetryableAssistantError in legacy pi-ai shim (@roboomp)

This commit is contained in:
can1357
2026-07-28 10:59:35 +02:00
3 changed files with 54 additions and 1 deletions
+3
View File
@@ -25,6 +25,9 @@
### Fixed
- Fixed `omp plugin install` of legacy pi extensions failing Bun's static named-export validation on `isContextOverflow` (e.g. `pi-blackhole`). The `@oh-my-pi/pi-ai` root barrel dropped upstream `@earendil-works/pi-ai`'s root re-export, so `legacy-pi-ai-shim.ts` (which backs both on-disk and `omp-legacy-pi-bundled:` virtual resolution) never surfaced it. The shim now bridges every upstream-root runtime symbol that still exists in omp — `isContextOverflow` (from `@oh-my-pi/pi-ai/error`) and `parseJsonWithRepair`/`parseStreamingJson`/`repairJson` (from `@oh-my-pi/pi-utils`) ([#6859](https://github.com/can1357/oh-my-pi/issues/6859)).
### 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
@@ -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<TApi extends Api>(model: Model<TApi>, 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
@@ -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)", () => {