@oh-my-pi/pi-ai 18.8.3 → 18.8.5

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 (44) hide show
  1. package/CHANGELOG.md +42 -64
  2. package/dist/types/auth/affinity.d.ts +4 -3
  3. package/dist/types/auth/oauth.d.ts +4 -3
  4. package/dist/types/auth/refresh.d.ts +1 -0
  5. package/dist/types/auth/select.d.ts +1 -1
  6. package/dist/types/auth/store.d.ts +2 -2
  7. package/dist/types/auth/types.d.ts +38 -10
  8. package/dist/types/auth-broker/client.d.ts +1 -1
  9. package/dist/types/auth-broker/remote-store.d.ts +5 -4
  10. package/dist/types/auth-broker/server.d.ts +2 -0
  11. package/dist/types/auth-gateway/dispatch.d.ts +2 -0
  12. package/dist/types/auth-gateway/http.d.ts +0 -2
  13. package/dist/types/provider-session-state.d.ts +2 -2
  14. package/dist/types/providers/anthropic.d.ts +6 -0
  15. package/dist/types/providers/cursor.d.ts +7 -7
  16. package/dist/types/registry/oauth/cursor.d.ts +1 -1
  17. package/dist/types/usage.d.ts +2 -0
  18. package/dist/types/utils/anthropic-auth.d.ts +21 -7
  19. package/dist/types/utils/resolve-peer.d.ts +2 -0
  20. package/package.json +6 -6
  21. package/src/auth/affinity.ts +9 -3
  22. package/src/auth/health.ts +2 -2
  23. package/src/auth/oauth.ts +16 -4
  24. package/src/auth/refresh.ts +17 -9
  25. package/src/auth/resets.ts +1 -1
  26. package/src/auth/select.ts +17 -12
  27. package/src/auth/store.ts +2 -2
  28. package/src/auth/types.ts +46 -11
  29. package/src/auth/usage.ts +1 -1
  30. package/src/auth-broker/client.ts +5 -2
  31. package/src/auth-broker/remote-store.ts +39 -9
  32. package/src/auth-broker/server.ts +33 -7
  33. package/src/auth-gateway/dispatch.ts +2 -0
  34. package/src/auth-gateway/http.ts +0 -8
  35. package/src/auth-gateway/server.ts +10 -9
  36. package/src/auth-storage.ts +7 -1
  37. package/src/provider-session-state.ts +2 -2
  38. package/src/providers/anthropic.ts +58 -12
  39. package/src/providers/cursor.ts +35 -9
  40. package/src/registry/oauth/cursor.ts +25 -16
  41. package/src/usage/github-copilot.ts +1 -1
  42. package/src/usage.ts +8 -0
  43. package/src/utils/anthropic-auth.ts +34 -10
  44. package/src/utils/resolve-peer.ts +7 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.8.5] - 2026-10-08
6
+
7
+ ### Added
8
+
9
+ - `oauth.refresh(id, signal, { reason: "auth-recovery" })` forwards provider-401 recovery intent to a delegated (auth broker) refresh, and `AuthStorageOptions.refreshOAuthCredentialMints` marks a `refreshOAuthCredential` hook that exchanges tokens itself so its tokens are reused for auth recovery ([#14752](https://github.com/can1357/oh-my-pi/pull/14752) by [@will-bogusz](https://github.com/will-bogusz))
10
+ - `SessionsApi.inherit` accepts an optional filter, called with each provider and whether the source's affinity is an explicit user pin, to copy only the affinities it accepts ([#14749](https://github.com/can1357/oh-my-pi/pull/14749) by [@will-bogusz](https://github.com/will-bogusz))
11
+
12
+ ### Changed
13
+
14
+ - `AuthApiKeyOptions.accountIds` also matches the login email, or else the project id, of credentials that carry no account id (see `oauthAccountKey`), so Antigravity requests prefer accounts that serve the requested model ([#14924](https://github.com/can1357/oh-my-pi/issues/14924)).
15
+
16
+ ### Fixed
17
+
18
+ - Fixed accounts sitting exactly at their `reservePct` (e.g. 70% used with a 30% reserve) still being picked and reported healthy instead of being held in reserve ([#14765](https://github.com/can1357/oh-my-pi/pull/14765) by [@will-bogusz](https://github.com/will-bogusz))
19
+ - Fixed `oauth.accessById` with `forceRefresh` returning the stored token unchanged while it was still valid; it now re-mints that one account (through the auth broker when configured) and returns that account's token even if another row is removed meanwhile ([#14752](https://github.com/can1357/oh-my-pi/pull/14752) by [@will-bogusz](https://github.com/will-bogusz))
20
+ - Fixed `omp -p` and other short-lived auth-broker clients missing from `omp usage clients`: usage still waiting for the 10-second report batch is now sent to the broker before the process exits ([#14899](https://github.com/can1357/oh-my-pi/pull/14899) by [@will-bogusz](https://github.com/will-bogusz))
21
+ - Fixed Cursor web fetches that were cut off by a dropped connection disappearing from resumed and rebuilt sessions; they now show as interrupted ([#14819](https://github.com/can1357/oh-my-pi/pull/14819) by [@jchanghong023](https://github.com/jchanghong023))
22
+ - Anthropic hosted web search can honor custom providers' OAuth-style request shaping and configured headers consistently with conversations ([#14919](https://github.com/can1357/oh-my-pi/pull/14919) by [@farnoy](https://github.com/farnoy))
23
+
24
+ ## [18.8.4] - 2026-10-08
25
+
26
+ ### Breaking Changes
27
+
28
+ - `AuthBrokerClient.notifyUsageStale` and `UsageLedgerStore.invalidateUsageCache` (including `RemoteAuthCredentialStore.invalidateUsageCache`) now take an optional leading `provider` argument: the signature is `(provider?: string, signal?: AbortSignal)` instead of `(signal?: AbortSignal)` ([#14761](https://github.com/can1357/oh-my-pi/pull/14761) by [@will-bogusz](https://github.com/will-bogusz))
29
+
30
+ ### Added
31
+
32
+ - Auth gateway route option `excludeProviders` leaves those providers' accounts out of `/v1/usage` and `/v1/credentials/check` ([#14755](https://github.com/can1357/oh-my-pi/pull/14755) by [@will-bogusz](https://github.com/will-bogusz))
33
+
34
+ ### Fixed
35
+
36
+ - Fixed a single-provider usage refresh from an auth broker client (`omp usage invalidate --provider`, Codex auto-redeem, a saved-reset redeem) wiping the broker's cached usage for every provider, so other accounts showed no usage on every client until their next successful probe ([#14761](https://github.com/can1357/oh-my-pi/pull/14761) by [@will-bogusz](https://github.com/will-bogusz))
37
+ - Fixed Anthropic sessions missing the prompt cache a second time after the API dropped thinking blocks from a changed conversation ([#14748](https://github.com/can1357/oh-my-pi/pull/14748) by [@will-bogusz](https://github.com/will-bogusz))
38
+ - Fixed Cursor logins never renewing: every refresh was rejected with "Invalid User API Key", so logins lapsed about 60 days after sign-in; a session Cursor has ended is now disabled with a re-login hint ([#14753](https://github.com/can1357/oh-my-pi/pull/14753) by [@will-bogusz](https://github.com/will-bogusz))
39
+ - Fixed the auth broker logging caller-supplied `X-Forwarded-For` / `X-Real-IP` values and unknown request paths; peers come from the socket unless `trustProxyHeaders` is set ([#14762](https://github.com/can1357/oh-my-pi/pull/14762) by [@will-bogusz](https://github.com/will-bogusz))
40
+ - Fixed false thinking-loop detections on Gemini, DeepSeek, and Grok when the model drafts or answers with repetitive code or markup (VRML, SVG, JSON); such output is no longer discarded and retried.
41
+ - Fixed Cursor provider leaking conversation checkpoints, blob stores and abort listeners after requests end ([#14669](https://github.com/can1357/oh-my-pi/pull/14669) by [@H4vC](https://github.com/H4vC))
42
+ - Fixed session-affinity pins growing without bound in long-lived gateways (now capped at 256 sessions per provider) ([#14672](https://github.com/can1357/oh-my-pi/pull/14672) by [@H4vC](https://github.com/H4vC))
43
+ - Fixed completed Cursor turns failing with "Cursor stream ended before turnEnded" when the connection closed after the answer had fully arrived ([#14851](https://github.com/can1357/oh-my-pi/pull/14851) by [@kyle-elliott-asymptote](https://github.com/kyle-elliott-asymptote)).
44
+ - Fixed Cursor provider errors that Cursor marks as not retryable being retried until the retry budget ran out ([#14851](https://github.com/can1357/oh-my-pi/pull/14851) by [@kyle-elliott-asymptote](https://github.com/kyle-elliott-asymptote)).
45
+
5
46
  ## [18.8.3] - 2026-10-07
6
47
 
7
48
  ### Added
@@ -2307,67 +2348,4 @@
2307
2348
  - Fixed `validateToolArguments` silently accepting JSON-encoded array strings (e.g. `'["a","b"]'`) against `union(string, array<string>)` schemas — providers that double-serialize tool-call arguments (Z.AI / GLM) caused tools like `search` to receive the literal `["a","b"]` as a single path, producing zero matches (single element) or glob parse errors (multi-element). A new pre-validation pass parses JSON-array-shaped strings when the schema explicitly accepts both shapes. ([#1788](https://github.com/can1357/oh-my-pi/issues/1788))
2308
2349
  - Fixed Anthropic thinking summaries that arrive wrapped in literal `<thinking>` tags so advisor/raw transcript dumps do not render nested thinking tags ([#2695](https://github.com/can1357/oh-my-pi/issues/2695)).
2309
2350
 
2310
- ## [16.0.0] - 2026-06-15
2311
-
2312
- ### Breaking Changes
2313
-
2314
- - Renamed the public dialect entrypoint from `@oh-my-pi/pi-ai/grammar` to `@oh-my-pi/pi-ai/dialect`.
2315
- - Renamed grammar dialect identifiers from `ToolCallSyntax` to `Dialect`, renamed the `Grammar` interface to `DialectDefinition`, and renamed `Grammar.syntax` to `DialectDefinition.dialect`.
2316
- - Added `DialectDefinition.renderThinking` and `DialectDefinition.renderTranscript` so dialect implementations serialize complete native chat transcripts, not just tool call/result blocks.
2317
-
2318
- ### Added
2319
-
2320
- - Added `renderTranscript` method to dialect definitions for serializing complete native chat transcripts
2321
- - Added `renderThinking` method to dialect definitions for rendering thinking/reasoning blocks
2322
- - Added support for 11 dialect implementations: Anthropic, DeepSeek, Gemini, Gemma, GLM, Harmony, Hermes, Kimi, Pi-native, Qwen3, and XML
2323
- - Added `createInbandScanner` factory function to instantiate dialect-specific scanners
2324
- - Added `getDialectDefinition` function to retrieve dialect implementations by name
2325
- - Added `renderToolCatalog` and `renderInbandToolPrompt` functions for tool catalog rendering
2326
- - Added `renderToolInventory` function to generate human-readable per-tool documentation with examples
2327
- - Added `renderToolExamples` function to render tool usage examples in the model's native dialect
2328
- - Added `encodeInbandToolHistory` function to encode tool call history in dialect-specific format
2329
- - Added `wrapInbandToolStream` function to process streaming responses with in-band tool call parsing
2330
- - Added `ThinkingInbandScanner` for parsing thinking/reasoning blocks across dialects
2331
- - Added `OwnedStream` class for managing dialect-aware streaming with tool call events
2332
- - Added in-band thinking channels to every dialect that was missing one: `gemini` (a ` ```thinking ` fence mirroring ` ```tool_code `), `gemma` (its native `<|channel>thought…<channel|>` reasoning channel), `kimi` (`<think>…</think>`), and `pi` (`<thinking>…</thinking>`). Each scanner now parses reasoning into thinking events instead of leaking chain-of-thought into the visible reply, and every dialect's `renderThinking` is a real channel that round-trips back through its scanner (no passthrough renderers).
2333
-
2334
- ### Changed
2335
-
2336
- - Moved public dialect entrypoint from `@oh-my-pi/pi-ai/grammar` to `@oh-my-pi/pi-ai/dialect` in package exports
2337
- - Updated internal imports in `stream-markup-healing.ts` to use new dialect module path
2338
- - Changed `renderToolInventory` to demote a tool description's own markdown headers by one level when it contains a top-level `# ` header, so they nest under the wrapping `# Tool: <name>` heading instead of reading as sibling sections. Descriptions that already start at `##` and headers inside fenced code blocks are left untouched.
2339
-
2340
- ### Fixed
2341
-
2342
- - Fixed Gemini, Gemma, Kimi, and Pi in-band scanners to respect `parseThinking: false`, leaving private reasoning markers in visible text when parsing is disabled
2343
- - Fixed thinking-channel parsing for streaming Gemini, Gemma, Kimi, and Pi outputs so split or partial `<thinking>` blocks no longer leak into visible replies
2344
- - Fixed in-band thinking finalization and Kimi stream-healing interactions so leaked `<think>` blocks are preserved when structured tool calls are present, not duplicated when explicit reasoning is present, and closed on stream flush.
2345
-
2346
- ### Removed
2347
-
2348
- - Removed `src/grammar/factory.ts` (replaced by `src/dialect/factory.ts`)
2349
- - Removed `src/grammar/rendering.ts` (functionality moved to `src/dialect/rendering.ts`)
2350
- - Removed `src/grammar/xml.ts` (replaced by `src/dialect/xml.ts`)
2351
-
2352
- ## [15.13.3] - 2026-06-15
2353
-
2354
- ### Added
2355
-
2356
- - Added the `gemini` in-band tool-call syntax with Python-style `tool_code` blocks and `default_api` invocations
2357
- - Added the `gemma` token-delimited in-band tool-call syntax using `<|tool_call>` and `<|tool_response>` blocks
2358
- - Added `gemini` and `gemma` to owned stream tool-result token detection so their tool responses are recognized
2359
- - Fixed truncated Gemini and Gemma tool blocks from being emitted as plain text during streaming
2360
- - Added the Azure OpenAI provider definition (`azure`) to the registry; `AZURE_OPENAI_API_KEY` resolves as its env-var API key via the catalog provider table.
2361
-
2362
- ### Changed
2363
-
2364
- - Gemini tool-call examples now render without the `default_api.` namespace prefix, keeping `<example>` blocks concise. The live wire format still uses `default_api.` per the Gemini grammar.
2365
-
2366
- ### Fixed
2367
-
2368
- - Fixed duplicate tool call projections by deduplicating provider-native `toolCall` events against in-band `tool_code` calls and keeping only the first real channel
2369
- - Dropped nameless native `toolCall` events so they no longer appear as surfaced tool calls in owned-mode streams
2370
- - Fixed truncated Gemini and Gemma tool blocks from being emitted as plain text during streaming
2371
- - Fixed Gemini/Gemma in-band tool-call parsing around Python comments, raw/unicode string literals, and Gemma close-token text inside string values.
2372
-
2373
- Older entries are archived in [packages/ai/CHANGELOG.md@a20cc0d04b64](https://github.com/can1357/oh-my-pi/blob/a20cc0d04b64f4a68fcc559cbd258743d984c50e/packages/ai/CHANGELOG.md).
2351
+ Older entries are archived in [packages/ai/CHANGELOG.md@d22e34333695](https://github.com/can1357/oh-my-pi/blob/d22e34333695d3dd9de7741ff45903681dba4606/packages/ai/CHANGELOG.md).
@@ -79,14 +79,15 @@ export declare class SessionAffinity implements SessionsApi {
79
79
  restoredAtMs?: number;
80
80
  }): boolean;
81
81
  /**
82
- * Copy every stored credential affinity from one live session to another.
82
+ * Copy stored credential affinities from one live session to another.
83
83
  *
84
84
  * The target receives its own sticky entries, so request resolution, usage
85
85
  * blocking, credential rotation, metadata, and persisted pins all continue
86
86
  * through the target session id without retaining a live dependency on the
87
- * source session.
87
+ * source session. `include` limits the copy to the providers it accepts,
88
+ * given whether the source's affinity is an explicit user pin.
88
89
  */
89
- inherit(sourceSessionId: string, targetSessionId: string): number;
90
+ inherit(sourceSessionId: string, targetSessionId: string, include?: (provider: string, explicit: boolean) => boolean): number;
90
91
  /**
91
92
  * Release a session's sticky credential so its next `KeyCascade.get` call
92
93
  * re-runs native pool ranking. This never blocks or penalizes the released
@@ -67,9 +67,10 @@ export declare class OAuthAccounts implements OAuthApi {
67
67
  * Resolve one stored OAuth credential by its durable storage row id.
68
68
  *
69
69
  * Unlike the normal session resolver, this method never ranks, rotates, or
70
- * falls back to sibling credentials. A forced refresh re-mints only the
71
- * requested row, preserving exact-account affinity for operations whose
72
- * provenance and policy boundary are tied to one workspace.
70
+ * falls back to sibling credentials. A generic forced refresh re-mints only the
71
+ * requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
72
+ * still-usable recent mint of that row. Either way exact-account affinity holds
73
+ * for operations whose provenance and policy boundary are tied to one workspace.
73
74
  *
74
75
  * Returns `undefined` when the row does not exist for `provider` or an
75
76
  * explicit runtime/config API-key override suppresses OAuth.
@@ -32,6 +32,7 @@ export interface OAuthRefresherDeps {
32
32
  pool: CredentialPool;
33
33
  policies: AccountPolicies;
34
34
  override?: AuthStorageOptions["refreshOAuthCredential"];
35
+ overrideMints?: AuthStorageOptions["refreshOAuthCredentialMints"];
35
36
  }
36
37
  /** Single-flighted, lease-guarded OAuth refresh with compare-and-set persistence. */
37
38
  export declare class OAuthRefresher {
@@ -8,7 +8,7 @@ import { type CredentialPool } from "./pool.js";
8
8
  import { type ApiKeySelection, type OAuthSelection } from "./rank.js";
9
9
  import { type OAuthRefresher } from "./refresh.js";
10
10
  import type { AuthCredentialStore } from "./store.js";
11
- import type { ApiKeyCredential, AuthApiKeyOptions, AuthCredential, OAuthCredential } from "./types.js";
11
+ import { type ApiKeyCredential, type AuthApiKeyOptions, type AuthCredential, type OAuthCredential } from "./types.js";
12
12
  import type { UsageService } from "./usage.js";
13
13
  /** Temporary block after a transient OAuth refresh failure. */
14
14
  export declare const OAUTH_REFRESH_FAILURE_BACKOFF_MS: number;
@@ -122,8 +122,8 @@ export interface UsageLedgerStore {
122
122
  }
123
123
  /** Broker-delegated OAuth refresh and usage-report operations. */
124
124
  export interface CredentialUpstream {
125
- /** Optional hook to notify the underlying store that usage report cache is stale. */
126
- invalidateUsageCache(signal?: AbortSignal): Promise<void>;
125
+ /** Optional hook to notify the underlying store that usage reports (one provider's, when given) are stale. */
126
+ invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
127
127
  /**
128
128
  * Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
129
129
  * it before the per-provider local refresh path. `RemoteAuthCredentialStore`
@@ -316,6 +316,12 @@ export type AuthStorageOptions = {
316
316
  * the actual refresh token never leaves the broker.
317
317
  */
318
318
  refreshOAuthCredential?: (provider: Provider, credentialId: number, credential: OAuthCredential, signal?: AbortSignal, reason?: OAuthRefreshReason) => Promise<OAuthCredentials>;
319
+ /**
320
+ * Set when {@link refreshOAuthCredential} exchanges the refresh token with the
321
+ * provider itself instead of delegating (e.g. to a broker), so its tokens count
322
+ * as this process's mints for auth-recovery reuse.
323
+ */
324
+ refreshOAuthCredentialMints?: boolean;
319
325
  /**
320
326
  * Human-readable description of the credential store backing this
321
327
  * AuthStorage instance. Surfaced through {@link AuthStorage.keys.describe}
@@ -409,7 +415,7 @@ export interface ModelUsageHealthOptions {
409
415
  export type AuthApiKeyOptions = {
410
416
  baseUrl?: string;
411
417
  modelId?: string;
412
- /** Provider account ids known to serve `modelId` from multi-account discovery; OAuth selection prefers them and tries other accounts only as a last resort. */
418
+ /** {@link oauthAccountKey}s of accounts known to serve `modelId` from multi-account discovery; OAuth selection prefers them and tries other accounts only as a last resort. */
413
419
  accountIds?: readonly string[];
414
420
  /**
415
421
  * Caller's cancel signal. Threaded into any broker-bound OAuth refresh so
@@ -429,6 +435,19 @@ export type AuthApiKeyOptions = {
429
435
  /** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
430
436
  recordAffinity?: boolean;
431
437
  };
438
+ /**
439
+ * Identity that multi-account discovery records in `Model.accountAccess` and
440
+ * that {@link AuthApiKeyOptions.accountIds} selection matches: the provider
441
+ * account id (Codex `chatgpt_account_id`), else the login email for providers
442
+ * whose credentials carry no account id (Antigravity), else the project id for
443
+ * a login whose email lookup failed. Same precedence as the credential store's
444
+ * identity key.
445
+ */
446
+ export declare function oauthAccountKey(identity: {
447
+ accountId?: string;
448
+ email?: string;
449
+ projectId?: string;
450
+ }): string | undefined;
432
451
  /** Non-secret identity bound to the OAuth credential selected for one request attempt. */
433
452
  export interface OAuthRequestIdentity {
434
453
  orgId?: string;
@@ -917,7 +936,7 @@ export interface KeysApi {
917
936
  modelId?: string;
918
937
  }): ApiKeyResolver;
919
938
  }
920
- /** Controls whether a row-id refresh may reuse a token minted by this refresher. */
939
+ /** Controls whether a row-id refresh may reuse a recently minted token. */
921
940
  export interface OAuthRefreshByIdOptions {
922
941
  /**
923
942
  * Return the stored credential when it still holds a fresh access token this
@@ -925,6 +944,12 @@ export interface OAuthRefreshByIdOptions {
925
944
  * refresh tokens repeatedly when a provider rejects every valid bearer.
926
945
  */
927
946
  reuseRecentMint?: boolean;
947
+ /**
948
+ * Provider-401 recovery intent. Forwarded to a delegated refresh (an auth
949
+ * broker), which may then return a token it minted in the last five minutes.
950
+ * Local reuse still requires {@link reuseRecentMint}.
951
+ */
952
+ reason?: OAuthRefreshReason;
928
953
  }
929
954
  /** OAuth login, access, account identity, and refresh operations. */
930
955
  export interface OAuthApi {
@@ -966,9 +991,10 @@ export interface OAuthApi {
966
991
  * Resolve one stored OAuth credential by its durable storage row id.
967
992
  *
968
993
  * Unlike the normal session resolver, this method never ranks, rotates, or
969
- * falls back to sibling credentials. A forced refresh re-mints only the
970
- * requested row, preserving exact-account affinity for operations whose
971
- * provenance and policy boundary are tied to one workspace.
994
+ * falls back to sibling credentials. A generic forced refresh re-mints only the
995
+ * requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
996
+ * still-usable recent mint of that row. Either way exact-account affinity holds
997
+ * for operations whose provenance and policy boundary are tied to one workspace.
972
998
  *
973
999
  * Returns `undefined` when the row does not exist for `provider` or an
974
1000
  * explicit runtime/config API-key override suppresses OAuth.
@@ -1002,8 +1028,9 @@ export interface OAuthApi {
1002
1028
  * Refresh the OAuth credential with the given id through a per-credential
1003
1029
  * single-flight. Concurrent callers for the same row await the same upstream
1004
1030
  * refresh attempt, which is required for providers that rotate refresh tokens
1005
- * on every successful refresh. Mints unconditionally unless
1006
- * {@link OAuthRefreshByIdOptions.reuseRecentMint} is set.
1031
+ * on every successful refresh. Mints unless {@link OAuthRefreshByIdOptions.reuseRecentMint}
1032
+ * finds a recent local mint, or a delegated refresh given
1033
+ * {@link OAuthRefreshByIdOptions.reason} `"auth-recovery"` returns its recent mint.
1007
1034
  */
1008
1035
  refresh(id: number, signal?: AbortSignal, options?: OAuthRefreshByIdOptions): Promise<AuthCredentialSnapshotEntry>;
1009
1036
  /**
@@ -1035,14 +1062,15 @@ export interface SessionsApi {
1035
1062
  restoredAtMs?: number;
1036
1063
  }): boolean;
1037
1064
  /**
1038
- * Copy every stored credential affinity from one live session to another.
1065
+ * Copy stored credential affinities from one live session to another.
1039
1066
  *
1040
1067
  * The target receives its own sticky entries, so request resolution, usage
1041
1068
  * blocking, credential rotation, metadata, and persisted pins all continue
1042
1069
  * through the target session id without retaining a live dependency on the
1043
- * source session.
1070
+ * source session. `include` limits the copy to the providers it accepts,
1071
+ * given whether the source's affinity is an explicit user pin.
1044
1072
  */
1045
- inherit(sourceSessionId: string, targetSessionId: string): number;
1073
+ inherit(sourceSessionId: string, targetSessionId: string, include?: (provider: string, explicit: boolean) => boolean): number;
1046
1074
  /**
1047
1075
  * Restrict one session's credentials for `provider` to the OAuth accounts
1048
1076
  * whose identity key (`email:<address>|org:<id>` for org-scoped providers;
@@ -85,7 +85,7 @@ export declare class AuthBrokerClient {
85
85
  fetchClientUsageSummary(query?: {
86
86
  sinceMs?: number;
87
87
  }, signal?: AbortSignal): Promise<ClientUsageSummaryResponse>;
88
- notifyUsageStale(signal?: AbortSignal): Promise<UsageStaleResponse>;
88
+ notifyUsageStale(provider?: string, signal?: AbortSignal): Promise<UsageStaleResponse>;
89
89
  refreshCredential(id: number, signal?: AbortSignal, reason?: OAuthRefreshReason): Promise<CredentialRefreshResponse>;
90
90
  disableCredential(id: number, cause: string, signal?: AbortSignal): Promise<CredentialDisableResponse>;
91
91
  /**
@@ -122,7 +122,7 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
122
122
  /** Drop all cache rows whose keys start with the supplied prefix. */
123
123
  deleteCachePrefix(prefix: string): void;
124
124
  cleanExpiredCache(): void;
125
- invalidateUsageCache(signal?: AbortSignal): Promise<void>;
125
+ invalidateUsageCache(provider?: string, signal?: AbortSignal): Promise<void>;
126
126
  /**
127
127
  * Store-level hook consumed by `AuthStorage` — routes refresh through the
128
128
  * broker so the actual refresh token never leaves the broker host. Returns
@@ -149,9 +149,10 @@ export declare class RemoteAuthCredentialStore implements AuthCredentialStore {
149
149
  ingestUsageReport(provider: Provider, credential: OAuthCredential, report: UsageReport): boolean;
150
150
  /**
151
151
  * Fold locally observed request usage into the pending report and schedule
152
- * a flush. One `POST /v1/usage/observed` at most per flush interval; on
153
- * failure the batch is retained and retried with the next flush. A 404
154
- * (pre-endpoint broker) disables reporting for the life of this store.
152
+ * a flush. One `POST /v1/usage/observed` at most per flush interval, plus a
153
+ * final one when the process exits; on failure the batch is retained and
154
+ * retried with the next flush. A 404 (pre-endpoint broker) disables
155
+ * reporting for the life of this store.
155
156
  *
156
157
  * `client` overrides the reporting identity — the auth-gateway attributes
157
158
  * each request to the originating install/app instead of the gateway host.
@@ -17,6 +17,8 @@ export interface AuthBrokerServerOptions {
17
17
  bind?: string;
18
18
  /** Accept any of these bearer tokens. Empty disables auth (loopback only). */
19
19
  bearerTokens: string[];
20
+ /** Honor forwarded peer headers only when the connecting proxy is trusted. Default false. */
21
+ trustProxyHeaders?: boolean;
20
22
  /** Broker version string surfaced on `/v1/healthz`. */
21
23
  version?: string;
22
24
  /** Refresh credentials expiring within this window. Default 5 min. */
@@ -17,6 +17,8 @@ export interface AuthGatewayRouteOptions {
17
17
  resolveModel: ModelResolver;
18
18
  /** Optional supplier for `/v1/models` listing. Returns the full model array. */
19
19
  listModels?: () => Iterable<Model<Api>>;
20
+ /** Providers the host does not serve; `/v1/usage` and `/v1/credentials/check` leave their accounts out. */
21
+ excludeProviders?: ReadonlySet<string>;
20
22
  /** Upstream transport for every provider call; defaults to global `fetch`. Test seam. */
21
23
  fetch?: FetchImpl;
22
24
  }
@@ -16,8 +16,6 @@ export declare function gatewayResponseHeaders(model: Model<Api>, info: {
16
16
  costUsd?: number;
17
17
  startedAt?: number;
18
18
  }): Record<string, string>;
19
- /** Use the socket peer unless the gateway explicitly trusts its reverse proxy. */
20
- export declare function resolvePeer(req: Request, socketAddress: string, trustProxyHeaders?: boolean): string;
21
19
  /** Keep configured gateway credentials out of URL and forwarded/logged request fields. */
22
20
  export declare function hasMisplacedBearer(req: Request, url: URL, tokens: ReadonlySet<string>): boolean;
23
21
  /**
@@ -18,8 +18,8 @@
18
18
  * siblings are endpoint-scoped and stay: `strictToolsDisabled`
19
19
  * (grammar-too-large 400 for the model's tool schema),
20
20
  * `replayUnsignedThinkingDisabled` / `thinkingReplayDisabled` (the endpoint
21
- * is a signing proxy), `prefixDroppedThinkingBlocks` (blocks the API itself
22
- * dropped).
21
+ * is a signing proxy), `prefixDroppedThinkingBlocks` (blocks a prefix-binding
22
+ * 400 made the client strip).
23
23
  * - **OpenAI Responses** — the `previous_response_id` chain baselines are
24
24
  * account-scoped: a stored response belongs to the account that created it.
25
25
  * Strict-tools / reasoning-effort fallbacks, replay warmup and the chaining
@@ -169,6 +169,12 @@ export type AnthropicClientOptionsResult = {
169
169
  * omitted there.
170
170
  */
171
171
  export declare function resolveAnthropicCustomHeadersForBaseUrl(baseUrl: string | undefined): Record<string, string> | undefined;
172
+ /**
173
+ * Merge headers case-insensitively; later sources win and preserve their key casing.
174
+ * Undefined sources are ignored. Differently cased duplicates are removed so the
175
+ * Headers constructor cannot join competing values comma-separated on the wire.
176
+ */
177
+ export declare function mergeHeaders(...headerSources: (Record<string, string> | undefined)[]): Record<string, string>;
172
178
  export type AnthropicUsageLike = {
173
179
  cache_creation?: {
174
180
  ephemeral_5m_input_tokens?: number | null;
@@ -169,13 +169,13 @@ export declare function emptyGrepPatternRejection(pattern: string | undefined, g
169
169
  * so reparsing unconditionally would erase the arguments of every such block
170
170
  * caught open by a truncated stream.
171
171
  *
172
- * Server-owned blocks are also paired here. `connect-scm` and `todo` are
173
- * stamped {@link kCursorExecResolved} the moment they open, so `agent-loop.ts`
174
- * synthesizes no placeholder for them and only their `toolCallCompleted` frame
175
- * pairs a result. A transport that closes before that frame would leave the
176
- * call unpaired, and `buildSessionContext` strips a dangling call from every
177
- * rebuilt transcript — the interaction disappears. An interrupted result is
178
- * emitted instead.
172
+ * Server-owned blocks are also paired here. `connect-scm`, `todo` and
173
+ * `web-fetch` are stamped {@link kCursorExecResolved} the moment they open,
174
+ * so `agent-loop.ts` synthesizes no placeholder for them and only their
175
+ * `toolCallCompleted` frame pairs a result. A transport that closes before
176
+ * that frame would leave the call unpaired, and `buildSessionContext` strips
177
+ * a dangling call from every rebuilt transcript — the interaction disappears.
178
+ * An interrupted result is emitted instead.
179
179
  *
180
180
  * MCP blocks are excluded even when resolved: the exec dispatch that marked
181
181
  * them owns their result, and `drainInFlightDispatches` awaits it before this
@@ -13,7 +13,7 @@ export declare function pollCursorAuth(uuid: string, verifier: string): Promise<
13
13
  }>;
14
14
  export declare function loginCursor(onAuthUrl: (url: string) => void, onPollStart?: () => void): Promise<OAuthCredentials>;
15
15
  export declare function loginCursorHook(callbacks: OAuthController): Promise<OAuthCredentials>;
16
- export declare function refreshCursorToken(apiKeyOrRefreshToken: string): Promise<OAuthCredentials>;
16
+ export declare function refreshCursorToken(refreshToken: string): Promise<OAuthCredentials>;
17
17
  export declare function refreshCursorHook(credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
18
18
  /** Request headers that present `accessToken` to cursor.com as its user's web session. */
19
19
  export declare function cursorSessionHeaders(userId: string, accessToken: string): Record<string, string>;
@@ -142,6 +142,8 @@ export interface UsageReport {
142
142
  * explicit fraction > used/limit > percent-unit used > inverted remaining.
143
143
  */
144
144
  export declare function resolveUsedFraction(limit: UsageLimit): number | undefined;
145
+ /** Whether remaining quota is at or below the reserve (both 0..1 fractions). */
146
+ export declare function isWithinUsageReserve(remainingFraction: number, reserveFraction: number): boolean;
145
147
  /**
146
148
  * One recorded usage-limit snapshot: a single limit window of one account at
147
149
  * a point in time. The usage cache itself is latest-snapshot-only; history
@@ -9,6 +9,11 @@ export declare function resolveAnthropicBaseUrlFromEnv(): string | undefined;
9
9
  * Checks if a token is an OAuth token by looking for sk-ant-oat prefix.
10
10
  */
11
11
  export declare function isOAuthToken(apiKey: string): boolean;
12
+ /** Optional request-shaping overrides for an already-resolved Anthropic credential. */
13
+ export interface AnthropicAuthOptions {
14
+ /** Override token detection; false explicitly disables OAuth-style request shaping. */
15
+ isOAuth?: boolean;
16
+ }
12
17
  /**
13
18
  * Build an {@link AnthropicAuthConfig} from an already-resolved API key.
14
19
  *
@@ -17,18 +22,27 @@ export declare function isOAuthToken(apiKey: string): boolean;
17
22
  * env-derived base; pass `undefined` to fall back to FOUNDRY/ANTHROPIC env
18
23
  * resolution and finally `DEFAULT_BASE_URL`.
19
24
  *
20
- * `isOAuth` is derived from the token prefix so the helper stays pure: callers
21
- * never have to thread the OAuth flag through their own resolution logic.
25
+ * `options.isOAuth` defaults to token detection. Pass the selected model's
26
+ * `isOAuth` to match the streaming path, which lets the model override the
27
+ * prefix (custom `anthropic-messages` providers default to OAuth shaping).
22
28
  */
23
- export declare function buildAnthropicAuthConfig(apiKey: string, baseUrl?: string): AnthropicAuthConfig;
29
+ export declare function buildAnthropicAuthConfig(apiKey: string, baseUrl?: string, options?: AnthropicAuthOptions): AnthropicAuthConfig;
30
+ /** Configured headers and fingerprint override policy for Anthropic hosted web search. */
31
+ export interface AnthropicSearchHeaderOptions {
32
+ /** The selected model's configured headers (`ModelRegistry.resolveModelHeaders`). */
33
+ modelHeaders?: Record<string, string>;
34
+ /** The selected model's `compat.allowAnthropicHeaderOverrides`. */
35
+ allowAnthropicHeaderOverrides?: boolean;
36
+ }
24
37
  /**
25
38
  * Builds HTTP headers for Anthropic API requests (search variant).
26
39
  *
27
- * Forwards `ANTHROPIC_CUSTOM_HEADERS` when the resolver deems them applicable
28
- * (Foundry mode, or a non-Anthropic base URL — typically an enterprise
29
- * gateway), matching the streaming path so web search behaves identically.
40
+ * Model headers and `ANTHROPIC_CUSTOM_HEADERS` (when the resolver deems them
41
+ * applicable: Foundry mode, or a non-Anthropic base URL such as an enterprise
42
+ * gateway) go through the same case-insensitive merge and enforced-header
43
+ * filtering as the streaming path, so web search behaves identically.
30
44
  */
31
- export declare function buildAnthropicSearchHeaders(auth: AnthropicAuthConfig): Record<string, string>;
45
+ export declare function buildAnthropicSearchHeaders(auth: AnthropicAuthConfig, options?: AnthropicSearchHeaderOptions): Record<string, string>;
32
46
  /**
33
47
  * Builds the full API URL for Anthropic messages endpoint.
34
48
  */
@@ -0,0 +1,2 @@
1
+ /** Use the socket peer unless the server explicitly trusts its reverse proxy. */
2
+ export declare function resolvePeer(req: Request, socketAddress: string, trustProxyHeaders?: boolean): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.8.3",
3
+ "version": "18.8.5",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,11 +155,11 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.8.3",
159
- "@oh-my-pi/pi-catalog": "18.8.3",
160
- "@oh-my-pi/pi-natives": "18.8.3",
161
- "@oh-my-pi/pi-utils": "18.8.3",
162
- "@oh-my-pi/pi-wire": "18.8.3"
158
+ "@oh-my-pi/omptype": "18.8.5",
159
+ "@oh-my-pi/pi-catalog": "18.8.5",
160
+ "@oh-my-pi/pi-natives": "18.8.5",
161
+ "@oh-my-pi/pi-utils": "18.8.5",
162
+ "@oh-my-pi/pi-wire": "18.8.5"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
@@ -360,19 +360,25 @@ export class SessionAffinity implements SessionsApi {
360
360
  }
361
361
 
362
362
  /**
363
- * Copy every stored credential affinity from one live session to another.
363
+ * Copy stored credential affinities from one live session to another.
364
364
  *
365
365
  * The target receives its own sticky entries, so request resolution, usage
366
366
  * blocking, credential rotation, metadata, and persisted pins all continue
367
367
  * through the target session id without retaining a live dependency on the
368
- * source session.
368
+ * source session. `include` limits the copy to the providers it accepts,
369
+ * given whether the source's affinity is an explicit user pin.
369
370
  */
370
- inherit(sourceSessionId: string, targetSessionId: string): number {
371
+ inherit(
372
+ sourceSessionId: string,
373
+ targetSessionId: string,
374
+ include?: (provider: string, explicit: boolean) => boolean,
375
+ ): number {
371
376
  if (!sourceSessionId || !targetSessionId || sourceSessionId === targetSessionId) return 0;
372
377
  let inherited = 0;
373
378
  for (const provider of this.#pool.providers()) {
374
379
  const credential = this.get(provider, sourceSessionId);
375
380
  if (!credential || !this.#permits(provider, targetSessionId, credential.index)) continue;
381
+ if (include && !include(provider, credential.explicit === true)) continue;
376
382
  this.record(
377
383
  provider,
378
384
  targetSessionId,
@@ -1,5 +1,5 @@
1
1
  import type { Provider } from "../types";
2
- import { resolveUsedFraction } from "../usage";
2
+ import { isWithinUsageReserve, resolveUsedFraction } from "../usage";
3
3
  import type {
4
4
  CredentialRankingContext,
5
5
  UsageCredential,
@@ -224,7 +224,7 @@ export class CredentialHealth implements HealthApi {
224
224
  return {
225
225
  credentialId: entry.id,
226
226
  credentialType,
227
- state: remainingFraction <= resolveReserveFraction(entry) ? "reserve" : "healthy",
227
+ state: isWithinUsageReserve(remainingFraction, resolveReserveFraction(entry)) ? "reserve" : "healthy",
228
228
  remainingFraction,
229
229
  };
230
230
  }),
package/src/auth/oauth.ts CHANGED
@@ -179,9 +179,20 @@ export class OAuthAccounts implements OAuthApi {
179
179
  options: AuthApiKeyOptions | undefined,
180
180
  ): Promise<OAuthAccessResolution> {
181
181
  try {
182
+ // tryOAuth refreshes only expired tokens; it resyncs this row from the store after a forced re-mint.
183
+ let index = selection.index;
184
+ if (options?.forceRefresh) {
185
+ await this.refresh(selection.credentialId, options.signal, {
186
+ reuseRecentMint: options.refreshReason === "auth-recovery",
187
+ reason: options.refreshReason,
188
+ });
189
+ // The refresh re-lists rows, so a concurrently removed row can shift this one's position.
190
+ index = this.#deps.pool.entries(provider).findIndex(entry => entry.id === selection.credentialId);
191
+ if (index === -1) throw new Error(`OAuth credential ${selection.credentialId} was removed during refresh`);
192
+ }
182
193
  const resolved = await this.#deps.selector.tryOAuth(
183
194
  provider,
184
- { credential: selection.credential, index: selection.index },
195
+ { credential: selection.credential, index },
185
196
  providerKey,
186
197
  undefined,
187
198
  options,
@@ -289,9 +300,10 @@ export class OAuthAccounts implements OAuthApi {
289
300
  * Resolve one stored OAuth credential by its durable storage row id.
290
301
  *
291
302
  * Unlike the normal session resolver, this method never ranks, rotates, or
292
- * falls back to sibling credentials. A forced refresh re-mints only the
293
- * requested row, preserving exact-account affinity for operations whose
294
- * provenance and policy boundary are tied to one workspace.
303
+ * falls back to sibling credentials. A generic forced refresh re-mints only the
304
+ * requested row; with `refreshReason: "auth-recovery"` it may instead reuse a
305
+ * still-usable recent mint of that row. Either way exact-account affinity holds
306
+ * for operations whose provenance and policy boundary are tied to one workspace.
295
307
  *
296
308
  * Returns `undefined` when the row does not exist for `provider` or an
297
309
  * explicit runtime/config API-key override suppresses OAuth.