@oh-my-pi/pi-ai 18.13.1 → 18.13.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.
- package/CHANGELOG.md +17 -18
- package/dist/types/auth/refresh.d.ts +1 -1
- package/dist/types/auth/sqlite-credential-store.d.ts +3 -1
- package/dist/types/auth/store.d.ts +13 -0
- package/dist/types/auth/types.d.ts +15 -0
- package/dist/types/auth-gateway/dispatch.d.ts +1 -1
- package/dist/types/auth-retry.d.ts +19 -0
- package/dist/types/error/flags.d.ts +1 -0
- package/dist/types/providers/google-types.d.ts +1 -0
- package/dist/types/providers/openai-completions.d.ts +13 -0
- package/dist/types/providers/transform-messages.d.ts +9 -0
- package/dist/types/utils/request-compression.d.ts +2 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +5 -8
- package/package.json +7 -7
- package/src/auth/cascade.ts +27 -18
- package/src/auth/oauth.ts +1 -0
- package/src/auth/refresh.ts +408 -52
- package/src/auth/select.ts +7 -1
- package/src/auth/sqlite-credential-store.ts +29 -1
- package/src/auth/store.ts +14 -0
- package/src/auth/types.ts +15 -0
- package/src/auth-gateway/dispatch.ts +11 -1
- package/src/auth-retry.ts +48 -1
- package/src/error/flags.ts +13 -0
- package/src/error/rate-limit.ts +67 -5
- package/src/error/retryable.ts +4 -1
- package/src/judgment/text.ts +1 -9
- package/src/providers/cowork-fetch.ts +0 -3
- package/src/providers/devin.ts +16 -7
- package/src/providers/google-gemini-cli.ts +10 -3
- package/src/providers/google-shared.ts +5 -2
- package/src/providers/google-types.ts +1 -1
- package/src/providers/openai-completions.ts +28 -9
- package/src/providers/transform-messages.ts +27 -7
- package/src/usage/github-copilot.ts +47 -16
- package/src/utils/request-compression.ts +1 -1
- package/src/utils/schema/json-schema-validator.ts +5 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.3] - 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Updated Bun engine requirement to ≥1.4.4
|
|
10
|
+
|
|
11
|
+
## [18.13.2] - 2026-10-27
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed OAuth logins shared by several processes being disabled with `invalid_grant` when they refreshed at the same time or one of them quit mid-refresh ([#17128](https://github.com/can1357/oh-my-pi/pull/17128) by [@sjawhar](https://github.com/sjawhar))
|
|
16
|
+
- Fixed transient Google 429 rate-limit and capacity responses retrying in place without blocking or rotating credentials, while daily caps and zero-entitlement limits still rotate ([#17127](https://github.com/can1357/oh-my-pi/pull/17127) by [@GTanger](https://github.com/GTanger))
|
|
17
|
+
- Fixed Aliyun DashScope's multimodal gateway failing a turn on its intermittent `InvalidParameter` 400s for valid inline images ("provided URL does not appear to be valid", "Download multimodal file timed out"); they are now retried ([#17101](https://github.com/can1357/oh-my-pi/pull/17101) by [@shuairongzeng](https://github.com/shuairongzeng))
|
|
18
|
+
- Fixed a revoked or rejected GitHub Copilot credential continuing to show its last cached usage quota instead of being dropped ([#17081](https://github.com/can1357/oh-my-pi/issues/17081))
|
|
19
|
+
- Fixed Devin requests compressing the request body on the UI thread, which briefly froze the TUI on long conversations; requests under 1 KiB are now sent uncompressed
|
|
20
|
+
|
|
5
21
|
## [18.13.1] - 2026-10-27
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -2308,21 +2324,4 @@
|
|
|
2308
2324
|
|
|
2309
2325
|
- Removed legacy GitLab Duo Workflow `chat` and `software_development` flow paths and the non-MCP action bridge in favor of the inline custom `ambient` flow.
|
|
2310
2326
|
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
### Added
|
|
2314
|
-
|
|
2315
|
-
- Added a third streaming thinking-loop detection heuristic to catch "progress-lexicon stalls" where models endlessly reshuffle motivational filler without introducing new vocabulary or concrete technical references
|
|
2316
|
-
- Added branded wordmark and logo animation to authentication flow
|
|
2317
|
-
- Added a third streaming thinking-loop detection shape — a _progress-lexicon stall_ — alongside verbatim tail repetition and near-duplicate (trigram) segments. It catches reasoning-summarizer loops that reshuffle the same motivational filler ("just doing it, pushing ahead, maintaining momentum") into fresh word order every paragraph: word-trigrams never cluster, but a run of substantial segments that recycle the recent vocabulary and introduce no _new_ concrete reference (path / identifier / code-span) trips the guard. Summarizer title/heading lines (`**Bold Title**`, `## Heading`) are stripped before analysis so their ever-changing wording cannot mask the stall by inflating novelty. Calibrated against 537k real non-Gemini reasoning blocks (zero false positives at novelty floor 0.2 / run length 8; the real loop sustains runs of 10+).
|
|
2318
|
-
- Added CoreWeave Serverless Inference provider login support via `COREWEAVE_API_KEY` and `WANDB_API_KEY` fallback.
|
|
2319
|
-
|
|
2320
|
-
### Changed
|
|
2321
|
-
|
|
2322
|
-
- Redesigned the OAuth callback page (`oauth.html`) to match the oh-my-pi web brand language: OKLCH purple-tinted dark neutrals, magenta→iris→cyan brand gradient on the wordmark, frosted-glass card over an ascii grid backdrop, and a colored status halo around the success/error icon. All assets are inlined; the `__OAUTH_STATE__` injection contract and success/error JS logic are unchanged.
|
|
2323
|
-
|
|
2324
|
-
### Fixed
|
|
2325
|
-
|
|
2326
|
-
- Fixed local llama.cpp (and any local OpenAI-compatible server rendering the Qwen3.6+ chat template) re-processing the full prompt every new user message even with `replayReasoningContent` enabled (#3541 follow-up to #3528). Sending `reasoning_content` alone wasn't enough: Qwen3's chat template strips `<think>...</think>` from any assistant turn whose index is `<= last_query_index`, so the moment a new user message (the user's next prompt, or the auto-learn capture-at-stop nudge) lands, every prior assistant turn becomes "older" and is re-rendered without the `<think>` block — diverging from the generation tokens still in the slot's KV cache. The chat-completions encoder now emits `preserve_thinking: true` for Qwen thinking dialects on local servers, route-split the same way the existing `enable_thinking` emission is: the `qwen` dialect rides the top-level field (llama.cpp's `--jinja` hook and Alibaba Cloud Model Studio's compatible-mode), the `qwen-chat-template` dialect (NVIDIA NIM, vLLM/SGLang's chat-template-kwargs path) rides only `chat_template_kwargs.preserve_thinking` because NIM's request schema is `additionalProperties: false` and rejects unknown top-level fields (#2299). The emission is hoisted above the `reasoning.enabled` gate so it fires for THREE cases the original gating missed: (1) runtime-discovered local Qwen models that ship with `reasoning: false` because the upstream `/v1/models` doesn't advertise the capability (same gotcha #3532 fixed for `replayReasoningContent`), (2) caller-disabled reasoning (`/think off`) — the kwarg is a history-rendering knob, not a per-turn thinking switch, and the slot still holds `<think>` tokens from earlier turns, and (3) forced-tool-choice / DeepSeek-style auto-disable. Qwen3.6+ then renders `<think>...</think>` for every assistant turn regardless of position, and the next-turn render matches the cached generation tokens. ([#3541](https://github.com/can1357/oh-my-pi/issues/3541))
|
|
2327
|
-
|
|
2328
|
-
Older entries are archived in [packages/ai/CHANGELOG.md@917a36589039](https://github.com/can1357/oh-my-pi/blob/917a365890396071a959dfd2865a3538518ef717/packages/ai/CHANGELOG.md).
|
|
2327
|
+
Older entries are archived in [packages/ai/CHANGELOG.md@a36a1f252e23](https://github.com/can1357/oh-my-pi/blob/a36a1f252e23031accb25bacf2b9894605ba524f/packages/ai/CHANGELOG.md).
|
|
@@ -54,7 +54,7 @@ export declare class OAuthRefresher {
|
|
|
54
54
|
* continuing.
|
|
55
55
|
*/
|
|
56
56
|
disableDefinitiveFailure(provider: string, credentialId: number | undefined, attemptedCredential: OAuthCredential, index: number, errorMsg: string): Promise<"disabled" | "peer-rotated" | "cas-lost">;
|
|
57
|
-
refresh(provider: Provider, credential: OAuthCredential, credentialId: number | undefined, signal?: AbortSignal, reason?: OAuthRefreshReason): Promise<OAuthCredentials>;
|
|
57
|
+
refresh(provider: Provider, credential: OAuthCredential, credentialId: number | undefined, signal?: AbortSignal, reason?: OAuthRefreshReason, rejectedAccess?: string): Promise<OAuthCredentials>;
|
|
58
58
|
/**
|
|
59
59
|
* Refresh the OAuth credential with the given id through a per-credential
|
|
60
60
|
* single-flight. Concurrent callers for the same row await the same upstream
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Database } from "bun:sqlite";
|
|
8
8
|
import { isSqliteBusyError, isSqliteCorruptionError } from "@oh-my-pi/pi-utils";
|
|
9
|
-
import type { AuthCredentialStore, CredentialRefreshLeaseFence } from "./store.js";
|
|
9
|
+
import type { AuthCredentialStore, CredentialRefreshLeaseFence, CredentialRefreshLeaseHolder } from "./store.js";
|
|
10
10
|
import type { AuthCredential, DisabledCredentialSummary, OAuthCredential, StoredAuthCredential, StoredCredentialBlock } from "./types.js";
|
|
11
11
|
import type { OAuthCredentials } from "../registry/oauth/types.js";
|
|
12
12
|
import type { ClientUsageReport, ClientUsageSummary, UsageHistoryEntry, UsageHistoryQuery } from "../usage.js";
|
|
@@ -89,6 +89,8 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
89
89
|
tryAcquireCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean;
|
|
90
90
|
getCredentialRefreshLeaseExpiresAt(credentialId: number): number | undefined;
|
|
91
91
|
renewCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean;
|
|
92
|
+
getCredentialRefreshLeaseHolder(credentialId: number): CredentialRefreshLeaseHolder | undefined;
|
|
93
|
+
handBackCredentialRefreshLease(credentialId: number, owner: string, holder: CredentialRefreshLeaseHolder): boolean;
|
|
92
94
|
releaseCredentialRefreshLease(credentialId: number, owner: string): void;
|
|
93
95
|
recordUsageSnapshots(entries: UsageHistoryEntry[]): void;
|
|
94
96
|
listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
@@ -96,6 +96,19 @@ export interface CredentialRefreshLeaseStore {
|
|
|
96
96
|
getCredentialRefreshLeaseExpiresAt(credentialId: number): number | undefined;
|
|
97
97
|
releaseCredentialRefreshLease(credentialId: number, owner: string): void;
|
|
98
98
|
renewCredentialRefreshLease(credentialId: number, owner: string, expiresAtMs: number): boolean;
|
|
99
|
+
/** The lease row as stored, expired or not; `undefined` when no lease row exists. */
|
|
100
|
+
getCredentialRefreshLeaseHolder?(credentialId: number): CredentialRefreshLeaseHolder | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* Give a lease this owner took back to the holder it replaced, with that
|
|
103
|
+
* holder's own expiry, so the holder can still renew it and save the token
|
|
104
|
+
* it may have exchanged. Returns false when `owner` no longer holds it.
|
|
105
|
+
*/
|
|
106
|
+
handBackCredentialRefreshLease?(credentialId: number, owner: string, holder: CredentialRefreshLeaseHolder): boolean;
|
|
107
|
+
}
|
|
108
|
+
/** Owner and expiry of a stored refresh lease row. */
|
|
109
|
+
export interface CredentialRefreshLeaseHolder {
|
|
110
|
+
owner: string;
|
|
111
|
+
expiresAtMs: number;
|
|
99
112
|
}
|
|
100
113
|
/** Usage history and client-observed request accounting. */
|
|
101
114
|
export interface UsageLedgerStore {
|
|
@@ -434,6 +434,13 @@ export type AuthApiKeyOptions = {
|
|
|
434
434
|
forceRefresh?: boolean;
|
|
435
435
|
/** Explicit provider-401 recovery; generic force refreshes leave this unset. */
|
|
436
436
|
refreshReason?: OAuthRefreshReason;
|
|
437
|
+
/**
|
|
438
|
+
* The access token the provider rejected with a 401, when this resolve is
|
|
439
|
+
* that 401's recovery. A refresh adopts a stored token that differs from it
|
|
440
|
+
* instead of exchanging the refresh token again: with single-use refresh
|
|
441
|
+
* tokens every needless exchange revokes the token its peers are using.
|
|
442
|
+
*/
|
|
443
|
+
rejectedAccessToken?: string;
|
|
437
444
|
/** When false, select as `sessionId` would without recording the choice as that session's sticky credential. */
|
|
438
445
|
recordAffinity?: boolean;
|
|
439
446
|
};
|
|
@@ -555,6 +562,12 @@ export interface StoredOAuthRefreshOptions<T extends OAuthCredential = OAuthCred
|
|
|
555
562
|
observedCredential?: T;
|
|
556
563
|
credentialFromRow: (credential: OAuthCredential) => T | undefined;
|
|
557
564
|
forceRefresh?: boolean;
|
|
565
|
+
/**
|
|
566
|
+
* The access token a provider just rejected with a 401. A forced refresh
|
|
567
|
+
* adopts a still-fresh stored row whose token differs from it: another
|
|
568
|
+
* process rotated the row, and its token has not been rejected.
|
|
569
|
+
*/
|
|
570
|
+
rejectedAccess?: string;
|
|
558
571
|
canRefresh?: (credential: T) => boolean;
|
|
559
572
|
refreshSkewMs?: number;
|
|
560
573
|
signal?: AbortSignal;
|
|
@@ -963,6 +976,8 @@ export interface OAuthRefreshByIdOptions {
|
|
|
963
976
|
* Local reuse still requires {@link reuseRecentMint}.
|
|
964
977
|
*/
|
|
965
978
|
reason?: OAuthRefreshReason;
|
|
979
|
+
/** The access token a provider 401 rejected; see {@link AuthApiKeyOptions.rejectedAccessToken}. */
|
|
980
|
+
rejectedAccess?: string;
|
|
966
981
|
}
|
|
967
982
|
/** OAuth login, access, account identity, and refresh operations. */
|
|
968
983
|
export interface OAuthApi {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ApiKeyResolver, type ResolvedApiKey } from "../auth-retry.js";
|
|
2
2
|
import type { AuthStorage } from "../auth-storage.js";
|
|
3
3
|
import { type GatewayErrorClassification } from "../error/gateway.js";
|
|
4
4
|
import type { Api, FetchImpl, Model, Usage } from "../types.js";
|
|
@@ -36,6 +36,13 @@ export interface ResolvedApiKey {
|
|
|
36
36
|
apiKey: string;
|
|
37
37
|
/** Durable row id of the credential that supplied this bearer, when known. */
|
|
38
38
|
credentialId?: number;
|
|
39
|
+
/**
|
|
40
|
+
* The OAuth access token behind this bearer, when the bearer came from a
|
|
41
|
+
* stored OAuth credential. A provider whose API key wraps the token in a
|
|
42
|
+
* structured document makes the two differ, so a 401 recovery names this
|
|
43
|
+
* one as the token the provider rejected.
|
|
44
|
+
*/
|
|
45
|
+
oauthAccess?: string;
|
|
39
46
|
/**
|
|
40
47
|
* Resolved after `LimitsApi.rotate` slept out a sibling's short block
|
|
41
48
|
* (`afterSiblingWait`): the driver accepts this bearer even if the request
|
|
@@ -55,6 +62,18 @@ export declare function resolvedApiKeyBearer(resolved: ApiKeyResolution): string
|
|
|
55
62
|
* resolvers (`KeyCascade.resolver`, coding-agent `createApiKeyResolver`).
|
|
56
63
|
*/
|
|
57
64
|
export declare function markAfterSiblingWait(resolved: ApiKeyResolution): ApiKeyResolution;
|
|
65
|
+
/**
|
|
66
|
+
* Remembers which OAuth access token produced each bearer a resolver served,
|
|
67
|
+
* so a 401 recovery can name the token of the request that actually failed.
|
|
68
|
+
* A resolver serves many requests, and they fail out of order: the last
|
|
69
|
+
* resolution is not that request's identity. Used by every resolver that
|
|
70
|
+
* serves more than one request ({@link KeyCascade.resolver}, the coding
|
|
71
|
+
* agent's `createApiKeyResolver`, the auth gateway's resolver).
|
|
72
|
+
*/
|
|
73
|
+
export declare function createOAuthProvenance(): {
|
|
74
|
+
record: (resolved: ApiKeyResolution) => void;
|
|
75
|
+
accessFor: (bearer: string | undefined) => string | undefined;
|
|
76
|
+
};
|
|
58
77
|
/** A static bearer string, or a {@link ApiKeyResolver} that mints/rotates one. */
|
|
59
78
|
export type ApiKey = string | ApiKeyResolver;
|
|
60
79
|
/** Keyless-provider credential marker; transports must not send it in authentication headers. */
|
|
@@ -94,6 +94,7 @@ export declare function isImmutableLatestThinkingText(text: string): boolean;
|
|
|
94
94
|
* output, so callers strip {@link Flag.Transient} when this matches.
|
|
95
95
|
*/
|
|
96
96
|
export declare const LLAMA_CPP_TOOL_CALL_PARSE_PATTERN: RegExp;
|
|
97
|
+
export declare const ALIYUN_MM_GATEWAY_PATTERN: RegExp;
|
|
97
98
|
/** Whether an OAuth refresh error message means the grant is definitively dead. */
|
|
98
99
|
export declare function isOAuthExpiry(errorMessage: string): boolean;
|
|
99
100
|
export declare function create(...flags: number[]): number;
|
|
@@ -3,6 +3,8 @@ import type { AssistantMessage, Context, Model, ServiceTier, StreamFunction, Str
|
|
|
3
3
|
import type { ChatCompletionMessageParam } from "./openai-chat-wire.js";
|
|
4
4
|
import { type OpenAIPromptCacheOptions } from "./openai-shared.js";
|
|
5
5
|
export { applyOpenRouterRoutingVariant } from "./openai-shared.js";
|
|
6
|
+
/** Whether a thinking block's signature is a raw LiteLLM (Anthropic) one, not a reasoning field name. */
|
|
7
|
+
export declare function isLiteLLMThinkingSignature(signature: string | undefined): signature is string;
|
|
6
8
|
/**
|
|
7
9
|
* Identify "real progress" stream chunks vs. keepalives, role-only preambles,
|
|
8
10
|
* and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
|
|
@@ -43,4 +45,15 @@ export interface OpenAICompletionsOptions extends StreamOptions {
|
|
|
43
45
|
*/
|
|
44
46
|
export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
|
|
45
47
|
export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined, timestamp?: number, cachedTokensHeader?: number): AssistantMessage["usage"];
|
|
48
|
+
/**
|
|
49
|
+
* Whether every chat-completions request to `model` drops the thinking text of
|
|
50
|
+
* its own prior turns: neither the base compat nor its `whenThinking` view
|
|
51
|
+
* sends it. Blocks with a raw LiteLLM signature ({@link isLiteLLMThinkingSignature})
|
|
52
|
+
* still replay in `thinking_blocks`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function completionsDropsOwnThinking(model: {
|
|
55
|
+
api?: string;
|
|
56
|
+
reasoning?: boolean;
|
|
57
|
+
compat?: Model["compat"];
|
|
58
|
+
} | null | undefined): boolean;
|
|
46
59
|
export declare function convertMessages(model: Model<"openai-completions">, context: Context, compat: ResolvedOpenAICompat): ChatCompletionMessageParam[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
|
|
1
2
|
import type { Api, AssistantMessage, Message, Model } from "../types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Maximum tool-call id length the strictest replay provider accepts.
|
|
@@ -54,6 +55,14 @@ export declare function isSurvivingAssistant(msg: AssistantMessage, model?: Mode
|
|
|
54
55
|
export declare function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages">;
|
|
55
56
|
/** Whether `turn` came from `model` itself: its own turns replay their native reasoning unchanged. */
|
|
56
57
|
export declare function isSameModelTurn(turn: Pick<AssistantMessage, "provider" | "api" | "model">, model: Pick<Model, "provider" | "api" | "id">): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Whether chat completions (`convertMessages`) writes the model's own past
|
|
60
|
+
* thinking under `compat`: as Mistral content parts, demoted text, or a
|
|
61
|
+
* reasoning field. Otherwise it drops it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function completionsSendsOwnThinking(model: {
|
|
64
|
+
reasoning?: boolean;
|
|
65
|
+
}, compat: Pick<ResolvedOpenAICompat, "mistralReasoningContentParts" | "requiresThinkingAsText" | "requiresReasoningContentForToolCalls" | "thinkingFormat" | "replayReasoningContent">): boolean;
|
|
57
66
|
/**
|
|
58
67
|
* Normalize tool call ID for cross-provider compatibility.
|
|
59
68
|
* OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { FetchImpl, Model } from "../types.js";
|
|
2
|
+
/** Bodies below one TCP segment gain nothing from compression; skip them. */
|
|
3
|
+
export declare const MIN_COMPRESSED_BODY_BYTES = 1024;
|
|
2
4
|
/**
|
|
3
5
|
* Compresses request bodies from {@link model} to its provider's own endpoint
|
|
4
6
|
* when its catalog rules declare encodings the endpoint decodes
|
|
@@ -28,14 +28,11 @@ export declare function getTagSelectedUnionBranch(branches: readonly unknown[],
|
|
|
28
28
|
* schemas asking about the same name keep separate answers, and one schema
|
|
29
29
|
* asked about two names keeps two answers.
|
|
30
30
|
*
|
|
31
|
-
* The root identity covers the graph because
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* that same wire-schema object. A graph mutated in place after production
|
|
37
|
-
* would already hand those three a stale answer; this cache adds no new
|
|
38
|
-
* assumption beyond the ones the schema pipeline already relies on.
|
|
31
|
+
* The root identity covers the graph because callers must not mutate a schema
|
|
32
|
+
* after its first query: wire schemas are covered by that same rule (their
|
|
33
|
+
* derived values memoize against the same identity), and raw MCP
|
|
34
|
+
* `inputSchema` and eval-tool `parameters` graphs reach this function with no
|
|
35
|
+
* other stamp on them, so a new identity is what resets the memo for those.
|
|
39
36
|
*/
|
|
40
37
|
export declare function schemaDefinesProperty(schema: unknown, key: string): boolean;
|
|
41
38
|
export declare function validateJsonSchemaValue(schema: unknown, value: unknown, root?: unknown): JsonSchemaValidationResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.13.
|
|
3
|
+
"version": "18.13.3",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,16 +155,16 @@
|
|
|
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.13.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.13.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.13.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.13.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.13.
|
|
158
|
+
"@oh-my-pi/omptype": "18.13.3",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.13.3",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.13.3",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.13.3",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.13.3"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
|
166
166
|
},
|
|
167
167
|
"engines": {
|
|
168
|
-
"bun": ">=1.
|
|
168
|
+
"bun": ">=1.4.4"
|
|
169
169
|
}
|
|
170
170
|
}
|
package/src/auth/cascade.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { authPolicyFor } from "@oh-my-pi/pi-catalog/compat/auth";
|
|
2
2
|
import { $env, $envExact, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import { type ApiKeyResolver, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
|
|
3
|
+
import { type ApiKeyResolver, createOAuthProvenance, markAfterSiblingWait, type ResolvedApiKey } from "../auth-retry";
|
|
4
4
|
import { getEnvApiKey, getEnvApiKeyName } from "../env-api-key";
|
|
5
5
|
import * as AIError from "../error";
|
|
6
6
|
import { isUsageLimitOutcome } from "../error/rate-limit";
|
|
@@ -332,11 +332,19 @@ export class KeyCascade implements KeysApi {
|
|
|
332
332
|
): Promise<ResolvedApiKey | undefined> {
|
|
333
333
|
let credentialId: number | undefined;
|
|
334
334
|
let oauthIdentity: OAuthRequestIdentity | undefined;
|
|
335
|
-
|
|
335
|
+
let oauthAccess: string | undefined;
|
|
336
|
+
const apiKey = await this.#resolve(provider, sessionId, options, (id, identity, access) => {
|
|
336
337
|
credentialId = id;
|
|
337
338
|
oauthIdentity = identity;
|
|
339
|
+
oauthAccess = access;
|
|
338
340
|
});
|
|
339
|
-
|
|
341
|
+
if (apiKey === undefined) return undefined;
|
|
342
|
+
return {
|
|
343
|
+
apiKey,
|
|
344
|
+
credentialId,
|
|
345
|
+
...(oauthAccess ? { oauthAccess } : {}),
|
|
346
|
+
...(oauthIdentity ? { oauthIdentity } : {}),
|
|
347
|
+
};
|
|
340
348
|
}
|
|
341
349
|
|
|
342
350
|
/**
|
|
@@ -365,7 +373,7 @@ export class KeyCascade implements KeysApi {
|
|
|
365
373
|
provider: string,
|
|
366
374
|
sessionId?: string,
|
|
367
375
|
options?: AuthApiKeyOptions,
|
|
368
|
-
onCredentialId?: (id: number, identity?: OAuthRequestIdentity) => void,
|
|
376
|
+
onCredentialId?: (id: number, identity?: OAuthRequestIdentity, oauthAccess?: string) => void,
|
|
369
377
|
): Promise<string | undefined> {
|
|
370
378
|
const restricted = this.#deps.affinity.isRestricted(provider, sessionId);
|
|
371
379
|
// Runtime override takes highest priority
|
|
@@ -405,7 +413,8 @@ export class KeyCascade implements KeysApi {
|
|
|
405
413
|
if (oauthResolved) {
|
|
406
414
|
if (onCredentialId && oauthResolved.credentialId !== undefined) {
|
|
407
415
|
const { orgId, region, inferenceRegion } = oauthResolved.credential;
|
|
408
|
-
|
|
416
|
+
const identity = { orgId, region, inferenceRegion };
|
|
417
|
+
onCredentialId(oauthResolved.credentialId, identity, oauthResolved.credential.access);
|
|
409
418
|
}
|
|
410
419
|
return oauthResolved.apiKey;
|
|
411
420
|
}
|
|
@@ -476,13 +485,18 @@ export class KeyCascade implements KeysApi {
|
|
|
476
485
|
*/
|
|
477
486
|
resolver(provider: string, options?: { sessionId?: string; baseUrl?: string; modelId?: string }): ApiKeyResolver {
|
|
478
487
|
const { sessionId, baseUrl, modelId } = options ?? {};
|
|
488
|
+
// One resolver serves many requests, and they fail out of order, so the
|
|
489
|
+
// token a 401 rejected is looked up from the bearer that request sent.
|
|
490
|
+
const provenance = createOAuthProvenance();
|
|
491
|
+
const resolve = async (extra: AuthApiKeyOptions): Promise<ResolvedApiKey | undefined> => {
|
|
492
|
+
const resolved = await this.getWithCredential(provider, sessionId, { baseUrl, modelId, ...extra });
|
|
493
|
+
provenance.record(resolved);
|
|
494
|
+
return resolved;
|
|
495
|
+
};
|
|
479
496
|
return async ({ lastChance, error, signal, previousKey }) => {
|
|
497
|
+
const rejectedAccessToken = provenance.accessFor(previousKey);
|
|
480
498
|
if (error === undefined) {
|
|
481
|
-
return
|
|
482
|
-
baseUrl,
|
|
483
|
-
modelId,
|
|
484
|
-
signal,
|
|
485
|
-
});
|
|
499
|
+
return resolve({ signal });
|
|
486
500
|
}
|
|
487
501
|
if (lastChance) {
|
|
488
502
|
const rotation = await this.#deps.rotate(provider, sessionId, {
|
|
@@ -499,18 +513,13 @@ export class KeyCascade implements KeysApi {
|
|
|
499
513
|
// because a peer may have refreshed the failed bearer.
|
|
500
514
|
if (AIError.isUsageLimit(error) || isUsageLimitOutcome(status, message)) return undefined;
|
|
501
515
|
}
|
|
502
|
-
const resolved = await
|
|
503
|
-
baseUrl,
|
|
504
|
-
modelId,
|
|
505
|
-
signal,
|
|
506
|
-
});
|
|
516
|
+
const resolved = await resolve({ signal });
|
|
507
517
|
return rotation.afterSiblingWait ? markAfterSiblingWait(resolved) : resolved;
|
|
508
518
|
}
|
|
509
|
-
return
|
|
510
|
-
baseUrl,
|
|
511
|
-
modelId,
|
|
519
|
+
return resolve({
|
|
512
520
|
forceRefresh: true,
|
|
513
521
|
refreshReason: AIError.status(error) === 401 ? "auth-recovery" : undefined,
|
|
522
|
+
rejectedAccessToken,
|
|
514
523
|
signal,
|
|
515
524
|
});
|
|
516
525
|
};
|
package/src/auth/oauth.ts
CHANGED
|
@@ -185,6 +185,7 @@ export class OAuthAccounts implements OAuthApi {
|
|
|
185
185
|
await this.refresh(selection.credentialId, options.signal, {
|
|
186
186
|
reuseRecentMint: options.refreshReason === "auth-recovery",
|
|
187
187
|
reason: options.refreshReason,
|
|
188
|
+
rejectedAccess: options.rejectedAccessToken,
|
|
188
189
|
});
|
|
189
190
|
// The refresh re-lists rows, so a concurrently removed row can shift this one's position.
|
|
190
191
|
index = this.#deps.pool.entries(provider).findIndex(entry => entry.id === selection.credentialId);
|