@oh-my-pi/pi-ai 16.3.11 → 16.3.13
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 +32 -0
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-retry.d.ts +3 -1
- package/dist/types/auth-storage.d.ts +13 -8
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/providers/cursor.d.ts +5 -0
- package/dist/types/providers/openai-shared.d.ts +6 -3
- package/dist/types/providers/register-builtins.d.ts +5 -0
- package/dist/types/registry/oauth/callback-server.d.ts +2 -0
- package/dist/types/registry/oauth/xai-oauth.d.ts +1 -7
- package/dist/types/registry/registry.d.ts +2 -1
- package/dist/types/registry/xai-oauth.d.ts +2 -1
- package/dist/types/types.d.ts +2 -0
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/idle-iterator.d.ts +10 -0
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +20 -16
- package/src/auth-broker/remote-store.ts +55 -5
- package/src/auth-gateway/server.ts +1 -0
- package/src/auth-retry.ts +7 -2
- package/src/auth-storage.ts +210 -46
- package/src/providers/anthropic.ts +78 -8
- package/src/providers/cursor.ts +17 -10
- package/src/providers/openai-codex-responses.ts +37 -6
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/openai-shared.ts +30 -14
- package/src/providers/register-builtins.ts +15 -0
- package/src/registry/oauth/__tests__/xai-oauth.test.ts +55 -0
- package/src/registry/oauth/callback-server.ts +40 -12
- package/src/registry/oauth/xai-oauth.ts +6 -10
- package/src/registry/xai-oauth.ts +2 -1
- package/src/stream.ts +7 -1
- package/src/types.ts +2 -0
- package/src/usage/openai-codex.ts +3 -2
- package/src/utils/event-stream.ts +26 -0
- package/src/utils/idle-iterator.ts +65 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.3.13] - 2026-07-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Changed the xAI Grok OAuth (`xai-oauth`) provider to use manual code-paste login by default. `/login` now accepts a pasted authorization code or full `http://127.0.0.1:56121/callback?code=...` redirect URL without starting a local callback listener ([#3277](https://github.com/can1357/oh-my-pi/pull/3277) by [@Jaaneek](https://github.com/Jaaneek)).
|
|
10
|
+
- Renamed the xAI Grok OAuth provider in login and credential prompts to "xAI Grok OAuth (SuperGrok or X Premium+)" ([#3277](https://github.com/can1357/oh-my-pi/pull/3277) by [@Jaaneek](https://github.com/Jaaneek)).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed the generic lazy-stream idle watchdog aborting healthy `cursor-agent` streams with "Provider stream stalled while waiting for the next event" while a Cursor exec-channel local tool (shell/read/grep/write/MCP/…) legitimately ran longer than the idle budget. Provider streams now advertise consumer-side local work in flight and the watchdog slides its deadline instead of aborting; genuinely silent streams still time out. ([#4593](https://github.com/can1357/oh-my-pi/issues/4593))
|
|
15
|
+
- Fixed OpenAI Codex/Responses reasoning streams so streamed thinking content is preserved when the final `output_item.done` reconstructs to an empty summary ([#4918](https://github.com/can1357/oh-my-pi/issues/4918)).
|
|
16
|
+
- Fixed Anthropic streams hanging forever when generation wedges mid-stream (notably long `write` tool calls on Opus 4.8 high/xhigh) while the server keeps sending `ping` keepalives: pings now extend the idle watchdog only within a bounded window (3x the idle timeout) since the last real stream event, so a stalled tool-call stream times out and recovers instead of hanging with no retry path ([#4900](https://github.com/can1357/oh-my-pi/issues/4900)).
|
|
17
|
+
|
|
18
|
+
## [16.3.12] - 2026-07-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Added `AssistantMessage.toolCallAbortMessages` for per-tool placeholder labels on aborted assistant turns ([#2783](https://github.com/can1357/oh-my-pi/issues/2783)).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed Anthropic replay 400s (`tool_use ids were found without tool_result blocks immediately after`) when a persisted assistant turn carries content after a completed tool call — such as a mid-turn `server-side-fallback` handoff (fallback block plus continued text/tool calls after the primary model's `tool_use`) or trailing text from cross-provider replays — by stable-partitioning assistant content so all `tool_use` blocks trail the non-`tool_use` chain. ([#4781](https://github.com/can1357/oh-my-pi/issues/4781), [#544](https://github.com/can1357/oh-my-pi/issues/544))
|
|
27
|
+
- Fixed access-token-only OAuth credentials attempting token refresh with an empty refresh token after expiry.
|
|
28
|
+
- Fixed gateway usage-limit retries falling through to cross-provider model fallback before trying a sibling credential from the same provider.
|
|
29
|
+
- Fixed Codex usage-limit rotation treating Plus and K-12 accounts as separate quota groups for shared 5-hour/7-day windows.
|
|
30
|
+
- Fixed OpenAI Responses streams that end with `response.done` being misclassified as premature stream closures.
|
|
31
|
+
- Fixed OpenCode Go `/login` credentials being shadowed by an existing `OPENCODE_API_KEY` env fallback after switching accounts. ([#4688](https://github.com/can1357/oh-my-pi/issues/4688))
|
|
32
|
+
- Fixed OpenAI Codex WebSocket continuations to treat proxy stale-anchor codes such as `codex_previous_response_stale` as an expired `previous_response_id` chain — same recovery class as the OpenAI-standard `previous_response_not_found` — so the turn is retried with full context instead of surfacing the error to the user ([#4624](https://github.com/can1357/oh-my-pi/issues/4624)).
|
|
33
|
+
- Fixed Azure Foundry Anthropic utility requests to omit the structured-output beta whenever strict tools are disabled, preventing `structured_outputs not supported in your workspace` failures for Sonnet 5 compaction ([#4679](https://github.com/can1357/oh-my-pi/issues/4679)).
|
|
34
|
+
- Fixed OAuth `launchUrl` advertisement for flows whose redirect never returns to the local callback server: custom-scheme redirects (e.g. GitLab Duo's `vscode://` URI, which `new URL` parses without complaint) and fixed non-loopback hosts no longer receive a `http://localhost:<port>/launch` copy target that misrepresents the callback endpoint and resolves nowhere for remote users.
|
|
35
|
+
- Codex load balancing: clear stale persisted and in-memory usage-limit blocks for an `openai-codex` account when a fresh live usage report shows it is allowed and below all limits, including broker-backed gateway snapshots, so traffic returns to recovered accounts instead of funneling to one sibling.
|
|
36
|
+
|
|
5
37
|
## [16.3.11] - 2026-07-06
|
|
6
38
|
|
|
7
39
|
### Fixed
|
|
@@ -19,7 +19,7 @@ export declare function getAuthBrokerTokenFilePath(): string;
|
|
|
19
19
|
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
20
20
|
*
|
|
21
21
|
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
22
|
-
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
22
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml` or `<agentDir>/config.yaml`.
|
|
23
23
|
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
24
24
|
*
|
|
25
25
|
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
@@ -21,6 +21,8 @@ export interface ApiKeyResolveContext {
|
|
|
21
21
|
lastChance: boolean;
|
|
22
22
|
/** The auth error that triggered this re-resolution, or `undefined` on the initial resolve. */
|
|
23
23
|
error: unknown;
|
|
24
|
+
/** Bearer used by the failed attempt, when the caller can expose it. */
|
|
25
|
+
previousKey?: string;
|
|
24
26
|
/** Caller cancel signal, threaded into any credential refresh / rotation work. */
|
|
25
27
|
signal?: AbortSignal;
|
|
26
28
|
}
|
|
@@ -56,7 +58,7 @@ export { isAuthRetryableError };
|
|
|
56
58
|
*/
|
|
57
59
|
export declare const AUTH_RETRY_STEPS: readonly boolean[];
|
|
58
60
|
/** Resolve a single retry step, swallowing resolver failures into `undefined`. */
|
|
59
|
-
export declare function resolveRetryKey(resolver: ApiKeyResolver, lastChance: boolean, error: unknown, signal?: AbortSignal): Promise<string | undefined>;
|
|
61
|
+
export declare function resolveRetryKey(resolver: ApiKeyResolver, lastChance: boolean, error: unknown, signal?: AbortSignal, previousKey?: string): Promise<string | undefined>;
|
|
60
62
|
/**
|
|
61
63
|
* Runs an auth-protected operation through the central a/b/c retry policy.
|
|
62
64
|
*
|
|
@@ -16,6 +16,7 @@ import { type CodexResetConsumeCode, type CodexResetCredit } from "./usage/opena
|
|
|
16
16
|
export type ApiKeyCredential = {
|
|
17
17
|
type: "api_key";
|
|
18
18
|
key: string;
|
|
19
|
+
source?: "login";
|
|
19
20
|
};
|
|
20
21
|
export type OAuthCredential = {
|
|
21
22
|
type: "oauth";
|
|
@@ -678,8 +679,8 @@ export declare class AuthStorage {
|
|
|
678
679
|
/**
|
|
679
680
|
* Classify where a provider's auth comes from, following the same precedence
|
|
680
681
|
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
|
681
|
-
* stored OAuth → env var → stored api_key →
|
|
682
|
-
* undefined when no auth is configured.
|
|
682
|
+
* stored OAuth → login-stored api_key → env var → stored api_key →
|
|
683
|
+
* fallback resolver. Returns undefined when no auth is configured.
|
|
683
684
|
*
|
|
684
685
|
* Compact, structured counterpart to {@link describeCredentialSource}.
|
|
685
686
|
*/
|
|
@@ -792,6 +793,7 @@ export declare class AuthStorage {
|
|
|
792
793
|
retryAfterMs?: number;
|
|
793
794
|
baseUrl?: string;
|
|
794
795
|
modelId?: string;
|
|
796
|
+
apiKey?: string;
|
|
795
797
|
signal?: AbortSignal;
|
|
796
798
|
}): Promise<UsageLimitMarkResult>;
|
|
797
799
|
/**
|
|
@@ -807,9 +809,10 @@ export declare class AuthStorage {
|
|
|
807
809
|
* 1. Runtime override (CLI --api-key)
|
|
808
810
|
* 2. Config override (models.yml `providers.<name>.apiKey`)
|
|
809
811
|
* 3. OAuth token from storage (auto-refreshed)
|
|
810
|
-
* 4.
|
|
811
|
-
* 5.
|
|
812
|
-
* 6.
|
|
812
|
+
* 4. API key persisted by a successful `/login`
|
|
813
|
+
* 5. Environment variable
|
|
814
|
+
* 6. Stored API key (e.g. a broker-migrated copy) — last resort, so an explicit env var wins
|
|
815
|
+
* 7. Fallback resolver (models.yml custom providers, last-resort)
|
|
813
816
|
*/
|
|
814
817
|
getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
|
|
815
818
|
/**
|
|
@@ -908,6 +911,7 @@ export declare class AuthStorage {
|
|
|
908
911
|
rotateSessionCredential(provider: string, sessionId: string | undefined, options?: {
|
|
909
912
|
error?: unknown;
|
|
910
913
|
modelId?: string;
|
|
914
|
+
apiKey?: string;
|
|
911
915
|
signal?: AbortSignal;
|
|
912
916
|
}): Promise<boolean>;
|
|
913
917
|
/**
|
|
@@ -986,9 +990,10 @@ export declare class AuthStorage {
|
|
|
986
990
|
* 1. Runtime override (`--api-key`).
|
|
987
991
|
* 2. Config override (`models.yml` `providers.<name>.apiKey`).
|
|
988
992
|
* 3. Stored OAuth credential.
|
|
989
|
-
* 4.
|
|
990
|
-
* 5.
|
|
991
|
-
* 6.
|
|
993
|
+
* 4. API key persisted by a successful `/login`.
|
|
994
|
+
* 5. Env var — overrides a stored static api_key (e.g. a stale broker copy).
|
|
995
|
+
* 6. Stored api_key credential.
|
|
996
|
+
* 7. Fallback resolver.
|
|
992
997
|
*
|
|
993
998
|
* The string is purely informational; consumers must not parse it.
|
|
994
999
|
*/
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import http2 from "node:http2";
|
|
1
2
|
import { type JsonValue } from "@bufbuild/protobuf";
|
|
3
|
+
import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
4
|
+
import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
2
5
|
import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, ToolCall, ToolResultMessage } from "../types.js";
|
|
3
6
|
import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
|
|
4
7
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
@@ -39,6 +42,8 @@ export interface BlockState {
|
|
|
39
42
|
export interface UsageState {
|
|
40
43
|
sawTokenDelta: boolean;
|
|
41
44
|
}
|
|
45
|
+
/** Exported for tests: drives one Cursor server message through the stream (exec waits mark the stream busy). */
|
|
46
|
+
export declare function handleServerMessage(msg: AgentServerMessage, output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, blobStore: Map<string, Uint8Array>, h2Request: http2.ClientHttp2Stream, execHandlers: CursorExecHandlers | undefined, onToolResult: CursorToolResultHandler | undefined, usageState: UsageState, requestContextTools: McpToolDefinition[], onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void): Promise<void>;
|
|
42
47
|
/** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
|
|
43
48
|
export declare function resolveExecHandler<TArgs, TResult>(args: TArgs, handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined, onToolResult: CursorToolResultHandler | undefined, buildFromToolResult: (toolResult: ToolResultMessage) => TResult, buildRejected: (reason: string) => TResult, buildError: (error: string) => TResult): Promise<{
|
|
44
49
|
execResult: TResult;
|
|
@@ -382,6 +382,8 @@ type ResponsesToolCallBlock = ToolCall & {
|
|
|
382
382
|
[kStreamingLastParseLen]?: number;
|
|
383
383
|
};
|
|
384
384
|
export declare function appendReasoningSummaryPart(item: ResponseReasoningItem, part: ResponseReasoningItem["summary"][number]): void;
|
|
385
|
+
/** Chooses the final reasoning text without discarding content already streamed into the block. */
|
|
386
|
+
export declare function finalizeReasoningThinking(item: ResponseReasoningItem, streamedThinking: string): string;
|
|
385
387
|
export declare function appendReasoningSummaryTextDelta(item: ResponseReasoningItem, block: ThinkingContent, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
|
|
386
388
|
export declare function appendReasoningSummaryPartDone(item: ResponseReasoningItem, block: ThinkingContent, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
|
|
387
389
|
export declare function appendMessageContentPart(item: ResponseOutputMessage, part: ResponseContentPartAddedEvent["part"] | undefined): void;
|
|
@@ -400,9 +402,10 @@ export interface ProcessResponsesStreamOptions {
|
|
|
400
402
|
onFirstToken?: () => void;
|
|
401
403
|
onOutputItemDone?: (item: ResponseOutputItem) => void;
|
|
402
404
|
/**
|
|
403
|
-
* Called when a terminal `response.completed
|
|
404
|
-
* is successfully processed. Only invoked on the
|
|
405
|
-
* thrown failure (`response.failed`) and
|
|
405
|
+
* Called when a terminal `response.completed`, `response.incomplete`, or
|
|
406
|
+
* `response.done` event is successfully processed. Only invoked on the
|
|
407
|
+
* successful-completion path; thrown failure (`response.failed`) and
|
|
408
|
+
* cancellation paths never call this.
|
|
406
409
|
* Used by callers to detect premature stream closure (i.e. the stream ended
|
|
407
410
|
* without a recognized terminal event).
|
|
408
411
|
*/
|
|
@@ -13,10 +13,15 @@
|
|
|
13
13
|
import type { AssistantMessageEventStream, Context, Model, OptionsForApi } from "../types.js";
|
|
14
14
|
import { AssistantMessageEventStream as EventStreamImpl } from "../utils/event-stream.js";
|
|
15
15
|
import type { BedrockOptions } from "./amazon-bedrock.js";
|
|
16
|
+
import type { CursorOptions } from "./cursor.js";
|
|
17
|
+
interface CursorProviderModule {
|
|
18
|
+
streamCursor: (model: Model<"cursor-agent">, context: Context, options: CursorOptions) => AssistantMessageEventStream;
|
|
19
|
+
}
|
|
16
20
|
interface BedrockProviderModule {
|
|
17
21
|
streamBedrock: (model: Model<"bedrock-converse-stream">, context: Context, options: BedrockOptions) => AssistantMessageEventStream;
|
|
18
22
|
}
|
|
19
23
|
export declare function setBedrockProviderModule(module: BedrockProviderModule): void;
|
|
24
|
+
export declare function setCursorProviderModule(module: CursorProviderModule): void;
|
|
20
25
|
export declare const streamAnthropic: (model: Model<"anthropic-messages">, context: Context, options: OptionsForApi<"anthropic-messages">) => EventStreamImpl;
|
|
21
26
|
export declare const streamAzureOpenAIResponses: (model: Model<"azure-openai-responses">, context: Context, options: OptionsForApi<"azure-openai-responses">) => EventStreamImpl;
|
|
22
27
|
export declare const streamGoogle: (model: Model<"google-generative-ai">, context: Context, options: OptionsForApi<"google-generative-ai">) => EventStreamImpl;
|
|
@@ -23,6 +23,8 @@ export interface OAuthCallbackFlowOptions {
|
|
|
23
23
|
* an actionable message before opening the browser.
|
|
24
24
|
*/
|
|
25
25
|
allowPortFallback?: boolean;
|
|
26
|
+
/** Skip the local callback server entirely; the user pastes the code or redirect URL back. */
|
|
27
|
+
manualInputOnly?: boolean;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Abstract base class for OAuth flows with local callback servers.
|
|
@@ -24,10 +24,7 @@ export declare function validateXAIEndpoint(url: string, field: string): string;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function isXAIAccessTokenExpiring(jwt: string, skewSeconds?: number): boolean;
|
|
26
26
|
/**
|
|
27
|
-
* xAI Grok OAuth
|
|
28
|
-
*
|
|
29
|
-
* Uses a fixed redirect URI so the callback server fails fast instead of
|
|
30
|
-
* falling back to a random port that xAI's redirect_uri allowlist rejects.
|
|
27
|
+
* xAI Grok OAuth code flow (Hermes `_xai_oauth_loopback_login` L5315-5469).
|
|
31
28
|
*/
|
|
32
29
|
export declare class XAIOAuthFlow extends OAuthCallbackFlow {
|
|
33
30
|
#private;
|
|
@@ -38,9 +35,6 @@ export declare class XAIOAuthFlow extends OAuthCallbackFlow {
|
|
|
38
35
|
}>;
|
|
39
36
|
exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
40
37
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Login with xAI Grok OAuth (SuperGrok Subscription).
|
|
43
|
-
*/
|
|
44
38
|
export declare function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCredentials>;
|
|
45
39
|
/**
|
|
46
40
|
* Refresh an xAI OAuth access token using a stored refresh_token.
|
|
@@ -265,9 +265,10 @@ declare const ALL: ({
|
|
|
265
265
|
readonly name: "xAI";
|
|
266
266
|
} | {
|
|
267
267
|
readonly id: "xai-oauth";
|
|
268
|
-
readonly name: "xAI Grok OAuth (SuperGrok
|
|
268
|
+
readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
|
|
269
269
|
readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<import("./oauth/index.js").OAuthCredentials>;
|
|
270
270
|
readonly refreshToken: (credentials: import("./oauth/index.js").OAuthCredentials) => Promise<import("./oauth/index.js").OAuthCredentials>;
|
|
271
|
+
readonly pasteCodeFlow: true;
|
|
271
272
|
} | {
|
|
272
273
|
readonly id: "xiaomi";
|
|
273
274
|
readonly name: "Xiaomi MiMo";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types.js";
|
|
2
2
|
export declare const xaiOauthProvider: {
|
|
3
3
|
readonly id: "xai-oauth";
|
|
4
|
-
readonly name: "xAI Grok OAuth (SuperGrok
|
|
4
|
+
readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
|
|
5
5
|
readonly login: (cb: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
|
|
6
6
|
readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
|
|
7
|
+
readonly pasteCodeFlow: true;
|
|
7
8
|
};
|
package/dist/types/types.d.ts
CHANGED
|
@@ -537,6 +537,8 @@ export interface AssistantMessage {
|
|
|
537
537
|
stopReason: StopReason;
|
|
538
538
|
stopDetails?: StopDetails | null;
|
|
539
539
|
errorMessage?: string;
|
|
540
|
+
/** Per-tool abort messages used when an aborted assistant turn needs different placeholder results per tool call. */
|
|
541
|
+
toolCallAbortMessages?: Record<string, string>;
|
|
540
542
|
/** HTTP status surfaced by the provider when the request failed. Populated by every provider's catch block alongside `errorMessage` so consumers (auth retry, telemetry, UI) can branch without regex-scraping the message. */
|
|
541
543
|
errorStatus?: number;
|
|
542
544
|
/** Structured machine-readable error classifier; see `utils/error-id.ts` for bit layout and helpers. */
|
|
@@ -22,6 +22,13 @@ export declare class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
22
22
|
fail(err: unknown): void;
|
|
23
23
|
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
24
24
|
result(): Promise<R>;
|
|
25
|
+
/** True while local work tracked via {@link trackLocalWork} is pending. */
|
|
26
|
+
get hasPendingLocalWork(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Track a local-work promise so idle watchdogs on this stream do not treat
|
|
29
|
+
* the event silence while it is pending as a provider stall.
|
|
30
|
+
*/
|
|
31
|
+
trackLocalWork<TWork>(work: Promise<TWork>): Promise<TWork>;
|
|
25
32
|
}
|
|
26
33
|
export declare class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
27
34
|
constructor();
|
|
@@ -83,6 +83,16 @@ export interface IdleTimeoutIteratorOptions {
|
|
|
83
83
|
* keepalive/no-op events from keeping a stalled tool call alive forever.
|
|
84
84
|
*/
|
|
85
85
|
isProgressItem?: (item: unknown) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Reports consumer-side local work in flight for the stream: the provider
|
|
88
|
+
* transport is waiting on a server-requested local tool bridge (e.g. the
|
|
89
|
+
* Cursor exec channel) before anything can flow upstream again. While it
|
|
90
|
+
* returns true, an expired idle / first-item deadline slides forward
|
|
91
|
+
* instead of aborting — the silence is ours, not a provider stall. The
|
|
92
|
+
* watchdog re-arms with a full budget once the local work completes, so a
|
|
93
|
+
* provider that stalls afterwards is still caught.
|
|
94
|
+
*/
|
|
95
|
+
hasPendingLocalWork?: () => boolean;
|
|
86
96
|
/**
|
|
87
97
|
* Cancel iteration as soon as this signal aborts. Required for caller-driven
|
|
88
98
|
* cancellation (ESC) when the underlying transport does not surface signal
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.3.
|
|
4
|
+
"version": "16.3.13",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.3.13",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.3.13",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.3.13",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Broker-aware auth-storage discovery used by both the coding-agent runtime and
|
|
3
|
-
* the catalog model generator. Keeps the precedence logic (env → config.yml →
|
|
3
|
+
* the catalog model generator. Keeps the precedence logic (env → config.yml/config.yaml →
|
|
4
4
|
* token file → local SQLite) in one place so build-time tooling sees the same
|
|
5
5
|
* credentials as the TUI.
|
|
6
6
|
*/
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getConfigRootDir,
|
|
13
13
|
isEnoent,
|
|
14
14
|
logger,
|
|
15
|
+
MAIN_CONFIG_FILENAMES,
|
|
15
16
|
} from "@oh-my-pi/pi-utils";
|
|
16
17
|
import { YAML } from "bun";
|
|
17
18
|
import { AuthStorage } from "../auth-storage";
|
|
@@ -72,21 +73,24 @@ interface ConfigSnapshot {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
for (const filename of MAIN_CONFIG_FILENAMES) {
|
|
77
|
+
const configPath = path.join(agentDir, filename);
|
|
78
|
+
try {
|
|
79
|
+
const raw = await Bun.file(configPath).text();
|
|
80
|
+
const parsed = YAML.parse(raw);
|
|
81
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
82
|
+
const record = parsed as Record<string, unknown>;
|
|
83
|
+
const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
|
|
84
|
+
const token =
|
|
85
|
+
typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
|
|
86
|
+
return { url, token };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (isEnoent(err)) continue;
|
|
89
|
+
logger.warn("auth-broker config unreadable", { path: configPath, error: String(err) });
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
89
92
|
}
|
|
93
|
+
return {};
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
function resolveSnapshotTtlMs(): number {
|
|
@@ -104,7 +108,7 @@ function resolveSnapshotTtlMs(): number {
|
|
|
104
108
|
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
105
109
|
*
|
|
106
110
|
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
107
|
-
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
111
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml` or `<agentDir>/config.yaml`.
|
|
108
112
|
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
109
113
|
*
|
|
110
114
|
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
@@ -61,6 +61,41 @@ function toCredentialBlockSnapshot(block: StoredCredentialBlock): CredentialBloc
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function credentialBlockSnapshotsEqual(
|
|
65
|
+
left: readonly CredentialBlockSnapshot[] | undefined,
|
|
66
|
+
right: readonly CredentialBlockSnapshot[] | undefined,
|
|
67
|
+
): boolean {
|
|
68
|
+
const leftBlocks = left ?? [];
|
|
69
|
+
const rightBlocks = right ?? [];
|
|
70
|
+
if (leftBlocks.length !== rightBlocks.length) return false;
|
|
71
|
+
for (let index = 0; index < leftBlocks.length; index += 1) {
|
|
72
|
+
const leftBlock = leftBlocks[index]!;
|
|
73
|
+
const rightBlock = rightBlocks[index]!;
|
|
74
|
+
if (
|
|
75
|
+
leftBlock.providerKey !== rightBlock.providerKey ||
|
|
76
|
+
leftBlock.blockScope !== rightBlock.blockScope ||
|
|
77
|
+
leftBlock.blockedUntilMs !== rightBlock.blockedUntilMs
|
|
78
|
+
) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function snapshotBlocksChanged(previous: readonly SnapshotEntry[], next: readonly SnapshotEntry[]): boolean {
|
|
86
|
+
const previousBlocksById = new Map<number, readonly CredentialBlockSnapshot[] | undefined>();
|
|
87
|
+
for (const entry of previous) previousBlocksById.set(entry.id, entry.blocks);
|
|
88
|
+
for (const entry of next) {
|
|
89
|
+
const previousBlocks = previousBlocksById.get(entry.id);
|
|
90
|
+
if (!credentialBlockSnapshotsEqual(previousBlocks, entry.blocks)) return true;
|
|
91
|
+
previousBlocksById.delete(entry.id);
|
|
92
|
+
}
|
|
93
|
+
for (const previousBlocks of previousBlocksById.values()) {
|
|
94
|
+
if (previousBlocks && previousBlocks.length > 0) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
64
99
|
function credentialEntryWithBlocks(
|
|
65
100
|
entry: AuthCredentialSnapshotEntry,
|
|
66
101
|
blocks: readonly CredentialBlockSnapshot[] | undefined,
|
|
@@ -173,6 +208,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
173
208
|
#cache: Map<string, CacheEntry> = new Map();
|
|
174
209
|
#usageCache?: UsageCacheEntry;
|
|
175
210
|
#usageInflight?: Promise<UsageReport[] | null>;
|
|
211
|
+
#usageCacheEpoch = 0;
|
|
176
212
|
#closed = false;
|
|
177
213
|
/**
|
|
178
214
|
* `true` once the SSE consumer received its first frame and hasn't dropped
|
|
@@ -202,10 +238,9 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
202
238
|
|
|
203
239
|
#applySnapshot(snapshot: SnapshotResponse, generation: number): void {
|
|
204
240
|
const nowMs = Date.now();
|
|
205
|
-
this.#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
241
|
+
const credentials = snapshot.credentials.map(entry => this.#normalizeSnapshotEntryBlocks(entry, nowMs));
|
|
242
|
+
if (snapshotBlocksChanged(this.#snapshot.credentials, credentials)) this.#invalidateUsageCache();
|
|
243
|
+
this.#snapshot = { ...snapshot, credentials };
|
|
209
244
|
this.#generation = generation;
|
|
210
245
|
this.#snapshotReceivedAt = nowMs;
|
|
211
246
|
const onSnapshot = this.#onSnapshot;
|
|
@@ -304,6 +339,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
304
339
|
): void {
|
|
305
340
|
const incoming = this.#normalizeSnapshotEntryBlocks(entry, Date.now());
|
|
306
341
|
const index = this.#snapshot.credentials.findIndex(candidate => candidate.id === incoming.id);
|
|
342
|
+
const previousBlocks = index === -1 ? undefined : this.#snapshot.credentials[index]?.blocks;
|
|
343
|
+
if (!credentialBlockSnapshotsEqual(previousBlocks, incoming.blocks)) this.#invalidateUsageCache();
|
|
307
344
|
const credentials =
|
|
308
345
|
index === -1
|
|
309
346
|
? [...this.#snapshot.credentials, incoming]
|
|
@@ -314,6 +351,8 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
314
351
|
}
|
|
315
352
|
|
|
316
353
|
#removeStreamCredential(id: number, refresher: RefresherSchedule, generation: number, serverNowMs: number): void {
|
|
354
|
+
const removed = this.#snapshot.credentials.find(entry => entry.id === id);
|
|
355
|
+
if (removed?.blocks && removed.blocks.length > 0) this.#invalidateUsageCache();
|
|
317
356
|
const credentials = this.#snapshot.credentials.filter(entry => entry.id !== id);
|
|
318
357
|
this.#snapshot = { ...this.#snapshot, generation, serverNowMs, refresher, credentials };
|
|
319
358
|
this.#generation = generation;
|
|
@@ -376,6 +415,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
376
415
|
|
|
377
416
|
upsertCredentialBlock(block: StoredCredentialBlock): void {
|
|
378
417
|
this.#upsertSnapshotBlock(block);
|
|
418
|
+
this.#invalidateUsageCache();
|
|
379
419
|
const body = toCredentialBlockSnapshot(block);
|
|
380
420
|
void this.#client
|
|
381
421
|
.upsertCredentialBlock(block.credentialId, body)
|
|
@@ -394,6 +434,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
394
434
|
|
|
395
435
|
deleteCredentialBlocks(credentialId: number): void {
|
|
396
436
|
this.#deleteSnapshotBlocks(credentialId);
|
|
437
|
+
this.#invalidateUsageCache();
|
|
397
438
|
void this.#client
|
|
398
439
|
.deleteCredentialBlocks(credentialId)
|
|
399
440
|
.then(() => {
|
|
@@ -698,6 +739,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
698
739
|
}
|
|
699
740
|
}
|
|
700
741
|
|
|
742
|
+
#invalidateUsageCache(): void {
|
|
743
|
+
this.#usageCache = undefined;
|
|
744
|
+
this.#usageInflight = undefined;
|
|
745
|
+
this.#usageCacheEpoch += 1;
|
|
746
|
+
}
|
|
747
|
+
|
|
701
748
|
/**
|
|
702
749
|
* Store-level hook consumed by `AuthStorage` — routes refresh through the
|
|
703
750
|
* broker so the actual refresh token never leaves the broker host. Returns
|
|
@@ -834,9 +881,11 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
834
881
|
return Promise.resolve(cached.reports);
|
|
835
882
|
}
|
|
836
883
|
if (this.#usageInflight) return this.#usageInflight;
|
|
884
|
+
const epoch = this.#usageCacheEpoch;
|
|
837
885
|
const inflight = this.#client
|
|
838
886
|
.fetchUsage()
|
|
839
887
|
.then(body => {
|
|
888
|
+
if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
|
|
840
889
|
this.#usageCache = { reports: body.reports, fetchedAt: Date.now() };
|
|
841
890
|
return body.reports;
|
|
842
891
|
})
|
|
@@ -845,11 +894,12 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
|
|
|
845
894
|
// Documented 15s TTL fallback: cache the null so sequential callers
|
|
846
895
|
// don't re-hit the broker while it's still down. See
|
|
847
896
|
// docs/auth-broker-gateway.md § "Client-side single-flight".
|
|
897
|
+
if (epoch !== this.#usageCacheEpoch) return this.#loadUsageReports();
|
|
848
898
|
this.#usageCache = { reports: null, fetchedAt: Date.now() };
|
|
849
899
|
return null;
|
|
850
900
|
})
|
|
851
901
|
.finally(() => {
|
|
852
|
-
this.#usageInflight = undefined;
|
|
902
|
+
if (this.#usageInflight === inflight) this.#usageInflight = undefined;
|
|
853
903
|
});
|
|
854
904
|
this.#usageInflight = inflight;
|
|
855
905
|
return inflight;
|
package/src/auth-retry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OAuthAccess } from "./auth-storage";
|
|
2
2
|
import * as AIError from "./error";
|
|
3
3
|
import { isAuthRetryableError } from "./error/auth-classify";
|
|
4
|
+
import { isUsageLimit } from "./error/flags";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Context passed to an {@link ApiKeyResolver} on each resolution attempt.
|
|
@@ -23,6 +24,8 @@ export interface ApiKeyResolveContext {
|
|
|
23
24
|
lastChance: boolean;
|
|
24
25
|
/** The auth error that triggered this re-resolution, or `undefined` on the initial resolve. */
|
|
25
26
|
error: unknown;
|
|
27
|
+
/** Bearer used by the failed attempt, when the caller can expose it. */
|
|
28
|
+
previousKey?: string;
|
|
26
29
|
/** Caller cancel signal, threaded into any credential refresh / rotation work. */
|
|
27
30
|
signal?: AbortSignal;
|
|
28
31
|
}
|
|
@@ -87,9 +90,11 @@ export async function resolveRetryKey(
|
|
|
87
90
|
lastChance: boolean,
|
|
88
91
|
error: unknown,
|
|
89
92
|
signal?: AbortSignal,
|
|
93
|
+
previousKey?: string,
|
|
90
94
|
): Promise<string | undefined> {
|
|
91
95
|
try {
|
|
92
|
-
|
|
96
|
+
const rotateSibling = lastChance || (!lastChance && isUsageLimit(error));
|
|
97
|
+
return (await resolver({ lastChance: rotateSibling, error, signal, previousKey })) || undefined;
|
|
93
98
|
} catch {
|
|
94
99
|
return undefined;
|
|
95
100
|
}
|
|
@@ -136,7 +141,7 @@ export async function withAuth<T>(
|
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
for (let i = 0; i < AUTH_RETRY_STEPS.length; i++) {
|
|
139
|
-
const nextKey = await resolveRetryKey(resolver, AUTH_RETRY_STEPS[i]!, lastError, signal);
|
|
144
|
+
const nextKey = await resolveRetryKey(resolver, AUTH_RETRY_STEPS[i]!, lastError, signal, lastKey);
|
|
140
145
|
if (nextKey === undefined || nextKey === lastKey) continue;
|
|
141
146
|
lastKey = nextKey;
|
|
142
147
|
try {
|