@oh-my-pi/pi-ai 17.2.9 → 17.2.11

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,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.11] - 2026-08-07
6
+
7
+ ### Breaking Changes
8
+
9
+ - Fixed handling of GitHub Copilot's model_not_available_for_integrator error to prevent unnecessary retries, preserving the actionable available models list.
10
+
11
+ ### Added
12
+
13
+ - Added support for reporting Cursor personal monthly USD quotas and remaining balances, labeled by verified profile email accounts.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed an issue where ANTHROPIC_BASE_URL was ignored for Anthropic chat requests, ensuring requests are routed to the configured host and forwarding ANTHROPIC_CUSTOM_HEADERS to non-official gateways.
18
+ - Fixed an issue where a legacy pre-organization login credential could persist and cause a permanent error row in omp usage even after a successful organization-scoped re-login.
19
+ - Fixed an issue where lazy provider streams (including Amazon Bedrock, Google, Cursor, Devin, and Ollama) ignored model-specific idle timeouts, which previously caused healthy but slow reasoning turns to prematurely time out.
20
+ - Improved error classification for Simplified Chinese quota-exhaustion and rate-limit messages, ensuring affected credentials are correctly rotated or backed off instead of being treated as unknown errors.
21
+ - Classified subscription and plan-cap 429 responses as rotatable usage limits rather than transient rate-limit throttles, enabling smoother credential rotation.
22
+
23
+ ## [17.2.10] - 2026-08-06
24
+
25
+ ### Breaking Changes
26
+
27
+ - Removed the `zod` dependency and `z`/`ZodType` re-exports. Tool schemas now use `omptype` `type()` schemas, with Zod-style authoring still available via `@oh-my-pi/omptype/zod`.
28
+
5
29
  ## [17.2.9] - 2026-08-05
6
30
 
7
31
  ### Fixed
package/README.md CHANGED
@@ -94,21 +94,18 @@ npm install @oh-my-pi/pi-ai
94
94
  ## Quick Start
95
95
 
96
96
  ```typescript
97
- import { z, getModel, stream, complete, Context, Tool } from "@oh-my-pi/pi-ai";
97
+ import { getModel, stream, complete, Context, Tool, type } from "@oh-my-pi/pi-ai";
98
98
 
99
99
  // Fully typed with auto-complete support for both providers and models
100
100
  const model = getModel("openai", "gpt-4o-mini");
101
101
 
102
- // Define tools with Zod schemas for type safety and validation
102
+ // Define tools with omptype schemas for type safety and validation
103
103
  const tools: Tool[] = [
104
104
  {
105
105
  name: "get_time",
106
106
  description: "Get the current time",
107
- parameters: z.object({
108
- timezone: z
109
- .string()
110
- .optional()
111
- .describe("Optional timezone (e.g., America/New_York)"),
107
+ parameters: type({
108
+ "timezone?": type("string").describe("Optional timezone (e.g., America/New_York)"),
112
109
  }),
113
110
  },
114
111
  ];
@@ -221,31 +218,30 @@ for (const block of response.content) {
221
218
 
222
219
  ## Tools
223
220
 
224
- Tools enable LLMs to interact with external systems. This library uses **Zod** schemas for type-safe tool definitions with automatic validation. Schemas are converted to JSON Schema for providers as needed.
221
+ Tools enable LLMs to interact with external systems. Omptype schemas provide type-safe definitions, runtime validation, and JSON Schema conversion for providers.
225
222
 
226
223
  ### Defining Tools
227
224
 
228
225
  ```typescript
229
- import { z, Tool } from "@oh-my-pi/pi-ai";
226
+ import { type Tool, type } from "@oh-my-pi/pi-ai";
230
227
 
231
- // Define tool parameters with Zod
232
228
  const weatherTool: Tool = {
233
229
  name: "get_weather",
234
230
  description: "Get current weather for a location",
235
- parameters: z.object({
236
- location: z.string().describe("City name or coordinates"),
237
- units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
231
+ parameters: type({
232
+ location: type("string").describe("City name or coordinates"),
233
+ units: type.enumerated("celsius", "fahrenheit").default("celsius"),
238
234
  }),
239
235
  };
240
236
 
241
237
  const bookMeetingTool: Tool = {
242
238
  name: "book_meeting",
243
239
  description: "Schedule a meeting",
244
- parameters: z.object({
245
- title: z.string().min(1),
246
- startTime: z.string().describe("ISO 8601 date-time"),
247
- endTime: z.string().describe("ISO 8601 date-time"),
248
- attendees: z.array(z.email()).min(1),
240
+ parameters: type({
241
+ title: type("string").atLeastLength(1),
242
+ startTime: type("string").describe("ISO 8601 date-time"),
243
+ endTime: type("string").describe("ISO 8601 date-time"),
244
+ attendees: type("string.email").array().atLeastLength(1),
249
245
  }),
250
246
  };
251
247
  ```
@@ -345,7 +341,7 @@ for await (const event of s) {
345
341
 
346
342
  ### Validating Tool Arguments
347
343
 
348
- When using `agentLoop`, tool arguments are automatically validated against your Zod parameter schemas before execution. If validation fails, the error is returned to the model as a tool result, allowing it to retry.
344
+ When using `agentLoop`, tool arguments are automatically validated against their omptype schemas before execution. Validation failures are returned to the model as tool results so it can retry.
349
345
 
350
346
  When implementing your own tool execution loop with `stream()` or `complete()`, use `validateToolCall` to validate arguments before passing them to your tools:
351
347
 
@@ -65,10 +65,10 @@ export declare function isGrammarError(error: unknown): boolean;
65
65
  */
66
66
  export declare function isFastModeUnsupported(error: unknown): boolean;
67
67
  /**
68
- * GitHub Copilot 400 rejecting a model its own `/models` catalog advertises —
69
- * transient fleet skew, not a malformed request. Reads the structural `code`
70
- * through the SDK/body envelopes, then falls back to the stringified body both
71
- * SDK families put in `message` (shapes drift; the wire text does not).
68
+ * GitHub Copilot 400 `model_not_supported` response for a model advertised by
69
+ * `/models` — transient fleet skew, not a malformed request. Reads the
70
+ * structural `code` through the SDK/body envelopes, then falls back to the
71
+ * stringified body both SDK families put in `message`.
72
72
  */
73
73
  export declare function isCopilotTransientModelError(error: unknown): boolean;
74
74
  export declare function classifyMessage(message: {
@@ -1,5 +1,4 @@
1
1
  export { type Type, type } from "@oh-my-pi/omptype";
2
- export { type ZodType, z } from "zod/v4";
3
2
  export * from "./api-registry.js";
4
3
  export type * from "./auth-broker/index.js";
5
4
  export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server.js";
@@ -12,4 +12,5 @@ export declare function pollCursorAuth(uuid: string, verifier: string): Promise<
12
12
  }>;
13
13
  export declare function loginCursor(onAuthUrl: (url: string) => void, onPollStart?: () => void): Promise<OAuthCredentials>;
14
14
  export declare function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials>;
15
+ export declare function extractCursorAccessTokenUserId(accessToken: string): string | undefined;
15
16
  export declare function isCursorTokenExpiringSoon(token: string, thresholdSeconds?: number): boolean;
@@ -6,6 +6,10 @@ export type * from "./types.js";
6
6
  * Register a custom OAuth provider.
7
7
  */
8
8
  export declare function registerOAuthProvider(provider: OAuthProviderInterface): void;
9
+ /**
10
+ * Remove a custom OAuth provider by ID.
11
+ */
12
+ export declare function unregisterOAuthProvider(id: string): void;
9
13
  /**
10
14
  * Get a custom OAuth provider by ID.
11
15
  */
@@ -4,7 +4,6 @@ import type { Type } from "@oh-my-pi/omptype";
4
4
  import type { DeleteArgs, DeleteResult, DiagnosticsArgs, DiagnosticsResult, GrepArgs, GrepResult, LsArgs, LsResult, McpResult, PiBashExecArgs, PiBashExecResult, PiEditExecArgs, PiEditExecResult, PiFindExecArgs, PiFindExecResult, PiGrepExecArgs, PiGrepExecResult, PiLsExecArgs, PiLsExecResult, PiReadExecArgs, PiReadExecResult, PiWriteExecArgs, PiWriteExecResult, ReadArgs, ReadResult, ShellArgs, ShellResult, WriteArgs, WriteResult } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
5
5
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
6
6
  import type { Api, FetchImpl, Model, Provider, ThinkingBudgets, Usage } from "@oh-my-pi/pi-catalog/types";
7
- import type { ZodType, z } from "zod/v4";
8
7
  import type { ApiKey } from "./auth-retry.js";
9
8
  import type { BedrockOptions } from "./providers/amazon-bedrock.js";
10
9
  import type { AnthropicOptions } from "./providers/anthropic.js";
@@ -650,8 +649,10 @@ export interface DeveloperMessage {
650
649
  providerPayload?: ProviderPayload;
651
650
  timestamp: number;
652
651
  }
652
+ /** How an automatic retry recovered or ultimately settled a failed attempt. */
653
653
  export type AssistantRetryRecoveryKind = "credential" | "model" | "wait" | "plain";
654
- export interface AssistantRetryRecovery {
654
+ /** Persisted presentation state for an assistant error superseded by an automatic retry saga. */
655
+ export type AssistantRetryRecovery = {
655
656
  kind: "auto-retry";
656
657
  status: "recovered";
657
658
  attempt: number;
@@ -664,7 +665,13 @@ export interface AssistantRetryRecovery {
664
665
  provider: string;
665
666
  model: string;
666
667
  };
667
- }
668
+ } | {
669
+ kind: "auto-retry";
670
+ status: "superseded";
671
+ attempt: number;
672
+ recovery: AssistantRetryRecoveryKind;
673
+ note: string;
674
+ };
668
675
  export interface ContextSnapshot {
669
676
  promptTokens: number;
670
677
  nonMessageTokens: number;
@@ -928,12 +935,12 @@ export type TJsonSchema = Record<string, unknown>;
928
935
  /**
929
936
  * Schema type accepted by the {@link Tool} interface.
930
937
  *
931
- * Canonical authoring uses Zod or ArkType. Extension compat may supply a JSON
932
- * Schema object (including TypeBox static schema objects).
938
+ * Canonical authoring uses ArkType. Extension compat may supply a JSON Schema
939
+ * object (including TypeBox static schema objects).
933
940
  */
934
- export type TSchema = ZodType | Type | TJsonSchema;
941
+ export type TSchema = Type | TJsonSchema;
935
942
  /** Resolve parameter types for tool execution / handlers. */
936
- export type Static<S> = S extends ZodType ? z.infer<S> : S extends Type ? S["infer"] : S extends {
943
+ export type Static<S> = S extends Type ? S["infer"] : S extends {
937
944
  static: infer T;
938
945
  } ? T : unknown;
939
946
  export interface ToolCallExample<TArgs = Record<string, unknown>> {
@@ -1,3 +1,4 @@
1
1
  import type { UsageProvider, UsageReport } from "../usage.js";
2
+ export declare function parseCursorIndividualUsage(payload: unknown, fetchedAt?: number): UsageReport | null;
2
3
  export declare function parseCursorUsage(payload: unknown, fetchedAt?: number): UsageReport | null;
3
4
  export declare const cursorUsageProvider: UsageProvider;
@@ -38,11 +38,10 @@ export declare function finalizeErrorMessage(error: unknown, rawRequestDump: Raw
38
38
  * Rewrite error message for GitHub Copilot request failures.
39
39
  * Must run AFTER finalizeErrorMessage since it replaces the message entirely.
40
40
  *
41
- * 400 model-unavailable = Copilot fleet skew. A model that `/models` advertises
42
- * (claude-sonnet-4.6, claude-opus-4.6, gpt-5.4, gpt-5.3-codex, ...)
43
- * flaps between 200 and 400 because only part of Copilot's fleet has it
44
- * in the integrator allowlist. After the in-request retry exhausts,
45
- * surface guidance rather than the raw error.
41
+ * 400 `model_not_supported` = Copilot fleet skew. A model that `/models`
42
+ * advertises can flap between 200 and 400 because only part of
43
+ * Copilot's fleet has it in the integrator allowlist. After the
44
+ * in-request retry exhausts, surface guidance rather than the raw error.
46
45
  * 401 = token invalid/expired → credential removal is safe, prompt re-login.
47
46
  * 403 = token valid but access denied (plan, model policy, org restriction) →
48
47
  * do NOT reuse the auth-failed string (which triggers credential removal).
@@ -1,9 +1,8 @@
1
1
  import { isCopilotTransientModelError } from "../error/flags.js";
2
2
  export { isCopilotTransientModelError };
3
3
  /**
4
- * Wrap an initial Copilot request so transient model-availability 400s
5
- * (`model_not_supported`, `model_not_available_for_integrator`) are retried a
6
- * small number of times. No-op for non-Copilot providers.
4
+ * Wrap an initial Copilot request so transient `model_not_supported` 400s are
5
+ * retried a small number of times. No-op for non-Copilot providers.
7
6
  *
8
7
  * The callback **MUST** create a fresh in-flight request each invocation — a
9
8
  * once-consumed AsyncIterable cannot be re-iterated.
@@ -12,4 +12,3 @@ export * from "./strict-tool-validation.js";
12
12
  export * from "./types.js";
13
13
  export * from "./typescript.js";
14
14
  export * from "./wire.js";
15
- export * from "./zod-decontaminate.js";
@@ -1,76 +1,36 @@
1
1
  /**
2
2
  * Compute the wire (JSON Schema) representation of a tool's parameters.
3
3
  *
4
- * Tools may author parameters in three shapes:
5
- * 1. Zod (canonical) converted to JSON Schema on demand.
6
- * 2. ArkType converted to JSON Schema via its native `toJsonSchema`.
7
- * 3. TypeBox / plain JSON Schema (legacy + extension compat) — upgraded to
8
- * draft 2020-12 without converting.
9
- *
10
- * All three are normalized at the boundary so providers and validators see the same
11
- * JSON Schema dialect.
4
+ * Tools may author parameters as ArkType schemas or legacy TypeBox / plain JSON
5
+ * Schema documents. Both are normalized at the boundary so providers and
6
+ * validators see the same JSON Schema dialect.
12
7
  */
13
8
  import type { Type } from "@oh-my-pi/omptype";
14
- import { type ZodType } from "zod/v4";
15
9
  import type { Tool } from "../../types.js";
16
- /**
17
- * True when `value` is a live Zod schema instance.
18
- *
19
- * The check is stricter than "has a `_zod` property" because a JSON
20
- * round-trip preserves the `_zod` key as a plain object and would otherwise
21
- * fool the predicate — see issue #1101, where MCP servers ship
22
- * `JSON.stringify(zodSchemaInstance)` as a tool's `inputSchema` and the
23
- * resulting plain object then explodes `z.toJSONSchema` because the prototype
24
- * (and every Zod parsing method) is gone.
25
- *
26
- * Live Zod instances always carry a `.parse` function on the prototype;
27
- * impostors do not.
28
- */
29
- export declare function isZodSchema(value: unknown): value is ZodType;
30
10
  /**
31
11
  * True when `value` is a live ArkType schema instance.
32
12
  *
33
13
  * ArkType schemas are callable functions carrying `toJsonSchema`/`assert`
34
- * methods. Zod v4 instances are non-callable objects (keyed off `_zod`), and
35
- * raw JSON Schema is a plain object — the three are disjoint. We deliberately
36
- * avoid the Standard Schema `~standard` marker because Zod v4 implements it too.
14
+ * methods, while raw JSON Schema is a plain object.
37
15
  */
38
16
  export declare function isArkSchema(value: unknown): value is Type;
39
17
  /**
40
- * Normalize `{}` (empty JSON Schema = `z.unknown()` / unconstrained value) to
41
- * boolean `true` in every schema-valued position. JSON Schema draft 2020-12
42
- * §4.3.1: `{}` and `true` are semantically equivalent ("any JSON value").
43
- * Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
44
- * "generate an empty object" rather than "any JSON value", causing open-typed
45
- * fields like `extra.title` (from `z.record(z.string(), z.unknown())`) to
46
- * always emit `{}` instead of the intended string/number/etc. (issue #1179).
18
+ * Normalize `{}` (an unconstrained schema) to boolean `true` in every
19
+ * schema-valued position. JSON Schema draft 2020-12 §4.3.1 defines them as
20
+ * semantically equivalent. Grammar-constrained samplers often treat the object
21
+ * form as "generate an empty object" rather than "any JSON value".
47
22
  *
48
- * Mutates in place. Provider-agnostic applied to every tool wire schema so
49
- * Anthropic, Google, OpenAI, Ollama, Bedrock, and Cursor all see the
50
- * normalized form, regardless of whether the source was Zod or TypeBox.
23
+ * Mutates in place and applies to every tool wire schema.
51
24
  */
52
25
  export declare function normalizeEmptySchemas(node: unknown): void;
53
- /** Convert a Zod schema into the JSON Schema shape providers consume. */
54
- export declare function zodToWireSchema(schema: ZodType): Record<string, unknown>;
55
26
  /**
56
27
  * Convert an ArkType schema into the JSON Schema shape providers consume.
57
- *
58
- * Mirrors {@link zodToWireSchema}: emit draft-2020-12, drop the `$schema`
59
- * metadata, run the JSON-schema post-process (NOT the Zod-only cleanup), then
60
- * close declared objects so the wire is `additionalProperties: false` like Zod.
61
- *
62
- * The `fallback` degrades any un-emittable node (a `.narrow()` predicate or a
63
- * morph) to its underlying base schema instead of throwing — matching Zod,
64
- * whose `.refine()`/`.transform()` likewise never appear in the wire schema.
65
28
  */
66
29
  export declare function arkToWireSchema(schema: Type): Record<string, unknown>;
67
30
  /**
68
31
  * Resolve a tool's parameters to a JSON Schema object suitable for sending
69
- * over the wire. Zod schemas are converted (and cached); legacy TypeBox / raw
70
- * JSON Schema parameters are upgraded to draft 2020-12 (and cached).
71
- *
72
- * Zod schemas also receive Zod-artifact cleanup; both branches normalize
73
- * schema-valued positions and nullable scalar unions.
32
+ * over the wire. ArkType schemas are converted and cached; legacy TypeBox /
33
+ * raw JSON Schema parameters are upgraded to draft 2020-12 and cached.
74
34
  */
75
35
  export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
76
36
  /**
@@ -1,22 +1,10 @@
1
1
  /**
2
2
  * Tool-call argument validation pipeline.
3
3
  *
4
- * Tools may declare their parameters as either Zod schemas (canonical) or
5
- * plain JSON Schema (legacy / extensions). This module is the single
6
- * entrypoint the agent calls before dispatching a tool — it:
7
- *
8
- * 1. Builds (or fetches from cache) a `ValidationContext` for the tool —
9
- * the Zod schema if available plus the equivalent wire JSON Schema, or
10
- * just the JSON Schema for non-Zod tools.
11
- * 2. Normalizes LLM quirks (null / "null" → omit-or-default substitution)
12
- * against the JSON Schema before validation.
13
- * 3. Validates with the Zod or JSON-Schema validator.
14
- * 4. On failure, walks the resulting issues and coerces common LLM type
15
- * drift (JSON-stringified values, boolean/number/string scalar drift),
16
- * drops unrecognized keys, and retries up to `MAX_COERCION_PASSES` times.
17
- * 5. Throws a formatted error if reconciliation fails; otherwise returns
18
- * the parsed arguments with original unknown root fields preserved (so
19
- * hallucinated top-level keys still surface to the caller).
4
+ * Tools may declare ArkType schemas or plain JSON Schema. This module builds a
5
+ * cached validation context, normalizes common LLM quirks against the wire
6
+ * schema, validates, performs conservative schema-directed coercions, and
7
+ * returns parsed arguments while preserving unknown root fields.
20
8
  *
21
9
  * The goal is to be conservative: every coercion is a structural rewrite that
22
10
  * keeps the schema in charge of acceptance — we never invent values, only
@@ -32,10 +20,8 @@ import type { Tool, ToolCall } from "../types.js";
32
20
  */
33
21
  export declare function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"];
34
22
  /**
35
- * Validates tool call arguments against the tool's schema (Zod or plain JSON
36
- * Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
37
- * containers, null/invalid-empty-string-for-optional, null-for-default) before
38
- * declaring failure.
23
+ * Validates tool call arguments against an ArkType or plain JSON Schema schema.
24
+ * Applies conservative LLM-quirk normalization before declaring failure.
39
25
  *
40
26
  * @throws Error with a formatted message when validation cannot be reconciled.
41
27
  */
@@ -17,6 +17,15 @@ interface OpenAIResponsesReplaySanitizeOptions {
17
17
  supportsImageDetailOriginal?: boolean;
18
18
  supportsComputerUse?: boolean;
19
19
  }
20
+ /**
21
+ * Removes response-only lifecycle status from item types that reject it when replayed as input.
22
+ *
23
+ * Returns the original array when no item needs sanitization.
24
+ */
25
+ export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem extends {
26
+ type?: unknown;
27
+ status?: unknown;
28
+ }>(items: TItem[]): TItem[];
20
29
  export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput;
21
30
  /** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
22
31
  export declare function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.2.9",
4
+ "version": "17.2.11",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,11 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/omptype": "17.2.9",
42
- "@oh-my-pi/pi-catalog": "17.2.9",
43
- "@oh-my-pi/pi-utils": "17.2.9",
44
- "@oh-my-pi/pi-wire": "17.2.9",
45
- "zod": "^4"
41
+ "@oh-my-pi/omptype": "17.2.11",
42
+ "@oh-my-pi/pi-catalog": "17.2.11",
43
+ "@oh-my-pi/pi-utils": "17.2.11",
44
+ "@oh-my-pi/pi-wire": "17.2.11"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@bufbuild/protoc-gen-es": "^2.12.1",
@@ -1374,11 +1374,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1374
1374
  try {
1375
1375
  let hasActiveApiKey = false;
1376
1376
  const activeIdentityKeys = new Set<string>();
1377
+ const activeOAuthCredentials: AuthCredential[] = [];
1377
1378
  for (const row of activeRows) {
1378
1379
  if (row.credential.type === "api_key") {
1379
1380
  hasActiveApiKey = true;
1380
1381
  continue;
1381
1382
  }
1383
+ activeOAuthCredentials.push(row.credential);
1382
1384
  const identityKey = resolveCredentialIdentityKey(provider, row.credential);
1383
1385
  if (identityKey) activeIdentityKeys.add(identityKey);
1384
1386
  }
@@ -1393,7 +1395,22 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
1393
1395
  const identityKey = resolveRowCredentialIdentityKey(provider, row);
1394
1396
  if (identityKey && activeIdentityKeys.has(identityKey)) {
1395
1397
  this.#hardDeleteStmt.run(row.id);
1398
+ continue;
1396
1399
  }
1400
+ // Exact key equality misses a tombstone whose key predates a format
1401
+ // the active row now uses (pre-org `<b>` vs `<b>|org:<o>`). An active
1402
+ // credential that WOULD have replaced this row had it still been
1403
+ // active supersedes its tombstone too, so mirror the replacement
1404
+ // matcher rather than restating a weaker rule. The one-way upgrade
1405
+ // and shared-workspace guards in matchesReplacementCredential carry
1406
+ // over, so this never over-deletes another member's or subscription's
1407
+ // row.
1408
+ const disabledCredential = deserializeCredential(row);
1409
+ if (disabledCredential === null) continue;
1410
+ const superseded = activeOAuthCredentials.some(active =>
1411
+ matchesReplacementCredential(provider, disabledCredential, identityKey, active),
1412
+ );
1413
+ if (superseded) this.#hardDeleteStmt.run(row.id);
1397
1414
  }
1398
1415
  } catch {
1399
1416
  // Best-effort cleanup; don't let it break the main operation
@@ -113,17 +113,15 @@ const STALE_RESPONSE_ITEM_DETAIL_PATTERN = /not[ _]?found|invalid|expired|stale|
113
113
  export const LLAMA_CPP_TOOL_CALL_PARSE_PATTERN =
114
114
  /failed to parse tool call arguments as json|\[json\.exception\.parse_error\.101\]/i;
115
115
 
116
- // Copilot fleet skew: HTTP 400 rejecting a model that `/models` advertised on
117
- // the very same host. Two codes appear in the wild — `model_not_supported`
118
- // (per-OAuth-client rollout gap) and `model_not_available_for_integrator`
119
- // (replicas whose integrator allowlist predates the model). Both flap
120
- // request-to-request, so a retry usually lands on a backend that has the model.
116
+ // Copilot fleet skew: HTTP 400 `model_not_supported` can reject a model that
117
+ // `/models` advertised on the same host when the request lands on a stale
118
+ // replica. `model_not_available_for_integrator` is deliberately excluded:
119
+ // GitHub also uses it for stable per-integrator entitlement denials and includes
120
+ // that integrator's actionable `Available models` list in the response.
121
121
  const COPILOT_TRANSIENT_MODEL_CODES: Record<string, true> = {
122
122
  model_not_supported: true,
123
- model_not_available_for_integrator: true,
124
123
  };
125
- const COPILOT_MODEL_UNAVAILABLE_PATTERN =
126
- /model_not_supported|model_not_available_for_integrator|not available for integrator/i;
124
+ const COPILOT_TRANSIENT_MODEL_PATTERN = /model_not_supported/i;
127
125
  // Anthropic strict-tool grammar too large / schema too complex (400 invalid_request_error).
128
126
  // Feature-gated deployments (Azure Foundry, Baseten, …) reject `strict: true`
129
127
  // tools outright when the hosted model lacks structured outputs, e.g.
@@ -377,8 +375,8 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
377
375
  kinds |= Flag.StaleResponsesItem;
378
376
  }
379
377
 
380
- // Copilot fleet-skew model rejection is transient.
381
- if (statusClean === 400 && COPILOT_MODEL_UNAVAILABLE_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
378
+ // Copilot's `model_not_supported` fleet-skew rejection is transient.
379
+ if (statusClean === 400 && COPILOT_TRANSIENT_MODEL_PATTERN.test(cleanMessage)) kinds |= Flag.Transient;
382
380
  if (matchesStrictToolsRejection(cleanMessage, statusClean)) kinds |= Flag.Grammar;
383
381
  if (matchesFastModeUnsupported(cleanMessage, statusClean)) kinds |= Flag.FastModeUnsupported;
384
382
  }
@@ -513,10 +511,10 @@ function providerErrorCode(error: object): string | undefined {
513
511
  }
514
512
 
515
513
  /**
516
- * GitHub Copilot 400 rejecting a model its own `/models` catalog advertises —
517
- * transient fleet skew, not a malformed request. Reads the structural `code`
518
- * through the SDK/body envelopes, then falls back to the stringified body both
519
- * SDK families put in `message` (shapes drift; the wire text does not).
514
+ * GitHub Copilot 400 `model_not_supported` response for a model advertised by
515
+ * `/models` — transient fleet skew, not a malformed request. Reads the
516
+ * structural `code` through the SDK/body envelopes, then falls back to the
517
+ * stringified body both SDK families put in `message`.
520
518
  */
521
519
  export function isCopilotTransientModelError(error: unknown): boolean {
522
520
  if (!error || typeof error !== "object" || status(error) !== 400) return false;
@@ -525,7 +523,7 @@ export function isCopilotTransientModelError(error: unknown): boolean {
525
523
  // prototype key (`__proto__`, `toString`, …) would otherwise read truthy.
526
524
  if (code !== undefined && Object.hasOwn(COPILOT_TRANSIENT_MODEL_CODES, code)) return true;
527
525
  const message: unknown = "message" in error ? error.message : undefined;
528
- return typeof message === "string" && COPILOT_MODEL_UNAVAILABLE_PATTERN.test(message);
526
+ return typeof message === "string" && COPILOT_TRANSIENT_MODEL_PATTERN.test(message);
529
527
  }
530
528
 
531
529
  export function classifyMessage(message: {
@@ -22,6 +22,13 @@ const ACCOUNT_RATE_LIMIT_PATTERN =
22
22
  /\baccount(?:'s)?\b[^\n]{0,80}\brate.?limit\b|\brate.?limit\b[^\n]{0,80}\baccount\b/i;
23
23
  const INSUFFICIENT_BALANCE_PATTERN = /insufficient.?balance/i;
24
24
  const SPEND_LIMIT_PATTERN = /spend.?limit/i;
25
+ const SUBSCRIPTION_CAP_PATTERN =
26
+ /\b(?:subscription|plan|membership)\b[^\n]{0,80}\b(?:rate.?limits?|quota|cap)\b|\b(?:rate.?limits?|quota|cap)\b[^\n]{0,80}\b(?:subscription|plan|membership)\b/i;
27
+ const TRANSIENT_INTERVAL_RATE_LIMIT_PATTERN = /\bper\s+(?:second|minute)\b/i;
28
+
29
+ function matchesSubscriptionCapText(errorMessage: string): boolean {
30
+ return SUBSCRIPTION_CAP_PATTERN.test(errorMessage) && !TRANSIENT_INTERVAL_RATE_LIMIT_PATTERN.test(errorMessage);
31
+ }
25
32
  const OPENROUTER_DAILY_FREE_LIMIT_PATTERN = /\bfree[-_ ]models[-_ ]per[-_ ]day\b/i;
26
33
  // gRPC/Connect end-streams carry the status as its name (`resource_exhausted`),
27
34
  // while HTTP bodies use the phrase ("resource exhausted"). Strip either form
@@ -40,6 +47,25 @@ const ACCOUNT_SCOPED_403_PATTERN =
40
47
  // "Your limit will reset in …"); the overall/account qualifiers arm above
41
48
  // already covers the rest.
42
49
  /\b(?:overall|account|organization|team|workspace)\b[^\n]{0,40}\b(?:message |request )?rate.?limit\b|\byour\b[^\n]{0,30}\b(?:limit )?will reset\b/i;
50
+ // Simplified Chinese account-quota exhaustion phrasing. Zhipu Coding Plan
51
+ // returns e.g. "429 已达到 5 小时的使用上限。您的限额将在 2026-08-06 20:06:00 重置。"
52
+ // (type=1308) when the 5h window is spent; other CN providers use 额度已用完 /
53
+ // 配额已耗尽 / 余额不足. These are persistent account-local caps that must
54
+ // rotate to a sibling credential, not transient rate limits, so they are
55
+ // matched before the RATE_LIMIT_EXCEEDED branch. The 上限 arm is anchored on
56
+ // the 使用 token: a rate/concurrency cap phrased as 每分钟请求数已达上限 /
57
+ // 并发请求数已达上限 / 速率达到上限 (no 使用) must NOT match, or it would burn a
58
+ // healthy sibling credential as a false quota. "速率限制" is absent for the
59
+ // same reason.
60
+ const CN_QUOTA_EXHAUSTED_PATTERN = /使用.{0,30}?上限|(?:额度|配额)已?(?:用|耗)(?:完|尽)|限额.{0,30}重置|余额不足/;
61
+ // Simplified Chinese rate/concurrency caps can contain both 使用 and 上限, but
62
+ // remain transient rather than account quota exhaustion.
63
+ const CN_TRANSIENT_CAP_PATTERN =
64
+ /速率.{0,30}上限|频率.{0,30}上限|每分钟.{0,30}上限|并发.{0,30}上限|使用.{0,30}(?:速率|频率|每分钟|并发).{0,30}上限/;
65
+ // Common Simplified Chinese throttle phrasing. Consulted by
66
+ // isOpaqueStatusBody so CN transients stay in the provider backoff lane instead
67
+ // of rotating through the opaque-429 fallback.
68
+ const CN_THROTTLE_PATTERN = /速率(?:限制|过快)|频率(?:过高|过快)|过于频繁|稍后[重再]试/;
43
69
 
44
70
  /**
45
71
  * Classify a rate-limit error message into a reason category.
@@ -64,6 +90,13 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
64
90
  return "QUOTA_EXHAUSTED";
65
91
  }
66
92
 
93
+ // Simplified Chinese quota-exhaustion phrasing (Zhipu Coding Plan and other
94
+ // CN providers). Must precede the MODEL_CAPACITY / RATE_LIMIT branches so an
95
+ // account-local cap rotates instead of backing off as a transient.
96
+ if (CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) {
97
+ return "QUOTA_EXHAUSTED";
98
+ }
99
+
67
100
  if (CONCURRENT_LIMIT_PATTERN.test(errorMessage)) {
68
101
  return "CONCURRENT_LIMIT";
69
102
  }
@@ -80,6 +113,10 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
80
113
  return "QUOTA_EXHAUSTED";
81
114
  }
82
115
 
116
+ if (matchesSubscriptionCapText(errorMessage)) {
117
+ return "QUOTA_EXHAUSTED";
118
+ }
119
+
83
120
  if (OPENROUTER_DAILY_FREE_LIMIT_PATTERN.test(errorMessage)) {
84
121
  return "QUOTA_EXHAUSTED";
85
122
  }
@@ -212,7 +249,20 @@ export function isOpaqueStatusBody(message: string): boolean {
212
249
  const cleaned = message
213
250
  .replace(/\b(?:429|402)\b/g, "")
214
251
  .replace(/\b(?:http|https|status|error|code|response|message)\b/gi, "");
215
- return !/[a-z\d]{3,}/i.test(cleaned);
252
+ // A body is informative when the text classifier can act on it. Any Latin
253
+ // word or Simplified Chinese phrasing the classifier recognizes (quota
254
+ // exhaustion or a throttle) defers to parseRateLimitReason; a body that
255
+ // is only status digits / HTTP framing is opaque and rotates conservatively.
256
+ // A Han-only body the classifier cannot interpret (e.g. Japanese Kanji
257
+ // quota text, since Japanese is out of scope) must stay opaque so the
258
+ // opaque-429 fallback still rotates. This keeps the exception scoped to
259
+ // text we actually classify, rather than to any Han ideograph.
260
+ return (
261
+ !/[a-z\d]{3,}/i.test(cleaned) &&
262
+ !CN_QUOTA_EXHAUSTED_PATTERN.test(cleaned) &&
263
+ !CN_TRANSIENT_CAP_PATTERN.test(cleaned) &&
264
+ !CN_THROTTLE_PATTERN.test(cleaned)
265
+ );
216
266
  }
217
267
 
218
268
  /**
@@ -224,8 +274,10 @@ export function isOpaqueStatusBody(message: string): boolean {
224
274
  export function matchesUsageLimitText(errorMessage: string): boolean {
225
275
  return (
226
276
  USAGE_LIMIT_PATTERN.test(errorMessage) ||
277
+ (CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) ||
227
278
  SPEND_LIMIT_PATTERN.test(errorMessage) ||
228
279
  ACCOUNT_RATE_LIMIT_PATTERN.test(errorMessage) ||
280
+ matchesSubscriptionCapText(errorMessage) ||
229
281
  OPENROUTER_DAILY_FREE_LIMIT_PATTERN.test(errorMessage)
230
282
  );
231
283
  }
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { type Type, type } from "@oh-my-pi/omptype";
2
- export { type ZodType, z } from "zod/v4";
3
2
  export * from "./api-registry";
4
3
  export type * from "./auth-broker";
5
4
  export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server";