@oh-my-pi/pi-ai 18.2.1 → 18.2.3

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 (36) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -0
  3. package/dist/types/auth/sqlite-credential-store.d.ts +2 -1
  4. package/dist/types/auth-gateway/session-state.d.ts +69 -16
  5. package/dist/types/auth-storage.d.ts +11 -6
  6. package/dist/types/index.d.ts +1 -0
  7. package/dist/types/provider-session-state.d.ts +46 -0
  8. package/dist/types/providers/amazon-bedrock.d.ts +3 -0
  9. package/dist/types/providers/aws-sigv4.d.ts +12 -0
  10. package/dist/types/providers/openai-responses.d.ts +15 -0
  11. package/dist/types/registry/oauth/github-copilot.d.ts +2 -6
  12. package/dist/types/registry/oauth/kimi.d.ts +2 -1
  13. package/dist/types/registry/oauth/types.d.ts +2 -0
  14. package/dist/types/usage/openai-codex.d.ts +3 -1
  15. package/dist/types/usage.d.ts +3 -1
  16. package/package.json +6 -6
  17. package/src/auth/sqlite-credential-store.ts +8 -33
  18. package/src/auth-gateway/server.ts +159 -84
  19. package/src/auth-gateway/session-state.ts +227 -29
  20. package/src/auth-storage.ts +19 -9
  21. package/src/index.ts +1 -0
  22. package/src/provider-session-state.ts +56 -0
  23. package/src/providers/amazon-bedrock.ts +20 -3
  24. package/src/providers/anthropic-messages-server.ts +80 -20
  25. package/src/providers/anthropic-signature.ts +5 -2
  26. package/src/providers/aws-sigv4.ts +16 -5
  27. package/src/providers/cursor.ts +53 -9
  28. package/src/providers/openai-completions.ts +6 -0
  29. package/src/providers/openai-responses.ts +27 -0
  30. package/src/registry/oauth/github-copilot.ts +2 -2
  31. package/src/registry/oauth/kimi.ts +4 -4
  32. package/src/registry/oauth/types.ts +2 -0
  33. package/src/stream.ts +36 -1
  34. package/src/usage/openai-codex.ts +94 -11
  35. package/src/usage.ts +5 -1
  36. package/src/utils/http-inspector.ts +20 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.2.3] - 2026-09-17
6
+
7
+ ### Added
8
+
9
+ - `stream()` and `streamSimple()` support asynchronous model header resolution for each request attempt, including authentication retries and cancellation.
10
+ - Provider login prompts can request masked entry with `secret: true`.
11
+
12
+ ## [18.2.2] - 2026-09-16
13
+
14
+ ### Added
15
+
16
+ - Added configurable `baseUrl` support for `bedrock-converse-stream` requests, enabling Amazon Bedrock providers and compatible custom providers to use VPC or PrivateLink endpoints, FIPS hosts, and internal gateways, including endpoints mounted under a path or authenticated with query parameters.
17
+
18
+ ### Fixed
19
+
20
+ - Corrupt credential databases are now backed up privately and recreated instead of preventing startup; signing in again restores credentials.
21
+ - Fixed malformed Anthropic thinking signatures that could freeze sessions at 100% CPU.
22
+ - Fixed Anthropic-compatible gateway tool-call handling so client-declared tools are reported with `stop_reason: "tool_use"` and already-executed native provider tools are not exposed for clients to run again.
23
+ - Fixed gateway session isolation when clients omit a session key, preventing one conversation's retained provider state from affecting another.
24
+ - Fixed retained provider state across credential switches so account-specific capabilities are re-evaluated while reusable endpoint capabilities remain available.
25
+ - Fixed session retention limits closing provider state while a request is still streaming.
26
+ - ChatGPT accounts that have exhausted a plan's usage window but still have available Codex credit can now continue to be selected for Codex requests.
27
+ - Cursor requests now honor explicit max-mode markers on wire-backed models instead of inferring the mode from the model suffix.
28
+ - OpenAI-compatible chat responses containing only structured tool calls now report time to first token correctly.
29
+
5
30
  ## [18.2.1] - 2026-09-15
6
31
 
7
32
  ### Added
package/README.md CHANGED
@@ -1084,6 +1084,8 @@ omp auth-broker logout # interactive — pick a stored credential to
1084
1084
 
1085
1085
  Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
1086
1086
 
1087
+ If SQLite reports corruption during startup, the damaged database and remaining journal sidecars are preserved beside it as private `agent.db.corrupt-<timestamp>-<id>*` backups before a fresh database is created. The log records the backup path. This restores startup, not unreadable credentials: log in again; retain the backups for manual data recovery. Lock contention and other non-corruption errors never reset the database.
1088
+
1087
1089
  `login` supports OAuth providers (Anthropic, OpenAI Codex, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.
1088
1090
 
1089
1091
  For the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Novita, DeepInfra, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.
@@ -4,7 +4,7 @@
4
4
  * The public AuthCredentialStore interface remains in ../auth-storage so local
5
5
  * and remote stores share the same contract.
6
6
  */
7
- import { Database } from "bun:sqlite";
7
+ import type { Database } from "bun:sqlite";
8
8
  import { isSqliteBusyError, isSqliteCorruptionError } from "@oh-my-pi/pi-utils";
9
9
  import type { AuthCredential, AuthCredentialStore, CredentialRefreshLeaseFence, DisabledCredentialSummary, StoredAuthCredential, StoredCredentialBlock } from "../auth-storage.js";
10
10
  import type { OAuthCredentials } from "../registry/oauth/types.js";
@@ -29,6 +29,7 @@ export declare function resolveCredentialIdentityKey(provider: string, credentia
29
29
  export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
30
30
  #private;
31
31
  constructor(db: Database);
32
+ /** Opens credential storage with bounded busy retries and one-shot corruption recovery. */
32
33
  static open(dbPath?: string): Promise<SqliteAuthCredentialStore>;
33
34
  listAuthCredentials(provider?: string): StoredAuthCredential[];
34
35
  listDisabledCredentials(provider?: string): Promise<DisabledCredentialSummary[]>;
@@ -19,9 +19,11 @@
19
19
  * A plain `Map<sessionId, …>` in a long-lived server process is a leak: nothing
20
20
  * ever reclaims an entry, and the entries own timers and sockets. This store is
21
21
  * an LRU with a hard entry ceiling that calls `close()` on everything it drops
22
- * and on everything it still holds at shutdown.
22
+ * and on everything it still holds at shutdown — but it only ever drops an
23
+ * entry no request is holding, because `close()` on a live entry tears down
24
+ * state an in-flight stream is still streaming through.
23
25
  */
24
- import type { Api, Model, ProviderSessionState } from "../types.js";
26
+ import type { Api, Context, Model, ProviderSessionState } from "../types.js";
25
27
  /**
26
28
  * Retained logical sessions. Each entry is a handful of small provider records
27
29
  * plus, for Codex, a WebSocket session — cheap to keep, but not free, so the
@@ -30,11 +32,58 @@ import type { Api, Model, ProviderSessionState } from "../types.js";
30
32
  * have been quiet longer than the 256 most recent ones.
31
33
  */
32
34
  export declare const AUTH_GATEWAY_MAX_SESSION_STATES = 256;
35
+ /**
36
+ * One request's claim on a retained session.
37
+ *
38
+ * `release()` is what makes the entry evictable again, so it MUST run for every
39
+ * outcome of the request — a `finally` at the call site for the synchronous
40
+ * paths, stream completion for the streaming ones. It is idempotent, so the
41
+ * two can overlap.
42
+ */
43
+ export interface AuthGatewaySessionStateLease {
44
+ /** The map to hand to `streamSimple` as `providerSessionState`. */
45
+ readonly states: Map<string, ProviderSessionState>;
46
+ /** Reset account-scoped records if an in-request auth retry switches accounts. */
47
+ updateAccount(account: string): void;
48
+ /** Give up this request's claim. Idempotent. */
49
+ release(): void;
50
+ }
51
+ /** Everything the store needs to place one request on a retained session. */
52
+ export interface AuthGatewaySessionStateRequest {
53
+ /**
54
+ * The client's own session key (`prompt_cache_key` / `sessionId`), or
55
+ * `undefined` when it sent none — blank counts as none. A supplied key is
56
+ * authoritative: the client is telling us which conversation this is.
57
+ */
58
+ clientKey: string | undefined;
59
+ model: Model<Api>;
60
+ /**
61
+ * System prompt, tools and message history of this request. Used only when
62
+ * `clientKey` is absent, to place the request on the conversation it
63
+ * continues.
64
+ */
65
+ context: Context;
66
+ /**
67
+ * Stable identity of the account this request's credential resolved to.
68
+ * A change means the gateway switched the session to a sibling credential,
69
+ * so the account-dependent lessons in the retained map are re-probed. The
70
+ * comparison happens on acquire and whenever an in-request auth retry
71
+ * resolves a sibling credential.
72
+ */
73
+ account: string;
74
+ }
33
75
  /**
34
76
  * Bounded per-session provider state, owned by one gateway server instance.
35
77
  *
36
78
  * Two gateways in the same process get separate stores, so neither can hand a
37
79
  * request another gateway's learned state or close it out from under one.
80
+ *
81
+ * The recency order is this class's own (a `Map` iterates in insertion order,
82
+ * and every acquire re-inserts) rather than `LRUCache`'s, because the policy
83
+ * needs two things a general cache cannot express: an entry that a request is
84
+ * still holding must be skipped when picking a victim, and an entry must be
85
+ * able to change key — `LRUCache` disposes on every removal, which is precisely
86
+ * the `close()` we must not run here.
38
87
  */
39
88
  export declare class AuthGatewaySessionStateStore {
40
89
  #private;
@@ -42,24 +91,28 @@ export declare class AuthGatewaySessionStateStore {
42
91
  /** Retained logical sessions. */
43
92
  get size(): number;
44
93
  /**
45
- * The provider-session map for one logical session on one model, created on
46
- * first use and returned by reference so provider mutations persist into the
47
- * next request.
94
+ * Claim the provider-session map for one request, created on first use and
95
+ * returned by reference so provider mutations persist into the next request.
48
96
  *
49
- * Keyed by session + provider + model id. The session is the identity that
50
- * matters — it is the same identity used for credential stickiness and
51
- * prefix-cache keying — but a client is free to reuse one session id across
52
- * models, and the coarsest provider entries do not separate models
53
- * themselves (`openai-responses` keys its strict-tools / history-replay
54
- * record by provider alone, Antigravity by a single constant), so the model
55
- * belongs in the key here. Endpoint is deliberately absent: every provider
56
- * whose learning is endpoint-specific already sub-keys it internally
97
+ * Keyed by provider + model + conversation (see {@link sessionKeys}). A
98
+ * client is free to reuse one session id across models, and the coarsest
99
+ * provider entries do not separate models themselves (`openai-responses`
100
+ * keys its strict-tools / history-replay record by provider alone,
101
+ * Antigravity by a single constant), so the model belongs in the key here.
102
+ * Endpoint is deliberately absent: every provider whose learning is
103
+ * endpoint-specific already sub-keys it internally
57
104
  * (`anthropic-messages:${baseUrl}\0${modelId}`,
58
105
  * `openai-completions:${provider}:${baseUrl}:${modelId}`), and repeating it
59
- * would only fragment the map. NUL separates the components so none of them
60
- * can forge the boundary.
106
+ * would only fragment the map. The credential is absent for the same reason
107
+ * — most of what is retained is true of the endpoint whoever calls it, and
108
+ * Codex already sub-keys its transport by account and bearer — so a
109
+ * credential switch resets the account-dependent subset instead of
110
+ * splitting the entry (see `resetAccountScopedProviderSessionState`).
111
+ *
112
+ * The returned lease MUST be released; until then the entry cannot be
113
+ * evicted.
61
114
  */
62
- acquire(sessionId: string, model: Model<Api>): Map<string, ProviderSessionState>;
115
+ acquire(request: AuthGatewaySessionStateRequest): AuthGatewaySessionStateLease;
63
116
  /** Close and drop every retained state. Called when the gateway shuts down. */
64
117
  close(): void;
65
118
  }
@@ -1,5 +1,5 @@
1
1
  import type { ApiKeyResolver } from "./auth-retry.js";
2
- import type { OAuthAuthInfo, OAuthController, OAuthCredentials, OAuthProviderId } from "./registry/oauth/types.js";
2
+ import type { OAuthAuthInfo, OAuthController, OAuthCredentials, OAuthPrompt, OAuthProviderId } from "./registry/oauth/types.js";
3
3
  import type { Provider } from "./types.js";
4
4
  import type { ClientUsageIdentity, ClientUsageReport, ClientUsageSummary, CredentialRankingStrategy, ObservedUsageEntry, UsageHistoryEntry, UsageHistoryQuery, UsageLogger, UsageProvider, UsageReport } from "./usage.js";
5
5
  import { type CodexResetConsumeCode, type CodexResetCredit } from "./usage/openai-codex-reset.js";
@@ -800,7 +800,7 @@ export declare class AuthStorage {
800
800
  * Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
801
801
  * still wins for the duration of a single invocation.
802
802
  */
803
- setConfigApiKey(provider: string, apiKey: string): void;
803
+ setConfigApiKey(provider: string, apiKeyConfig: string): void;
804
804
  /**
805
805
  * Remove a single config-sourced API key override.
806
806
  */
@@ -815,6 +815,13 @@ export declare class AuthStorage {
815
815
  * Used for custom provider keys from models.json.
816
816
  */
817
817
  setFallbackResolver(resolver: (provider: string) => string | undefined): void;
818
+ /**
819
+ * Install the host's async config-value resolver. Coding-agent uses this so
820
+ * every stored/config credential reference shares command caching,
821
+ * failure backoff, and process hardening even when AuthStorage was created
822
+ * independently and later attached to a registry.
823
+ */
824
+ setConfigValueResolver(resolver: (config: string) => Promise<string | undefined>): void;
818
825
  /**
819
826
  * Reload credentials from storage.
820
827
  */
@@ -935,10 +942,7 @@ export declare class AuthStorage {
935
942
  /** onAuth is required by auth-storage but optional in OAuthController */
936
943
  onAuth: (info: OAuthAuthInfo) => void;
937
944
  /** onPrompt is required for some providers (github-copilot, openai-codex) */
938
- onPrompt: (prompt: {
939
- message: string;
940
- placeholder?: string;
941
- }) => Promise<string>;
945
+ onPrompt: (prompt: OAuthPrompt) => Promise<string>;
942
946
  }): Promise<OAuthLoginIdentity | undefined>;
943
947
  /**
944
948
  * Logout from a provider.
@@ -976,6 +980,7 @@ export declare class AuthStorage {
976
980
  ingestUsageHeaders(provider: Provider, headers: Record<string, string>, options?: {
977
981
  sessionId?: string;
978
982
  baseUrl?: string;
983
+ responseStatus?: number;
979
984
  }): boolean;
980
985
  /**
981
986
  * The {@link UsageProvider} registered for `provider`, or undefined when the
@@ -8,6 +8,7 @@ export * from "./auth-storage.js";
8
8
  export * from "./error/rate-limit.js";
9
9
  export * from "./oneshot-retry.js";
10
10
  export * from "./provider-details.js";
11
+ export * from "./provider-session-state.js";
11
12
  export * from "./providers/anthropic.js";
12
13
  export * from "./providers/anthropic-client.js";
13
14
  export * from "./providers/azure-openai-responses.js";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Credential-rotation handling for a retained `providerSessionState` map.
3
+ *
4
+ * A host that keeps one provider-session map per logical conversation (the
5
+ * auth-gateway's server-owned store, an in-process omp session) can outlive the
6
+ * credential that filled it: `AuthStorage.markUsageLimitReached` and the
7
+ * auth-retry resolver both switch a session to a sibling account mid-flight.
8
+ * Most of what a provider learns is a property of the *endpoint*, so rebuilding
9
+ * the whole map on a switch would re-pay every rejected round-trip the map
10
+ * exists to avoid. A minority is a property of the *account*, and keeping that
11
+ * across a switch is a bug.
12
+ *
13
+ * Audit of what the retained records hold, per provider:
14
+ *
15
+ * - **Anthropic** — `fastModeDisabled` is account-scoped: the rejection reads
16
+ * "this model does not support fast mode for your account", i.e. a plan
17
+ * entitlement, so a switch to an entitled sibling must re-probe. Its
18
+ * siblings are endpoint-scoped and stay: `strictToolsDisabled`
19
+ * (grammar-too-large 400 for the model's tool schema),
20
+ * `replayUnsignedThinkingDisabled` / `thinkingReplayDisabled` (the endpoint
21
+ * is a signing proxy), `prefixDroppedThinkingBlocks` (blocks the API itself
22
+ * dropped), `controlStates` (per-conversation control baselines).
23
+ * - **OpenAI Responses** — the `previous_response_id` chain baselines are
24
+ * account-scoped: a stored response belongs to the account that created it.
25
+ * Strict-tools / reasoning-effort fallbacks, replay warmup and the chaining
26
+ * circuit breaker are endpoint-scoped and stay.
27
+ * - **OpenAI Completions** — strict-tools and reasoning-effort fallbacks only;
28
+ * both endpoint-scoped. Nothing to reset.
29
+ * - **Codex** — already sub-keys its WebSocket sessions by account id AND
30
+ * bearer (`getCodexWebSocketSessionKey`), so a switch naturally lands on a
31
+ * fresh transport session while the old one stays reachable for teardown.
32
+ * Resetting from the outside would close a socket a retry may still be on.
33
+ * - **Antigravity** — `lastGoodEndpoint` is endpoint-scoped; the agent /
34
+ * conversation ids are conversation-scoped. Neither depends on the account.
35
+ * - **GitLab Duo** — the active workflow is account-bound, but it is a live
36
+ * server-side workflow plus socket, and the switch happens *inside* the
37
+ * request that may still be resuming it. Tearing it down here would abort the
38
+ * very turn that rotated; it stays on its existing session-close path.
39
+ */
40
+ import type { ProviderSessionState } from "./types.js";
41
+ /**
42
+ * Reset the account-dependent lessons in `states`, keeping everything a
43
+ * provider learned about the endpoint. Call when a retained map is about to be
44
+ * reused for a session whose credential now resolves to a different account.
45
+ */
46
+ export declare function resetAccountScopedProviderSessionState(states: Map<string, ProviderSessionState>): void;
@@ -5,6 +5,9 @@
5
5
  * SigV4 signing and decodes the `application/vnd.amazon.eventstream` response.
6
6
  * No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
7
7
  * Bun's native `HTTPS_PROXY` support.
8
+ *
9
+ * A `models.yml` `baseUrl` is the request origin verbatim (VPC endpoint, gateway, …);
10
+ * only AWS's own regional host is re-pointed at the resolved region. SigV4 unaffected.
8
11
  */
9
12
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
10
13
  import type { StreamFunction, StreamOptions, ThinkingBudgets } from "../types.js";
@@ -45,6 +45,18 @@ export declare function formatAmzDate(d: Date): {
45
45
  longDate: string;
46
46
  shortDate: string;
47
47
  };
48
+ /**
49
+ * AWS's canonical-request spec encodes each name/value first, THEN sorts by
50
+ * the encoded form ("Sort the encoded parameter names by character code" —
51
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/create-canonical-request.html).
52
+ * Sorting the decoded form instead gives the wrong order whenever encoding
53
+ * changes a character's relative position — e.g. raw key `%7B` (decodes to
54
+ * `{`, 0x7B) vs `x` (0x78): decoded, `x` < `{`; encoded, `%` (0x25) < `x`, so
55
+ * `%7B` sorts first. A gateway that validates SigV4 (or AWS itself) computes
56
+ * the signature over ITS OWN canonicalization and rejects ours if the two
57
+ * disagree on order.
58
+ */
59
+ export declare function canonicalQuery(query: string | undefined): string;
48
60
  export interface SignedHeaders {
49
61
  host: string;
50
62
  "x-amz-date": string;
@@ -85,6 +85,21 @@ interface OpenAIResponsesChainState {
85
85
  /** Set once chaining is judged unsupported for this session (circuit breaker). */
86
86
  disabled: boolean;
87
87
  }
88
+ /**
89
+ * Drop the account-bound half of every retained `openai-responses` record in
90
+ * `states`: the stateful `previous_response_id` chain baselines.
91
+ *
92
+ * Chaining stores the turn server-side under the account that created it, so a
93
+ * baseline minted by one credential is dead weight the moment the session is
94
+ * switched to a sibling account — the next delta request answers
95
+ * `Previous response not found` and burns a turn re-learning that. Everything
96
+ * else this record holds describes the *deployment*, not the account
97
+ * (strict-tools demotion, reasoning-effort fallback, native-history-replay
98
+ * warmup, the chaining circuit breaker), and is deliberately preserved:
99
+ * re-learning an endpoint's limits on every credential switch is the cost this
100
+ * state exists to avoid.
101
+ */
102
+ export declare function resetOpenAIResponsesAccountScopedState(states: Map<string, ProviderSessionState>): void;
88
103
  type OpenRouterAnthropicCacheControl = {
89
104
  type: "ephemeral";
90
105
  ttl?: "1h";
@@ -1,12 +1,8 @@
1
1
  import type { FetchImpl } from "../../types.js";
2
- import type { OAuthController, OAuthCredentials } from "./types.js";
2
+ import type { OAuthController, OAuthCredentials, OAuthPrompt } from "./types.js";
3
3
  type GitHubCopilotLoginOptions = {
4
4
  onAuth: (url: string, instructions?: string) => void;
5
- onPrompt: (prompt: {
6
- message: string;
7
- placeholder?: string;
8
- allowEmpty?: boolean;
9
- }) => Promise<string>;
5
+ onPrompt: (prompt: OAuthPrompt) => Promise<string>;
10
6
  onProgress?: (message: string) => void;
11
7
  copilotIntegrationId?: unknown;
12
8
  signal?: AbortSignal;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Kimi Code OAuth flow (device authorization grant)
3
3
  */
4
- export declare let getKimiCommonHeaders: () => Readonly<{
4
+ /** Lazily resolve the process-stable device headers used by Kimi requests. */
5
+ export declare const getKimiCommonHeaders: () => Readonly<{
5
6
  "User-Agent": `KimiCLI/${string}`;
6
7
  "X-Msh-Platform": "kimi_cli";
7
8
  "X-Msh-Version": string;
@@ -33,6 +33,8 @@ export type OAuthPrompt = {
33
33
  message: string;
34
34
  placeholder?: string;
35
35
  allowEmpty?: boolean;
36
+ /** Request masked entry from interactive hosts. Hosts that cannot hide input must reject the prompt. */
37
+ secret?: boolean;
36
38
  };
37
39
  export type OAuthAuthInfo = {
38
40
  /**
@@ -5,6 +5,8 @@ import type { CredentialRankingStrategy, UsageProvider, UsageReport } from "../u
5
5
  * ingesting them lets credential selection block an exhausted account before
6
6
  * the next request burns a wire 429.
7
7
  */
8
- export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number): UsageReport | null;
8
+ export declare function parseCodexRateLimitHeaders(headers: Record<string, string>, now?: number, context?: {
9
+ responseStatus?: number;
10
+ }): UsageReport | null;
9
11
  export declare const openaiCodexUsageProvider: UsageProvider;
10
12
  export declare const codexRankingStrategy: CredentialRankingStrategy;
@@ -480,7 +480,9 @@ export interface UsageProvider {
480
480
  id: Provider;
481
481
  fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null>;
482
482
  /** Parse provider rate-limit response headers (lowercased keys) into a usage report, if supported. */
483
- parseRateLimitHeaders?(headers: Record<string, string>, now?: number): UsageReport | null;
483
+ parseRateLimitHeaders?(headers: Record<string, string>, now?: number, context?: {
484
+ responseStatus?: number;
485
+ }): UsageReport | null;
484
486
  supports?(params: UsageFetchParams): boolean;
485
487
  /** True when fetchUsage contacts upstream and can authenticate the credential for health checks. */
486
488
  validatesCredentials?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.2.1",
3
+ "version": "18.2.3",
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.2.1",
128
- "@oh-my-pi/pi-catalog": "18.2.1",
129
- "@oh-my-pi/pi-natives": "18.2.1",
130
- "@oh-my-pi/pi-utils": "18.2.1",
131
- "@oh-my-pi/pi-wire": "18.2.1"
127
+ "@oh-my-pi/omptype": "18.2.3",
128
+ "@oh-my-pi/pi-catalog": "18.2.3",
129
+ "@oh-my-pi/pi-natives": "18.2.3",
130
+ "@oh-my-pi/pi-utils": "18.2.3",
131
+ "@oh-my-pi/pi-wire": "18.2.3"
132
132
  },
133
133
  "devDependencies": {
134
134
  "@types/bun": "^1.3.14"
@@ -4,7 +4,7 @@
4
4
  * The public AuthCredentialStore interface remains in ../auth-storage so local
5
5
  * and remote stores share the same contract.
6
6
  */
7
- import { Database, type Statement } from "bun:sqlite";
7
+ import type { Database, Statement } from "bun:sqlite";
8
8
  import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
10
  import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
@@ -15,6 +15,7 @@ import {
15
15
  isSqliteBusyError,
16
16
  isSqliteCorruptionError,
17
17
  logger,
18
+ openSqliteDatabase,
18
19
  } from "@oh-my-pi/pi-utils";
19
20
  import type {
20
21
  AuthCredential,
@@ -25,7 +26,6 @@ import type {
25
26
  StoredAuthCredential,
26
27
  StoredCredentialBlock,
27
28
  } from "../auth-storage";
28
- import * as AIError from "../error";
29
29
  import type { OAuthCredentials } from "../registry/oauth/types";
30
30
  import type { Provider } from "../types";
31
31
  import type {
@@ -509,6 +509,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
509
509
  );
510
510
  }
511
511
 
512
+ /** Opens credential storage with bounded busy retries and one-shot corruption recovery. */
512
513
  static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
513
514
  const dir = path.dirname(dbPath);
514
515
  const dirExists = await fs
@@ -519,23 +520,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
519
520
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
520
521
  }
521
522
 
522
- // Concurrent omp startups can race against WAL recovery and the schema
523
- // init's first lock-taking statement. Bun's default `busy_timeout` is 0,
524
- // so retry the open on `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY` with bounded
525
- // exponential backoff before surfacing the failure. See issue #2421.
526
- const maxAttempts = 4;
527
- const baseDelayMs = 100;
528
- let lastBusyError: Error | undefined;
529
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
530
- let db: Database | undefined;
531
- try {
532
- db = new Database(dbPath);
533
- // Install the busy handler BEFORE the first lock-taking statement
534
- // on this connection. The leases DDL below and the constructor's
535
- // schema init both acquire locks during WAL recovery; without a
536
- // non-zero `busy_timeout` they fail immediately with SQLITE_BUSY.
537
- // See issue #2421.
538
- SqliteAuthCredentialStore.#installBusyTimeout(db);
523
+ return openSqliteDatabase(
524
+ dbPath,
525
+ async db => {
539
526
  try {
540
527
  await fs.chmod(dbPath, 0o600);
541
528
  } catch {
@@ -543,20 +530,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
543
530
  }
544
531
  SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(db);
545
532
  return new SqliteAuthCredentialStore(db);
546
- } catch (err) {
547
- db?.close();
548
- if (!isSqliteBusyError(err)) {
549
- throw err;
550
- }
551
- lastBusyError = err instanceof Error ? err : new Error(String(err));
552
- if (attempt < maxAttempts - 1) {
553
- await Bun.sleep(baseDelayMs * 2 ** attempt);
554
- }
555
- }
556
- }
557
- throw new AIError.ConfigurationError(
558
- `Failed to open auth database at '${dbPath}' after ${maxAttempts} attempts: ${lastBusyError?.message}`,
559
- { cause: lastBusyError },
533
+ },
534
+ { recoverCorruption: true },
560
535
  );
561
536
  }
562
537