@oh-my-pi/pi-ai 18.8.4 → 18.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -16
- package/dist/types/auth/affinity.d.ts +4 -3
- package/dist/types/auth/oauth.d.ts +4 -3
- package/dist/types/auth/refresh.d.ts +1 -0
- package/dist/types/auth/select.d.ts +1 -1
- package/dist/types/auth/types.d.ts +38 -10
- package/dist/types/auth-broker/remote-store.d.ts +4 -3
- package/dist/types/providers/anthropic.d.ts +6 -0
- package/dist/types/providers/cursor.d.ts +7 -7
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +21 -7
- package/dist/types/utils/glyph-codec.d.ts +1 -1
- package/dist/types/utils/schema/normalize.d.ts +8 -3
- package/package.json +6 -6
- package/src/auth/affinity.ts +9 -3
- package/src/auth/health.ts +2 -2
- package/src/auth/oauth.ts +16 -4
- package/src/auth/refresh.ts +17 -9
- package/src/auth/select.ts +17 -12
- package/src/auth/types.ts +46 -11
- package/src/auth-broker/remote-store.ts +81 -14
- package/src/auth-storage.ts +7 -1
- package/src/providers/anthropic.ts +6 -4
- package/src/providers/cursor.ts +8 -8
- package/src/usage.ts +8 -0
- package/src/utils/anthropic-auth.ts +34 -10
- package/src/utils/glyph-codec.ts +200 -7
- package/src/utils/schema/CONSTRAINTS.md +9 -7
- package/src/utils/schema/fields.ts +20 -0
- package/src/utils/schema/normalize.ts +64 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.8.6] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Google Gemini and Cloud Code Assist (Antigravity) requests failing when tool schemas contain unsupported JSON Schema keywords or fields that allow multiple types.
|
|
10
|
+
- Fixed auth broker account selection and usage-limit enforcement to consistently use the selected account’s quota when multiple accounts are present.
|
|
11
|
+
- Fixed Anthropic-family model streaming so encoded marker tokens are decoded correctly in text, tool-call updates, partial messages, and completed tool calls.
|
|
12
|
+
|
|
13
|
+
## [18.8.5] - 2026-10-08
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `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))
|
|
18
|
+
- `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))
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `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)).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- 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))
|
|
27
|
+
- 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))
|
|
28
|
+
- 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))
|
|
29
|
+
- 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))
|
|
30
|
+
- 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))
|
|
31
|
+
|
|
5
32
|
## [18.8.4] - 2026-10-08
|
|
6
33
|
|
|
7
34
|
### Breaking Changes
|
|
@@ -2314,19 +2341,4 @@
|
|
|
2314
2341
|
- Fixed OpenAI-compatible Ollama completions that return empty `finish_reason:length` after filling `num_ctx` so they surface an actionable context-window error instead of an empty length stop. ([#2774](https://github.com/can1357/oh-my-pi/issues/2774))
|
|
2315
2342
|
- Fixed Codex browser login issuing credentials for the `opencode` OAuth originator while OMP requests identify as `pi`, which could make the first authenticated Codex request return 401 ([#2696](https://github.com/can1357/oh-my-pi/issues/2696)).
|
|
2316
2343
|
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
### Added
|
|
2320
|
-
|
|
2321
|
-
- Added Umans AI Coding Plan API-key login support and `UMANS_AI_CODING_PLAN_API_KEY` environment fallback ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)).
|
|
2322
|
-
|
|
2323
|
-
### Fixed
|
|
2324
|
-
|
|
2325
|
-
- Fixed OpenAI Responses, Azure OpenAI Responses, and Codex Responses providers ignoring async `onPayload` replacement bodies. Provider payload hooks can now transform the actual request body sent upstream, matching the Anthropic/Gemini replacement contract.
|
|
2326
|
-
- Fixed OpenAI-compatible chat-completions streams that send object-shaped tool arguments in fragments by deep-merging nested objects and task arrays instead of replacing earlier chunks. ([#2617](https://github.com/can1357/oh-my-pi/issues/2617))
|
|
2327
|
-
- Fixed OpenAI Responses strict-mode tool schema normalization for nullable enum MCP parameters so enum constraints are distributed to matching `anyOf` branches instead of being copied onto the `null` branch. ([#1835](https://github.com/can1357/oh-my-pi/issues/1835))
|
|
2328
|
-
- Fixed Cursor provider formatting tool errors with the same `[Tool Result]` prefix as successful results, causing Composer models to misinterpret error messages (e.g. "Pattern must not be empty") as directives over long conversations. Errors now use a `[Tool Error]` prefix so the model can distinguish failures from successes in the prompt history. ([#1853](https://github.com/can1357/oh-my-pi/pull/1853))
|
|
2329
|
-
- 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))
|
|
2330
|
-
- 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)).
|
|
2331
|
-
|
|
2332
|
-
Older entries are archived in [packages/ai/CHANGELOG.md@d22e34333695](https://github.com/can1357/oh-my-pi/blob/d22e34333695d3dd9de7741ff45903681dba4606/packages/ai/CHANGELOG.md).
|
|
2344
|
+
Older entries are archived in [packages/ai/CHANGELOG.md@0dd6aff5f282](https://github.com/can1357/oh-my-pi/blob/0dd6aff5f2821aec1e5b54c6a458e323667a949b/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
|
|
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
|
|
72
|
-
*
|
|
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
|
|
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;
|
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
971
|
-
*
|
|
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
|
|
1006
|
-
*
|
|
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
|
|
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;
|
|
@@ -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
|
|
153
|
-
*
|
|
154
|
-
* (pre-endpoint broker) disables
|
|
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.
|
|
@@ -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
|
|
173
|
-
* stamped {@link kCursorExecResolved} the moment they open,
|
|
174
|
-
* synthesizes no placeholder for them and only their
|
|
175
|
-
* pairs a result. A transport that closes before
|
|
176
|
-
* call unpaired, and `buildSessionContext` strips
|
|
177
|
-
* rebuilt transcript — the interaction disappears.
|
|
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
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -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`
|
|
21
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* gateway)
|
|
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
|
*/
|
|
@@ -4,7 +4,7 @@ import { AssistantMessageEventStream } from "./event-stream.js";
|
|
|
4
4
|
export interface GlyphCodec {
|
|
5
5
|
/** Context with model-bound glyphs encoded and one conditional convention notice. */
|
|
6
6
|
context: Context;
|
|
7
|
-
/** Decodes glyph tokens
|
|
7
|
+
/** Decodes glyph tokens live: text and tool-call deltas, partial snapshots, completed tool calls, and the terminal message. */
|
|
8
8
|
wrap(inner: AssistantMessageEventStream): AssistantMessageEventStream;
|
|
9
9
|
/** Decodes Cursor Pi calls and encodes their provider-bound results. */
|
|
10
10
|
wrapCursorExecHandlers(handlers: CursorExecHandlers): CursorExecHandlers;
|
|
@@ -10,7 +10,11 @@ export interface NormalizeSchemaOptions {
|
|
|
10
10
|
unsupportedFields: (key: string) => boolean;
|
|
11
11
|
normalizeFieldNames: boolean;
|
|
12
12
|
collapseNullFields: boolean;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Target takes a scalar `type` only: `["T", "null"]` becomes `T` (+ `nullable`)
|
|
15
|
+
* and a multi-type array becomes one `anyOf` branch per type.
|
|
16
|
+
*/
|
|
17
|
+
scalarizeTypeArrays: boolean;
|
|
14
18
|
stripNullableKeyword: boolean;
|
|
15
19
|
autoPropertyOrdering: boolean;
|
|
16
20
|
ensureObjectProperties: boolean;
|
|
@@ -66,8 +70,9 @@ export declare function normalizeSchemaForMCP(value: unknown): unknown;
|
|
|
66
70
|
* rejected; collapse to `enum` with an inferred scalar `type`.
|
|
67
71
|
* - `oneOf` is not an MFJS combinator (only `anyOf` is); residual `oneOf` is
|
|
68
72
|
* folded into `anyOf`.
|
|
69
|
-
* - `type` must be a scalar string; `
|
|
70
|
-
*
|
|
73
|
+
* - `type` must be a scalar string; `["T", "null"]` reduces to `T` (the
|
|
74
|
+
* `null` member is dropped — `nullable` is unsupported) and a multi-type
|
|
75
|
+
* array becomes one typed `anyOf` branch per type.
|
|
71
76
|
* - Enum-bearing nodes get an inferred `type` (the idiomatic MFJS form; a bare
|
|
72
77
|
* `enum` is valid too) so `anyOf` branches always carry a `type`.
|
|
73
78
|
* - Validation/decorative keywords (`minItems`, `maxItems`, `maxLength`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.8.
|
|
3
|
+
"version": "18.8.6",
|
|
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.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.8.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.8.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.8.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.8.
|
|
158
|
+
"@oh-my-pi/omptype": "18.8.6",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.8.6",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.8.6",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.8.6",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.8.6"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/affinity.ts
CHANGED
|
@@ -360,19 +360,25 @@ export class SessionAffinity implements SessionsApi {
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
|
-
* Copy
|
|
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(
|
|
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,
|
package/src/auth/health.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
294
|
-
*
|
|
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.
|
package/src/auth/refresh.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface OAuthRefresherDeps {
|
|
|
93
93
|
pool: CredentialPool;
|
|
94
94
|
policies: AccountPolicies;
|
|
95
95
|
override?: AuthStorageOptions["refreshOAuthCredential"];
|
|
96
|
+
overrideMints?: AuthStorageOptions["refreshOAuthCredentialMints"];
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/** Single-flighted, lease-guarded OAuth refresh with compare-and-set persistence. */
|
|
@@ -139,6 +140,13 @@ export class OAuthRefresher {
|
|
|
139
140
|
this.#recentMints.set(id, { provider, access: credential.access, at: now });
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/** Whether refreshes run the provider token exchange in this process rather than delegating it. */
|
|
144
|
+
#mintsLocally(): boolean {
|
|
145
|
+
return this.#deps.override
|
|
146
|
+
? this.#deps.overrideMints === true
|
|
147
|
+
: this.#deps.store.refreshOAuthCredential === undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
142
150
|
/**
|
|
143
151
|
* Refresh one stored OAuth credential under durable row ownership.
|
|
144
152
|
*/
|
|
@@ -345,7 +353,7 @@ export class OAuthRefresher {
|
|
|
345
353
|
} else {
|
|
346
354
|
this.#deps.store.updateAuthCredential(row.id, merged);
|
|
347
355
|
}
|
|
348
|
-
if (this.#
|
|
356
|
+
if (this.#mintsLocally()) {
|
|
349
357
|
this.#rememberMint(provider, row.id, merged);
|
|
350
358
|
}
|
|
351
359
|
this.#deps.pool.replace(
|
|
@@ -462,11 +470,7 @@ export class OAuthRefresher {
|
|
|
462
470
|
.then(refreshed => {
|
|
463
471
|
// A delegated refresh may have returned a broker-cached token rather
|
|
464
472
|
// than minting one. Only direct local requests can establish mint time.
|
|
465
|
-
if (
|
|
466
|
-
!hasRefreshLeases(this.#deps.store) &&
|
|
467
|
-
this.#deps.override === undefined &&
|
|
468
|
-
this.#deps.store.refreshOAuthCredential === undefined
|
|
469
|
-
) {
|
|
473
|
+
if (!hasRefreshLeases(this.#deps.store) && this.#mintsLocally()) {
|
|
470
474
|
this.#rememberMint(provider, credentialId, refreshed);
|
|
471
475
|
}
|
|
472
476
|
return refreshed;
|
|
@@ -613,7 +617,7 @@ export class OAuthRefresher {
|
|
|
613
617
|
const promise = (async () => {
|
|
614
618
|
this.#deps.pool.bump("credential-refresh-start");
|
|
615
619
|
try {
|
|
616
|
-
return await this.#forceRefreshCredentialByIdUnshared(id, signal);
|
|
620
|
+
return await this.#forceRefreshCredentialByIdUnshared(id, signal, options?.reason);
|
|
617
621
|
} catch (error) {
|
|
618
622
|
this.#deps.pool.bump("credential-refresh-failure");
|
|
619
623
|
throw error;
|
|
@@ -625,7 +629,11 @@ export class OAuthRefresher {
|
|
|
625
629
|
return raceSignal(promise, signal, "credential refresh aborted");
|
|
626
630
|
}
|
|
627
631
|
|
|
628
|
-
async #forceRefreshCredentialByIdUnshared(
|
|
632
|
+
async #forceRefreshCredentialByIdUnshared(
|
|
633
|
+
id: number,
|
|
634
|
+
signal?: AbortSignal,
|
|
635
|
+
reason?: OAuthRefreshReason,
|
|
636
|
+
): Promise<AuthCredentialSnapshotEntry> {
|
|
629
637
|
for (const provider of this.#deps.pool.providers()) {
|
|
630
638
|
const entries = this.#deps.pool.entries(provider);
|
|
631
639
|
const index = entries.findIndex(entry => entry.id === id);
|
|
@@ -645,7 +653,7 @@ export class OAuthRefresher {
|
|
|
645
653
|
const stale: OAuthCredential = { ...attempted, expires: 0 };
|
|
646
654
|
let refreshed: OAuthCredentials;
|
|
647
655
|
try {
|
|
648
|
-
refreshed = await this.#refreshSingleFlight(provider as Provider, stale, id, signal);
|
|
656
|
+
refreshed = await this.#refreshSingleFlight(provider as Provider, stale, id, signal, reason);
|
|
649
657
|
} catch (error) {
|
|
650
658
|
// A definitively-dead grant tears the row down here, where the
|
|
651
659
|
// attempted credential is known. CAS on the persisted credential so a
|
package/src/auth/select.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getOAuthApiKey, getOAuthProvider } from "../registry/oauth";
|
|
|
4
4
|
import type { OAuthCredentials, OAuthProvider } from "../registry/oauth/types";
|
|
5
5
|
import type { Provider } from "../types";
|
|
6
6
|
import type { CredentialRankingContext, CredentialRankingStrategy, PlanGate, UsageReport } from "../usage";
|
|
7
|
+
import { isWithinUsageReserve } from "../usage";
|
|
7
8
|
import type { RankingStrategyResolver } from "../usage/registry";
|
|
8
9
|
import type { SessionAffinity } from "./affinity";
|
|
9
10
|
import {
|
|
@@ -28,12 +29,13 @@ import {
|
|
|
28
29
|
} from "./rank";
|
|
29
30
|
import { mergeRefreshedCredential, OAUTH_REFRESH_SKEW_MS, type OAuthRefresher } from "./refresh";
|
|
30
31
|
import type { AuthCredentialStore } from "./store";
|
|
31
|
-
import
|
|
32
|
-
ApiKeyCredential,
|
|
33
|
-
AuthApiKeyOptions,
|
|
34
|
-
AuthCredential,
|
|
35
|
-
OAuthCredential,
|
|
36
|
-
|
|
32
|
+
import {
|
|
33
|
+
type ApiKeyCredential,
|
|
34
|
+
type AuthApiKeyOptions,
|
|
35
|
+
type AuthCredential,
|
|
36
|
+
type OAuthCredential,
|
|
37
|
+
oauthAccountKey,
|
|
38
|
+
type StoredAuthCredential,
|
|
37
39
|
} from "./types";
|
|
38
40
|
import type { UsageService } from "./usage";
|
|
39
41
|
import {
|
|
@@ -475,7 +477,9 @@ export class CredentialSelector {
|
|
|
475
477
|
blocked,
|
|
476
478
|
blockedUntil,
|
|
477
479
|
inReserve:
|
|
478
|
-
reserveFraction !== undefined &&
|
|
480
|
+
reserveFraction !== undefined &&
|
|
481
|
+
remainingFraction !== undefined &&
|
|
482
|
+
isWithinUsageReserve(remainingFraction, reserveFraction),
|
|
479
483
|
reserveMeasured: reserveFraction !== undefined && remainingFraction !== undefined,
|
|
480
484
|
accountPriority: policy?.priority === undefined || !Number.isFinite(policy.priority) ? 0 : policy.priority,
|
|
481
485
|
allowanceSpent: remainingFraction === 0,
|
|
@@ -547,9 +551,10 @@ export class CredentialSelector {
|
|
|
547
551
|
const accountIds = options?.accountIds?.length ? new Set(options.accountIds) : undefined;
|
|
548
552
|
const enforceAccounts =
|
|
549
553
|
accountIds !== undefined &&
|
|
550
|
-
credentials.some(
|
|
551
|
-
|
|
552
|
-
|
|
554
|
+
credentials.some(({ credential }) => {
|
|
555
|
+
const accountKey = oauthAccountKey(credential);
|
|
556
|
+
return accountKey !== undefined && accountIds.has(accountKey);
|
|
557
|
+
});
|
|
553
558
|
const hasAccountPolicy = credentials.some(
|
|
554
559
|
({ credential }) => this.#deps.policies.forCredential(provider, credential) !== undefined,
|
|
555
560
|
);
|
|
@@ -883,8 +888,8 @@ export class CredentialSelector {
|
|
|
883
888
|
for (const pass of passes) {
|
|
884
889
|
for (const candidate of candidates) {
|
|
885
890
|
if (preflightFailures.has(candidate)) continue;
|
|
886
|
-
const
|
|
887
|
-
if (pass.enforceAccounts && (
|
|
891
|
+
const candidateAccountKey = oauthAccountKey(candidate.selection.credential);
|
|
892
|
+
if (pass.enforceAccounts && (candidateAccountKey === undefined || !accountIds?.has(candidateAccountKey)))
|
|
888
893
|
continue;
|
|
889
894
|
const resolved = await this.tryOAuth(provider, candidate.selection, providerKey, sessionId, options, {
|
|
890
895
|
checkUsage,
|