@oh-my-pi/pi-ai 17.3.4 → 17.3.7

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/types/auth-storage.d.ts +12 -2
  3. package/dist/types/error/rate-limit.d.ts +5 -2
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/oneshot-retry.d.ts +89 -0
  6. package/dist/types/providers/anthropic-wire.d.ts +18 -5
  7. package/dist/types/providers/openai-shared.d.ts +1 -1
  8. package/dist/types/types.d.ts +5 -4
  9. package/dist/types/usage/kimi.d.ts +3 -1
  10. package/dist/types/utils/schema/strict-tool-validation.d.ts +10 -1
  11. package/dist/types/utils/schema/wire.d.ts +6 -0
  12. package/dist/types/utils/thinking-loop.d.ts +12 -9
  13. package/package.json +5 -5
  14. package/src/auth-storage.ts +36 -5
  15. package/src/error/flags.ts +6 -2
  16. package/src/error/rate-limit.ts +31 -2
  17. package/src/index.ts +1 -0
  18. package/src/oneshot-retry.ts +230 -0
  19. package/src/providers/anthropic-messages-server.ts +5 -5
  20. package/src/providers/anthropic-wire.ts +31 -5
  21. package/src/providers/anthropic.ts +15 -4
  22. package/src/providers/cursor.ts +44 -4
  23. package/src/providers/openai-completions.ts +107 -63
  24. package/src/providers/openai-responses.ts +13 -10
  25. package/src/providers/openai-shared.ts +10 -3
  26. package/src/registry/alibaba-coding-plan.ts +2 -2
  27. package/src/registry/oauth/kimi.ts +20 -0
  28. package/src/stream.ts +59 -57
  29. package/src/types.ts +5 -4
  30. package/src/usage/kimi.ts +15 -0
  31. package/src/usage/umans.ts +93 -20
  32. package/src/utils/leaked-thinking-stream.ts +2 -2
  33. package/src/utils/schema/strict-tool-validation.ts +39 -5
  34. package/src/utils/schema/wire.ts +31 -0
  35. package/src/utils/thinking-loop.ts +96 -63
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.3.7] - 2026-08-17
6
+
7
+ ### Changed
8
+
9
+ - Send the `omp/<version>` User-Agent on xAI chat (`xai` and `xai-oauth`) unless the request already set its own.
10
+
11
+ ## [17.3.5] - 2026-08-16
12
+
13
+ ### Added
14
+
15
+ - Added retryable oneshot completion support (`retryTransientCompletion`) so non-agent LLM calls correctly retry on transient provider failures (Anthropic overload/rate-limit errors, HTTP 429/500/502/503/529), honoring provider-supplied retry-after timing before giving up.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed xAI availability detection so paid-key-only setups correctly default to `xai/grok-4.5` instead of the free SuperGrok catalog; explicit `xai-oauth/…` selectors still work as before.
20
+ - Fixed xAI Responses requests sending unsupported parameters (reasoning summary, presence/frequency penalties) that some models rejected.
21
+ - Fixed Umans usage reporting incorrectly marking quota as exhausted based on raw request counts instead of actual weighted usage, and improved the usage display to show both a soft-cap warning and a hard exhaustion limit with an accurate countdown to reset.
22
+ - Fixed `omp usage invalidate` to fully clear stale usage data and force a fresh refresh, so upgraded subscriptions no longer show outdated quota information.
23
+ - Improved session recovery to correctly treat certain Cursor HTTP/2 connection errors as transient instead of ending the session.
24
+ - Fixed OpenAI-compatible streams (e.g. DeepSeek) that are cut off mid-generation being silently treated as a completed response instead of being retried.
25
+ - Fixed DeepSeek resource-exhaustion interruptions not being automatically retried.
26
+ - Fixed tool-call IDs being lost during same-model replay, which could break correlation with custom gateways.
27
+ - Fixed Kimi Code multi-account routing to prefer accounts with more available quota, respect usage-limit cooldowns, and keep consistent usage history across token refreshes.
28
+ - Fixed Anthropic custom signing-proxy conversations losing tool-search results and thinking content during replay.
29
+ - Fixed rare runaway response loops across model providers so they now fail gracefully instead of repeating indefinitely.
30
+ - Fixed xAI rejecting entire turns due to certain MCP tool schema shapes, restoring compatibility while isolating any remaining incompatible tools rather than failing the whole request.
31
+ - Fixed Alibaba DashScope/Bailian transient per-minute rate limits being misclassified as full quota exhaustion, causing unnecessary long backoffs instead of quick retries.
32
+ - Fixed Anthropic-compatible streams dropping thinking content, which broke replay of prior reasoning.
33
+ - Updated the Alibaba Coding Plan China login flow to point to the current Bailian API-key management console.
34
+
5
35
  ## [17.3.4] - 2026-08-14
6
36
 
7
37
  ### Fixed
@@ -804,10 +804,20 @@ export declare class AuthStorage {
804
804
  */
805
805
  has(provider: string): boolean;
806
806
  /**
807
- * Check if any form of auth is configured for a provider.
808
- * Unlike getApiKey(), this doesn't refresh OAuth tokens.
807
+ * Dedicated auth for default-model availability (picker / `getAvailable`).
808
+ * Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike
809
+ * {@link hasResolvableAuth} it ignores cross-provider env aliases so
810
+ * `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`).
809
811
  */
810
812
  hasAuth(provider: string): boolean;
813
+ /**
814
+ * Whether a request could resolve a key for this provider, including
815
+ * cross-provider env aliases (`xai-oauth` borrowing `XAI_API_KEY`).
816
+ * Use this for explicit model preflight (`xai-oauth/grok-4.6`); use
817
+ * {@link hasAuth} for auto-availability so the default picker stays on
818
+ * paid `xai` when only `XAI_API_KEY` is set.
819
+ */
820
+ hasResolvableAuth(provider: string): boolean;
811
821
  /**
812
822
  * True iff a dedicated, non-env credential source is configured for this
813
823
  * provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`.
@@ -3,11 +3,14 @@
3
3
  * Ported from opencode-antigravity-auth plugin for consistency.
4
4
  */
5
5
  export type RateLimitReason = "QUOTA_EXHAUSTED" | "INSUFFICIENT_G1_CREDITS_BALANCE" | "RATE_LIMIT_EXCEEDED" | "CONCURRENT_LIMIT" | "MODEL_CAPACITY_EXHAUSTED" | "SERVER_ERROR" | "UNKNOWN";
6
+ /** True for DashScope/Bailian's documented OpenAI-compatible TPM/TPS throttle. */
7
+ export declare function isDashScopeTokenLimitText(errorMessage: string): boolean;
6
8
  /**
7
9
  * Classify a rate-limit error message into a reason category.
8
10
  * Priority order: explicit details in a resource-exhausted error > QUOTA
9
- * (Antigravity "quota will reset") > CONCURRENT_LIMIT > MODEL_CAPACITY >
10
- * QUOTA (account) > RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > bare resource-exhausted > UNKNOWN.
11
+ * (Antigravity "quota will reset") > CN quota > DASHSCOPE_TOKEN_LIMIT (TPM/TPS
12
+ * throttle) > CONCURRENT_LIMIT > MODEL_CAPACITY > QUOTA (account) > RATE_LIMIT >
13
+ * QUOTA (generic) > SERVER_ERROR > bare resource-exhausted > UNKNOWN.
11
14
  *
12
15
  * Bare "resource exhausted" / "resource_exhausted" maps to MODEL_CAPACITY (transient, short wait).
13
16
  * Explicit details such as "quota exceeded" retain their normal classification.
@@ -6,6 +6,7 @@ export * from "./auth-gateway/types.js";
6
6
  export * from "./auth-retry.js";
7
7
  export * from "./auth-storage.js";
8
8
  export * from "./error/rate-limit.js";
9
+ export * from "./oneshot-retry.js";
9
10
  export * from "./provider-details.js";
10
11
  export * from "./providers/anthropic.js";
11
12
  export * from "./providers/anthropic-client.js";
@@ -0,0 +1,89 @@
1
+ import type { AssistantMessage } from "./types.js";
2
+ import { type HeadersLike } from "./utils/retry-after.js";
3
+ /**
4
+ * Transient-failure retry for **oneshot** (non-agent-loop) completions.
5
+ *
6
+ * Why this exists: `streamSimple`/`completeSimple` retry *auth* failures
7
+ * (credential rotation) but deliberately surface *transient* provider failures
8
+ * — Anthropic `overloaded_error`, `rate_limit_error`, HTTP 429/500/502/503/529
9
+ * — as a **resolved** `AssistantMessage` with `stopReason: "error"`. For the
10
+ * main agent turn that is correct: `TurnRecovery` owns recovery there, and it
11
+ * must refuse to replay once tool calls or visible text have streamed.
12
+ *
13
+ * Oneshots have no such hazard. A summary, title, handoff, or image
14
+ * description produces no side effects, so re-issuing the whole request is
15
+ * safe and is almost always what the caller wants. Before this helper every
16
+ * oneshot call site had to re-implement that decision, and most did not —
17
+ * failing on the first blip, or swallowing it into `null` so a transient
18
+ * overload was indistinguishable from a legitimate empty result.
19
+ *
20
+ * Classification reuses the existing provider predicates (`AIError`), so the
21
+ * set of retryable Anthropic failures stays defined in exactly one place.
22
+ * Usage limits are included: unlike the provider loop — which excludes them so
23
+ * credential rotation can own them — a oneshot has no rotation layer above it,
24
+ * and the retry hint the provider supplies (`retry-after`, "try again in ~5m")
25
+ * is honored, so waiting is the correct response.
26
+ */
27
+ export interface OneshotRetryOptions {
28
+ /** Total attempts, including the first. Default 3. Values < 1 are treated as 1. */
29
+ maxAttempts?: number;
30
+ /** First backoff step in ms; doubles per attempt. Default 500. */
31
+ baseDelayMs?: number;
32
+ /**
33
+ * Upper bound for a single wait. Default 30_000. A provider retry hint
34
+ * longer than this aborts the retry instead of parking the caller — the
35
+ * error surfaces so higher-level recovery (or the user) can decide.
36
+ */
37
+ maxDelayMs?: number;
38
+ /**
39
+ * Stops further attempts. Two distinct paths, both preserving the caller's
40
+ * intent: an abort already visible when an attempt settles surfaces that
41
+ * attempt's own result (`completeSimple` reports `stopReason: "aborted"`),
42
+ * while an abort that lands during the backoff wait rejects with the abort
43
+ * reason — a user cancel stays a cancel and is never relabelled as the
44
+ * provider failure we happened to be waiting on.
45
+ *
46
+ * This helper does NOT pass the signal into `run` — cancelling the in-flight
47
+ * request is the closure's job, because a per-attempt deadline must be
48
+ * rebuilt on every attempt. Construct it inside `run`
49
+ * (`signal: AbortSignal.timeout(MS)`, or `AbortSignal.any([outer, perAttempt])`);
50
+ * a deadline captured outside would fire once and then abort every retry,
51
+ * silently turning this helper into a single attempt.
52
+ */
53
+ signal?: AbortSignal;
54
+ /**
55
+ * Headers of the attempt that just failed, used to honor `retry-after`.
56
+ *
57
+ * Load-bearing: a transient Anthropic failure arrives as a **resolved**
58
+ * `AssistantMessage`, and `AssistantMessage` carries no headers — so without
59
+ * this the real `retry-after` / `x-ratelimit-reset` values on a 429/529 are
60
+ * invisible and only the (usually hint-free) error text is available.
61
+ * Callers that already capture headers via `SimpleStreamOptions.onResponse`
62
+ * should return the latest capture here; it is read once per failed attempt.
63
+ * Thrown errors need no wiring — headers are recovered from the error itself.
64
+ */
65
+ getResponseHeaders?: () => HeadersLike;
66
+ /** Observability hook. Fires immediately before sleeping. */
67
+ onRetry?: (info: OneshotRetryInfo) => void;
68
+ }
69
+ export interface OneshotRetryInfo {
70
+ /** 1-based index of the attempt that just failed. */
71
+ attempt: number;
72
+ maxAttempts: number;
73
+ delayMs: number;
74
+ /** True when `delayMs` came from a provider retry hint rather than backoff. */
75
+ fromRetryHint: boolean;
76
+ errorMessage: string;
77
+ /** `AIError` classification bits of the failure. */
78
+ errorId: number;
79
+ }
80
+ /**
81
+ * Run a oneshot completion, retrying transient provider failures.
82
+ *
83
+ * Handles both failure shapes: a resolved `AssistantMessage` carrying
84
+ * `stopReason: "error"` (what `completeSimple` produces) and a thrown error
85
+ * (what the raw HTTP helpers produce). A non-retryable failure is returned or
86
+ * rethrown unchanged, so existing caller error handling keeps working — this
87
+ * only removes the *first-blip* failure mode.
88
+ */
89
+ export declare function retryTransientCompletion(run: (attempt: number) => Promise<AssistantMessage>, options?: OneshotRetryOptions): Promise<AssistantMessage>;
@@ -69,6 +69,10 @@ export type ServerToolUseBlockParam = {
69
69
  export type WebSearchServerToolUseBlockParam = ServerToolUseBlockParam & {
70
70
  name: "web_search";
71
71
  };
72
+ /** Tool-search server-tool call whose matching result is replayable by omp. */
73
+ export type ToolSearchServerToolUseBlockParam = ServerToolUseBlockParam & {
74
+ name: "tool_search_tool_regex" | "tool_search_tool_bm25";
75
+ };
72
76
  /** Native web-search result replayed inside an assistant turn. */
73
77
  export type WebSearchToolResultBlockParam = {
74
78
  type: "web_search_tool_result";
@@ -76,14 +80,23 @@ export type WebSearchToolResultBlockParam = {
76
80
  content: unknown;
77
81
  [key: string]: unknown;
78
82
  };
79
- /** True for the complete native web-search history variants omp can replay. */
80
- export declare function isAnthropicWebSearchHistoryBlock(block: {
83
+ /** Native tool-search result replayed inside an assistant turn. */
84
+ export type ToolSearchToolResultBlockParam = {
85
+ type: "tool_search_tool_result";
86
+ tool_use_id: string;
87
+ content: unknown;
88
+ [key: string]: unknown;
89
+ };
90
+ /** Anthropic server-tool history variants omp can replay atomically. */
91
+ export type AnthropicServerToolHistoryBlockParam = WebSearchServerToolUseBlockParam | WebSearchToolResultBlockParam | ToolSearchServerToolUseBlockParam | ToolSearchToolResultBlockParam;
92
+ /** True when a block is complete Anthropic server-tool history omp can replay. */
93
+ export declare function isAnthropicServerToolHistoryBlock(block: {
81
94
  type: string;
82
95
  name?: unknown;
83
96
  id?: unknown;
84
97
  tool_use_id?: unknown;
85
98
  content?: unknown;
86
- }): block is WebSearchServerToolUseBlockParam | WebSearchToolResultBlockParam;
99
+ }): block is AnthropicServerToolHistoryBlockParam;
87
100
  export type ThinkingBlockParam = {
88
101
  type: "thinking";
89
102
  thinking: string;
@@ -108,7 +121,7 @@ export type FallbackBlockParam = {
108
121
  model: string;
109
122
  };
110
123
  };
111
- export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | ServerToolUseBlockParam | WebSearchToolResultBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam | FallbackBlockParam;
124
+ export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | ServerToolUseBlockParam | WebSearchToolResultBlockParam | ToolSearchToolResultBlockParam | ThinkingBlockParam | RedactedThinkingBlockParam | FallbackBlockParam;
112
125
  /**
113
126
  * A single conversation turn.
114
127
  *
@@ -284,7 +297,7 @@ export type ResponseContentBlock = {
284
297
  id: string;
285
298
  name: string;
286
299
  input?: Record<string, unknown> | null;
287
- } | ServerToolUseBlockParam | WebSearchToolResultBlockParam | {
300
+ } | ServerToolUseBlockParam | WebSearchToolResultBlockParam | ToolSearchToolResultBlockParam | {
288
301
  type: "fallback";
289
302
  from: {
290
303
  model: string;
@@ -582,7 +582,7 @@ type CommonSamplingOptions = Pick<StreamOptions, "temperature" | "topP" | "topK"
582
582
  * reflect the model's context window rather than the upstream output limit.
583
583
  */
584
584
  export declare function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(params: P, options: CommonSamplingOptions | undefined, model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
585
- compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
585
+ compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams" | "supportsPenaltyAndStopParams">;
586
586
  }): void;
587
587
  type ReasoningOptions = {
588
588
  reasoning?: string;
@@ -521,19 +521,20 @@ export interface AnthropicFallbackContent {
521
521
  };
522
522
  }
523
523
  /**
524
- * Verbatim Anthropic web-search call/result retained for same-provider
525
- * history replay. Other providers discard it in `transformMessages`.
524
+ * Verbatim Anthropic web-search or tool-search call/result retained for
525
+ * same-provider history replay. Other providers discard it in
526
+ * `transformMessages`.
526
527
  */
527
528
  export interface AnthropicServerToolContent {
528
529
  type: "anthropicServerTool";
529
530
  block: {
530
531
  type: "server_tool_use";
531
532
  id: string;
532
- name: "web_search";
533
+ name: "web_search" | "tool_search_tool_regex" | "tool_search_tool_bm25";
533
534
  input?: Record<string, unknown> | null;
534
535
  [key: string]: unknown;
535
536
  } | {
536
- type: "web_search_tool_result";
537
+ type: "web_search_tool_result" | "tool_search_tool_result";
537
538
  tool_use_id: string;
538
539
  content: unknown;
539
540
  [key: string]: unknown;
@@ -1,2 +1,4 @@
1
- import type { UsageProvider } from "../usage.js";
1
+ import type { CredentialRankingStrategy, UsageProvider } from "../usage.js";
2
2
  export declare const kimiUsageProvider: UsageProvider;
3
+ /** Ranks Kimi OAuth accounts by the canonical 5-hour and 7-day quota windows. */
4
+ export declare const kimiRankingStrategy: CredentialRankingStrategy;
@@ -7,10 +7,19 @@
7
7
  * copied onto a `type: "null"` branch, or an `enum` placed on an `array`
8
8
  * schema instead of its `items`). One such tool 400s the entire turn, so
9
9
  * callers quarantine just the offending tool. See issue #2652.
10
+ *
11
+ * xAI additionally rejects a leftover *root* `anyOf`/`oneOf` whose branches
12
+ * are not objects ("tool parameter root must be an object type"). That class
13
+ * is opt-in via {@link FindStrictToolSchemaViolationOptions.rejectXaiRootObjectUnion}
14
+ * so OpenAI/Azure/Codex keep valid object-root unions.
10
15
  */
16
+ export interface FindStrictToolSchemaViolationOptions {
17
+ /** xAI (paid + OAuth) only: leftover object-root unions 400 the whole turn. */
18
+ rejectXaiRootObjectUnion?: boolean;
19
+ }
11
20
  /**
12
21
  * Walk a tool parameter schema for OpenAI-strict `enum`/`const`-vs-`type`
13
22
  * contradictions. Returns a JSON-pointer-ish path to the first offending node,
14
23
  * or `null` when the schema is safe to emit.
15
24
  */
16
- export declare function findStrictToolSchemaViolation(schema: unknown, path?: string): string | null;
25
+ export declare function findStrictToolSchemaViolation(schema: unknown, path?: string, options?: FindStrictToolSchemaViolationOptions): string | null;
@@ -14,6 +14,12 @@ import type { Tool } from "../../types.js";
14
14
  * methods, while raw JSON Schema is a plain object.
15
15
  */
16
16
  export declare function isArkSchema(value: unknown): value is Type;
17
+ /**
18
+ * Return an xAI-compatible copy of an object-root schema whose union consists
19
+ * only of typeless required-key fragments. Other providers must retain the
20
+ * union because it is a real model-facing constraint.
21
+ */
22
+ export declare function flattenExclusiveRequiredRootUnion(schema: Record<string, unknown>): Record<string, unknown>;
17
23
  /**
18
24
  * Normalize `{}` (an unconstrained schema) to boolean `true` in every
19
25
  * schema-valued position. JSON Schema draft 2020-12 §4.3.1 defines them as
@@ -5,11 +5,12 @@ import { AssistantMessageEventStream } from "./event-stream.js";
5
5
  * classifiers treat it as a transient (retryable) stop without bespoke rules. */
6
6
  export declare const THINKING_LOOP_ERROR_MARKER = "Thinking loop detected";
7
7
  /**
8
- * True when `model.id` belongs to a family guarded for thinking/response loops:
9
- * Gemini, DeepSeek, or Grok.
8
+ * True when `model.id` belongs to a family guarded by the semantic loop
9
+ * heuristics: Gemini, DeepSeek, or Grok. Exact suffix-cycle detection applies to
10
+ * every enabled model independently of this predicate.
10
11
  *
11
12
  * Model identity is derived only from its id; provider and compatibility metadata
12
- * do not opt opaque aliases into the guard.
13
+ * do not opt opaque aliases into semantic detection.
13
14
  */
14
15
  export declare function isLoopGuardedModel(model: Model<Api>, options?: StreamOptions): boolean;
15
16
  /**
@@ -19,6 +20,8 @@ export declare function isLoopGuardedModel(model: Model<Api>, options?: StreamOp
19
20
  */
20
21
  export declare class ThinkingLoopDetector {
21
22
  #private;
23
+ private readonly semanticHeuristics;
24
+ constructor(semanticHeuristics?: boolean);
22
25
  push(delta: string): string | null;
23
26
  /** Process the buffered trailing paragraph (one with no blank-line / heading
24
27
  * terminator). Called when the thinking block ends so the final segment —
@@ -75,12 +78,12 @@ export declare class GeminiHeaderRunDetector {
75
78
  */
76
79
  export declare function guardThinkingLoopStream(inner: AssistantMessageEventStream, model: Model<Api>, controller: AbortController, options?: StreamOptions): AssistantMessageEventStream;
77
80
  /**
78
- * Apply the loop guard around a provider dispatch. For non-guarded models
79
- * (or when disabled) this is a transparent pass-through. For guarded models it injects a
80
- * guard abort signal into the provider call so a detected loop tears down the
81
- * upstream, then wraps the returned stream. The guard only raises the retryable
82
- * stall; bounding the re-samples and the final cook pass lives in the
83
- * result-awaiting caller.
81
+ * Apply the loop guard around a provider dispatch. Unless explicitly disabled,
82
+ * every model gets exact suffix-cycle detection; Gemini, DeepSeek, and Grok also
83
+ * get the semantic heuristics selected by {@link isLoopGuardedModel}. The guard
84
+ * injects an abort signal into the provider call so a detected loop tears down
85
+ * the upstream, then wraps the returned stream. Bounding result-path re-samples
86
+ * lives in the result-awaiting caller.
84
87
  */
85
88
  export declare function withThinkingLoopGuard<O extends {
86
89
  signal?: AbortSignal;
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.3.4",
4
+ "version": "17.3.7",
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,10 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/omptype": "17.3.4",
42
- "@oh-my-pi/pi-catalog": "17.3.4",
43
- "@oh-my-pi/pi-utils": "17.3.4",
44
- "@oh-my-pi/pi-wire": "17.3.4"
41
+ "@oh-my-pi/omptype": "17.3.7",
42
+ "@oh-my-pi/pi-catalog": "17.3.7",
43
+ "@oh-my-pi/pi-utils": "17.3.7",
44
+ "@oh-my-pi/pi-wire": "17.3.7"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@bufbuild/protoc-gen-es": "^2.12.1",
@@ -53,7 +53,7 @@ import { cursorUsageProvider } from "./usage/cursor";
53
53
  import { googleGeminiCliUsageProvider } from "./usage/gemini";
54
54
  import { githubCopilotUsageProvider } from "./usage/github-copilot";
55
55
  import { antigravityRankingStrategy, antigravityUsageProvider } from "./usage/google-antigravity";
56
- import { kimiUsageProvider } from "./usage/kimi";
56
+ import { kimiRankingStrategy, kimiUsageProvider } from "./usage/kimi";
57
57
  import { minimaxCodeUsageProvider } from "./usage/minimax-code";
58
58
  import { ollamaCloudUsageProvider, ollamaUsageProvider } from "./usage/ollama";
59
59
  import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
@@ -1082,6 +1082,7 @@ const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>(
1082
1082
  ["openai-codex", codexRankingStrategy],
1083
1083
  ["anthropic", claudeRankingStrategy],
1084
1084
  ["google-antigravity", antigravityRankingStrategy],
1085
+ ["kimi-code", kimiRankingStrategy],
1085
1086
  ["zai", zaiRankingStrategy],
1086
1087
  ["opencode-go", opencodeGoRankingStrategy],
1087
1088
  ]);
@@ -2680,18 +2681,32 @@ export class AuthStorage {
2680
2681
  }
2681
2682
 
2682
2683
  /**
2683
- * Check if any form of auth is configured for a provider.
2684
- * Unlike getApiKey(), this doesn't refresh OAuth tokens.
2684
+ * Dedicated auth for default-model availability (picker / `getAvailable`).
2685
+ * Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike
2686
+ * {@link hasResolvableAuth} it ignores cross-provider env aliases so
2687
+ * `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`).
2685
2688
  */
2686
2689
  hasAuth(provider: string): boolean {
2687
2690
  if (this.#runtimeOverrides.has(provider)) return true;
2688
2691
  if (this.#configOverrides.has(provider)) return true;
2689
2692
  if (this.#getCredentialsForProvider(provider).length > 0) return true;
2690
- if (getEnvApiKey(provider)) return true;
2693
+ if (this.#hasDedicatedEnvAuth(provider)) return true;
2691
2694
  if (this.#fallbackResolver?.(provider)) return true;
2692
2695
  return false;
2693
2696
  }
2694
2697
 
2698
+ /**
2699
+ * Whether a request could resolve a key for this provider, including
2700
+ * cross-provider env aliases (`xai-oauth` borrowing `XAI_API_KEY`).
2701
+ * Use this for explicit model preflight (`xai-oauth/grok-4.6`); use
2702
+ * {@link hasAuth} for auto-availability so the default picker stays on
2703
+ * paid `xai` when only `XAI_API_KEY` is set.
2704
+ */
2705
+ hasResolvableAuth(provider: string): boolean {
2706
+ if (this.hasAuth(provider)) return true;
2707
+ return Boolean(getEnvApiKey(provider));
2708
+ }
2709
+
2695
2710
  /**
2696
2711
  * True iff a dedicated, non-env credential source is configured for this
2697
2712
  * provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`.
@@ -2711,6 +2726,22 @@ export class AuthStorage {
2711
2726
  return false;
2712
2727
  }
2713
2728
 
2729
+ /**
2730
+ * Env auth that belongs to this provider, not a cross-provider alias.
2731
+ *
2732
+ * `getEnvApiKey("xai-oauth")` also accepts `XAI_API_KEY` so an explicit
2733
+ * `xai-oauth/…` stream can still borrow the paid key. Availability and
2734
+ * origin must not: otherwise an API-key-only setup marks SuperGrok as
2735
+ * signed in and `pickDefaultAvailableModel` prefers `xai-oauth/grok-4.6`
2736
+ * over paid `xai/grok-4.6`.
2737
+ */
2738
+ #hasDedicatedEnvAuth(provider: string): boolean {
2739
+ if (provider === "xai-oauth") {
2740
+ return Boolean($env.XAI_OAUTH_TOKEN?.trim());
2741
+ }
2742
+ return Boolean(getEnvApiKey(provider));
2743
+ }
2744
+
2714
2745
  /**
2715
2746
  * Classify where a provider's auth comes from, following the same precedence
2716
2747
  * as {@link AuthStorage.getApiKey}: runtime override → config override →
@@ -2727,7 +2758,7 @@ export class AuthStorage {
2727
2758
  if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) {
2728
2759
  return { kind: "api_key" };
2729
2760
  }
2730
- if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
2761
+ if (this.#hasDedicatedEnvAuth(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
2731
2762
  if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" };
2732
2763
  if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
2733
2764
  return undefined;
@@ -9,6 +9,7 @@ import {
9
9
  } from "./classes";
10
10
  import {
11
11
  isAccountScopedCapText,
12
+ isDashScopeTokenLimitText,
12
13
  isOpaqueStatusBody,
13
14
  isUsageLimitStatus,
14
15
  matchesUsageLimitText,
@@ -106,7 +107,7 @@ const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
106
107
  const TRANSIENT_ENVELOPE_BEFORE_START_PATTERN = /before message_start/i;
107
108
  export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
108
109
  export const TRANSIENT_TRANSPORT_PATTERN =
109
- /\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i;
110
+ /\b(?:no[_ -]?capacity|(?:high|peak)[ _-]?demand|(?:at|over|insufficient)[ _-]?capacity|capacity[ _-]?(?:exceeded|exhausted)|peak[ _-]?load)\b|overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|unable.?to.?connect\.\s*is the computer able to access the url\?|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|nghttp2_(?:internal_error|refused_stream)|stream closed with error code nghttp2_(?:internal_error|refused_stream)|malformed.?function.?call/i;
110
111
  const AUTH_FAILURE_PATTERN =
111
112
  /\b(?:401|403|unauthorized|forbidden|authentication|auth[_ ]?unavailable|no auth available|(?:invalid|no)[_ ]?api[_ ]?key)\b/i;
112
113
  const MALFORMED_FUNCTION_CALL_PATTERN = /\bmalformed.?function.?call\b/i;
@@ -441,7 +442,10 @@ export function classify(error: unknown, api?: Api): number {
441
442
  } else if (link instanceof ProviderHttpError) {
442
443
  let linkKinds = 0;
443
444
  const { status: codeStatus, code } = link;
444
- if (code === "usage_limit_reached" || code === "insufficient_quota") {
445
+ if (
446
+ code === "usage_limit_reached" ||
447
+ (code === "insufficient_quota" && !isDashScopeTokenLimitText(link.message))
448
+ ) {
445
449
  linkKinds |= Flag.UsageLimit;
446
450
  }
447
451
  if (code === "overloaded_error" || code === "rate_limit_error") {
@@ -69,6 +69,26 @@ const CN_TRANSIENT_CAP_PATTERN =
69
69
  // isOpaqueStatusBody so CN transients stay in the provider backoff lane instead
70
70
  // of rotating through the opaque-429 fallback.
71
71
  const CN_THROTTLE_PATTERN = /速率(?:限制|过快)|频率(?:过高|过快)|过于频繁|稍后[重再]试/;
72
+ // DashScope / Bailian (Alibaba Model Studio) reports its per-minute token
73
+ // throttle (429 Throttling.AllocationQuota, type `insufficient_quota`) with
74
+ // OpenAI-compatible billing wording — "You exceeded your current quota,
75
+ // please check your plan and billing details. … (type=insufficient_quota
76
+ // param=insufficient_quota)" — and links the error-code doc's `token-limit`
77
+ // anchor. Per that doc section the error is a transient TPM/TPS cap that
78
+ // clears within the minute window, not an account-local quota exhaustion.
79
+ // The same doc anchor also covers permanent errors such as "Free allocated
80
+ // quota exceeded", so require both the anchor and the exact throttle wording.
81
+ // The identical wording WITHOUT the anchor stays quota-exhausted (OpenAI's
82
+ // real account-quota error uses the same sentence).
83
+ const DASHSCOPE_TOKEN_LIMIT_DOC_PATTERN = /error-code[^()\s]*#token-limit/i;
84
+ const DASHSCOPE_TOKEN_LIMIT_MESSAGE_PATTERN =
85
+ /\byou exceeded your current quota, please check your plan and billing details\b/i;
86
+ /** True for DashScope/Bailian's documented OpenAI-compatible TPM/TPS throttle. */
87
+ export function isDashScopeTokenLimitText(errorMessage: string): boolean {
88
+ return (
89
+ DASHSCOPE_TOKEN_LIMIT_DOC_PATTERN.test(errorMessage) && DASHSCOPE_TOKEN_LIMIT_MESSAGE_PATTERN.test(errorMessage)
90
+ );
91
+ }
72
92
 
73
93
  const GOOGLE_RPC_ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo";
74
94
  const LONG_RATE_LIMIT_DELAY_MS = 5 * 60 * 1000;
@@ -133,8 +153,9 @@ function isQuotaExhaustedReason(reason: RateLimitReason): boolean {
133
153
  /**
134
154
  * Classify a rate-limit error message into a reason category.
135
155
  * Priority order: explicit details in a resource-exhausted error > QUOTA
136
- * (Antigravity "quota will reset") > CONCURRENT_LIMIT > MODEL_CAPACITY >
137
- * QUOTA (account) > RATE_LIMIT > QUOTA (generic) > SERVER_ERROR > bare resource-exhausted > UNKNOWN.
156
+ * (Antigravity "quota will reset") > CN quota > DASHSCOPE_TOKEN_LIMIT (TPM/TPS
157
+ * throttle) > CONCURRENT_LIMIT > MODEL_CAPACITY > QUOTA (account) > RATE_LIMIT >
158
+ * QUOTA (generic) > SERVER_ERROR > bare resource-exhausted > UNKNOWN.
138
159
  *
139
160
  * Bare "resource exhausted" / "resource_exhausted" maps to MODEL_CAPACITY (transient, short wait).
140
161
  * Explicit details such as "quota exceeded" retain their normal classification.
@@ -162,6 +183,13 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
162
183
  return "QUOTA_EXHAUSTED";
163
184
  }
164
185
 
186
+ // DashScope/Bailian TPM/TPS throttle: billing-worded like OpenAI's account
187
+ // quota, but the doc anchor marks it a per-minute token cap that clears on
188
+ // its own — short backoff on the same credential, never rotation/block.
189
+ if (isDashScopeTokenLimitText(errorMessage)) {
190
+ return "RATE_LIMIT_EXCEEDED";
191
+ }
192
+
165
193
  if (CONCURRENT_LIMIT_PATTERN.test(errorMessage)) {
166
194
  return "CONCURRENT_LIMIT";
167
195
  }
@@ -342,6 +370,7 @@ export function isOpaqueStatusBody(message: string): boolean {
342
370
  export function matchesUsageLimitText(errorMessage: string): boolean {
343
371
  const structuredReason = parseGoogleRpcRateLimitReason(errorMessage);
344
372
  if (structuredReason !== undefined) return isQuotaExhaustedReason(structuredReason);
373
+ if (isDashScopeTokenLimitText(errorMessage)) return false;
345
374
  return (
346
375
  USAGE_LIMIT_PATTERN.test(errorMessage) ||
347
376
  (CN_QUOTA_EXHAUSTED_PATTERN.test(errorMessage) && !CN_TRANSIENT_CAP_PATTERN.test(errorMessage)) ||
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./auth-gateway/types";
6
6
  export * from "./auth-retry";
7
7
  export * from "./auth-storage";
8
8
  export * from "./error/rate-limit";
9
+ export * from "./oneshot-retry";
9
10
  export * from "./provider-details";
10
11
  export * from "./providers/anthropic";
11
12
  export * from "./providers/anthropic-client";