@oh-my-pi/pi-ai 17.4.0 → 17.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +23 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/auth-retry.d.ts +8 -3
  5. package/dist/types/auth-storage.d.ts +9 -0
  6. package/dist/types/dialect/qwen-xml.d.ts +6 -0
  7. package/dist/types/error/auth-classify.d.ts +8 -8
  8. package/dist/types/error/oauth.d.ts +2 -1
  9. package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
  10. package/dist/types/providers/cursor-pi-args.d.ts +21 -0
  11. package/dist/types/providers/cursor.d.ts +23 -3
  12. package/dist/types/providers/google-types.d.ts +6 -0
  13. package/dist/types/providers/mock.d.ts +3 -1
  14. package/dist/types/providers/openai-shared.d.ts +6 -1
  15. package/dist/types/registry/oauth/google-antigravity.d.ts +2 -6
  16. package/dist/types/registry/oauth/index.d.ts +2 -3
  17. package/dist/types/registry/oauth/types.d.ts +2 -1
  18. package/dist/types/registry/oauth/xai-oauth.d.ts +2 -2
  19. package/dist/types/registry/registry.d.ts +1 -1
  20. package/dist/types/registry/types.d.ts +2 -1
  21. package/dist/types/registry/xai-oauth.d.ts +1 -1
  22. package/dist/types/types.d.ts +21 -0
  23. package/dist/types/utils/stream-markup-healing.d.ts +1 -1
  24. package/package.json +8 -6
  25. package/src/auth-retry.ts +41 -3
  26. package/src/auth-storage.ts +229 -75
  27. package/src/dialect/qwen-xml.ts +82 -0
  28. package/src/error/auth-classify.ts +10 -8
  29. package/src/error/oauth.ts +2 -1
  30. package/src/providers/amazon-bedrock.ts +12 -19
  31. package/src/providers/anthropic.ts +29 -35
  32. package/src/providers/cursor/exec-modern.ts +2 -0
  33. package/src/providers/cursor-pi-args.ts +38 -2
  34. package/src/providers/cursor.ts +357 -32
  35. package/src/providers/google-shared.ts +11 -12
  36. package/src/providers/google-types.ts +7 -0
  37. package/src/providers/mock.ts +4 -1
  38. package/src/providers/openai-codex-responses.ts +34 -8
  39. package/src/providers/openai-completions.ts +2 -2
  40. package/src/providers/openai-shared.ts +72 -14
  41. package/src/registry/oauth/google-antigravity.ts +247 -162
  42. package/src/registry/oauth/index.ts +3 -3
  43. package/src/registry/oauth/types.ts +2 -1
  44. package/src/registry/oauth/xai-oauth.ts +14 -7
  45. package/src/registry/types.ts +2 -1
  46. package/src/registry/xai-oauth.ts +2 -2
  47. package/src/stream.ts +8 -4
  48. package/src/types.ts +22 -0
  49. package/src/utils/schema/CONSTRAINTS.md +1 -0
  50. package/src/utils/schema/fields.ts +20 -4
  51. package/src/utils/stream-markup-healing.ts +9 -2
@@ -71,6 +71,8 @@ export interface AuthRetryKeyState {
71
71
  refreshedCurrent: boolean;
72
72
  /** Whether the legacy non-usage auth path already switched to one sibling. */
73
73
  legacyAuthSwitchUsed: boolean;
74
+ /** Whether this operation already replayed once after an explicit token-refresh request. */
75
+ tokenRefreshReplayUsed?: boolean;
74
76
  /** Total outbound attempts accepted for this logical operation, including the initial request. */
75
77
  attempts: number;
76
78
  }
@@ -83,9 +85,11 @@ export declare function resolveNextAuthRetryKey(state: AuthRetryKeyState, resolv
83
85
  * retry (identical to the legacy static-key path).
84
86
  * - A resolver → initial `attempt`, then resolver-driven retries until the
85
87
  * applicable policy is exhausted, the resolver declines or cycles, or the
86
- * operation reaches {@link AUTH_RETRY_MAX_ATTEMPTS}. Ordinary 401/auth
87
- * failures retain one refresh-same plus one sibling switch; 403/usage-limit
88
- * failures rotate directly through distinct siblings.
88
+ * operation reaches {@link AUTH_RETRY_MAX_ATTEMPTS}. An explicit typed
89
+ * token-refresh request gets exactly one refresh-current replay and never
90
+ * enters sibling rotation. Ordinary 401/auth failures retain one
91
+ * refresh-same plus one sibling switch; 403/usage-limit failures rotate
92
+ * directly through distinct siblings.
89
93
  *
90
94
  * Used by non-streaming consumers (image generation, web search, completion
91
95
  * helpers). The streaming driver in `stream.ts` implements the same policy with
@@ -134,6 +138,7 @@ export interface WithOAuthAccessOptions {
134
138
  * `projectId`, `enterpriseUrl`) instead of bare API-key bytes.
135
139
  *
136
140
  * - initial → `getOAuthAccess` (or `opts.seed`).
141
+ * - typed token-refresh request → one forced refresh-current replay, then stop.
137
142
  * - 401/auth failure → one `getOAuthAccess` with `forceRefresh: true` for the
138
143
  * current account, then sibling rotation.
139
144
  * - 403/usage-limit failure → `rotateSessionCredential` directly, without a
@@ -742,6 +742,15 @@ export declare class AuthStorage {
742
742
  * Remove a runtime API key override.
743
743
  */
744
744
  removeRuntimeApiKey(provider: string): void;
745
+ /**
746
+ * Install a runtime usage provider override (not persisted to disk).
747
+ *
748
+ * Runtime overrides are checked before the configured resolver, including its
749
+ * built-in fallback. Removing the override restores that resolver unchanged.
750
+ */
751
+ setRuntimeUsageProvider(provider: Provider, usageProvider: UsageProvider, apiKey?: string): void;
752
+ /** Remove a runtime usage provider override and restore configured/default resolution. */
753
+ removeRuntimeUsageProvider(provider: Provider): void;
745
754
  /**
746
755
  * Register a per-provider API key sourced from user configuration
747
756
  * (e.g. `models.yml` `providers.<name>.apiKey`). Higher priority than
@@ -0,0 +1,6 @@
1
+ import type { InbandScanEvent, InbandScanner } from "./types.js";
2
+ export declare class QwenXmlInbandScanner implements InbandScanner {
3
+ #private;
4
+ feed(chunk: string): InbandScanEvent[];
5
+ flush(): InbandScanEvent[];
6
+ }
@@ -8,13 +8,13 @@ export declare function isDefinitiveOAuthFailure(errorMsg: string): boolean;
8
8
  /** Whether an upstream response explicitly says the supplied OAuth bearer was invalidated. */
9
9
  export declare function isInvalidatedOAuthTokenError(error: unknown): boolean;
10
10
  /**
11
- * Whether an upstream failure should rotate to a sibling credential: a hard
12
- * `401`, a `403` (token valid but access denied — plan, model policy, or org
13
- * restriction a sibling account may not share), an account-scoped policy
14
- * denial such as Codex `cyber_policy`, a body-classified usage limit (Codex
15
- * `usage_limit_reached`, Anthropic account rate-limit, Google
16
- * `resource_exhausted`, OpenAI `insufficient_quota`, …), or a bare `429`
17
- * whose payload did not preserve a richer quota code. Transient 429s
18
- * (`Too many requests`, per-minute caps) stay in the upstream-backoff lane.
11
+ * Whether an upstream failure should retry through the credential resolver: a
12
+ * typed token-refresh request, a hard `401`, a `403` (token valid but access
13
+ * denied — plan, model policy, or org restriction a sibling account may not
14
+ * share), an account-scoped policy denial such as Codex `cyber_policy`, a
15
+ * body-classified usage limit (Codex `usage_limit_reached`, Anthropic account
16
+ * rate-limit, Google `resource_exhausted`, OpenAI `insufficient_quota`, …), or
17
+ * a bare `429` whose payload did not preserve a richer quota code. Transient
18
+ * 429s (`Too many requests`, per-minute caps) stay in the upstream-backoff lane.
19
19
  */
20
20
  export declare function isAuthRetryableError(error: unknown): boolean;
@@ -33,7 +33,8 @@ export interface OAuthErrorOptions {
33
33
  /**
34
34
  * A failure inside an interactive OAuth / device-code login flow. The `kind`
35
35
  * pinpoints the stage. Timeout/polling are classified transient; everything
36
- * else is a hard auth failure so the credential layer does not silently retry.
36
+ * else is a hard auth failure. The auth-retry classifier separately treats
37
+ * `token-refresh` as an explicit request to refresh and replay once.
37
38
  */
38
39
  export declare class OAuthError extends Error {
39
40
  readonly kind: OAuthErrorKind;
@@ -17,7 +17,7 @@ import type { ToolResultMessage } from "../../types.js";
17
17
  * virtual registry. Re-exported here because this is where the frame builders
18
18
  * and their translation are consumed together.
19
19
  */
20
- export { omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, } from "../cursor-pi-args.js";
20
+ export { cursorEditOwnedReadPath, cursorRawReadPath, omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, } from "../cursor-pi-args.js";
21
21
  /** Flatten a tool result's content into the single `output` string the Pi frames carry. */
22
22
  export declare function piOutputText(toolResult: ToolResultMessage): string;
23
23
  /**
@@ -42,6 +42,27 @@ export declare function piReadPath(readPath: string, offset?: number, limit?: nu
42
42
  * the coding-agent read tool; the selector remains embedded in the path.
43
43
  */
44
44
  export declare function piReadPathHasRange(readPath: string): boolean;
45
+ /**
46
+ * Force a Cursor exec read onto `read`'s verbatim `:raw` selector.
47
+ *
48
+ * Native `editToolCall` (StrReplace) materializes via `readArgs` then
49
+ * `writeArgs`. The server treats the read result as file bytes and writes
50
+ * them back. A hashline-formatted native read (`[path#TAG]` + `LINE:`
51
+ * prefixes) poisons that cycle: the write would persist the markup.
52
+ * `:raw` is the existing selector that drops both. A path that already
53
+ * carries `raw` is left alone; a range-only selector gets `raw` inserted
54
+ * so the range still applies without the gutter.
55
+ */
56
+ export declare function cursorRawReadPath(readPath: string): string;
57
+ /**
58
+ * Path the edit-owned materialization read should execute.
59
+ *
60
+ * Range is composed first (`piReadPath` already uses `:raw` for a range),
61
+ * then a whole-file path is forced onto `:raw`. The caller must drop
62
+ * `offset`/`limit` after this so the bridge's `piReadPath` cannot append a
63
+ * second `:raw` onto the already-composed selector.
64
+ */
65
+ export declare function cursorEditOwnedReadPath(readPath: string, offset?: number, limit?: number): string | null;
45
66
  /**
46
67
  * The same range as {@link piReadPath}, rendered for a transcript block rather
47
68
  * than for execution.
@@ -1,5 +1,5 @@
1
1
  import http2 from "node:http2";
2
- import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
2
+ import type { CursorRule, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
3
3
  import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
4
4
  import { type JsonValue } from "@oh-my-pi/pi-catalog/discovery/protobuf";
5
5
  import type { AssistantMessage, Context, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, Model, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
@@ -12,6 +12,8 @@ export interface CursorOptions extends StreamOptions {
12
12
  conversationId?: string;
13
13
  execHandlers?: CursorExecHandlers;
14
14
  onToolResult?: CursorToolResultHandler;
15
+ /** Wire model id selected after thinking-effort routing (`resolveWireModelId`). */
16
+ wireModelId?: string;
15
17
  }
16
18
  /**
17
19
  * Maps an opaque HTTP/2 negotiation failure into an actionable error.
@@ -32,7 +34,7 @@ export type ToolCallState = ToolCall & {
32
34
  [kStreamingBlockIndex]: number;
33
35
  [kStreamingPartialJson]?: string;
34
36
  [kStreamingLastParseLen]?: number;
35
- [kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm" | "web-fetch";
37
+ [kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "cursor-edit" | "connect-scm" | "web-fetch";
36
38
  [kStreamingEnvelopeId]?: string;
37
39
  [kCursorExecResolved]?: true;
38
40
  };
@@ -57,6 +59,17 @@ export interface BlockState {
57
59
  openToolCalls: Map<string, ToolCallState>;
58
60
  /** MCP call IDs synthesized from exec frames before their redundant streamed block arrives. */
59
61
  resolvedMcpToolCallIds: Set<string>;
62
+ /**
63
+ * Native `editToolCall` (StrReplace) ids whose materialization `readArgs` /
64
+ * `writeArgs` must stay raw and must not synthesize extra transcript blocks.
65
+ *
66
+ * Optional so existing test harnesses stay valid. Both the interaction
67
+ * envelope `call_id` and the inner `toolCallId` are recorded — exec frames
68
+ * pair on the inner id.
69
+ */
70
+ editOwnedToolCallIds?: Set<string>;
71
+ /** Edit blocks whose write already persisted a `toolResult`. */
72
+ pairedEditToolCallIds?: Set<string>;
60
73
  firstTokenTime: number | undefined;
61
74
  setTextBlock: (b: (TextContent & {
62
75
  [kStreamingBlockIndex]: number;
@@ -79,7 +92,7 @@ export interface UsageState {
79
92
  sawTokenDelta: boolean;
80
93
  }
81
94
  /** Exported for tests: drives one Cursor server message through the stream (exec waits mark the stream busy). */
82
- 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>;
95
+ 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[], requestContextRules?: CursorRule[], onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void): Promise<void>;
83
96
  /**
84
97
  * Exported for tests: verifies handler is invoked with correct `this` when passed as bound.
85
98
  *
@@ -191,6 +204,13 @@ export declare function mergeCursorMcpToolCallArgs(streamed: Record<string, unkn
191
204
  export declare function synthesizeCursorExecToolCall(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, toolCallId: string, toolName: string, args: Record<string, unknown>): void;
192
205
  /** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
193
206
  export declare function processInteractionUpdate(update: any, output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState, usageState: UsageState): void;
207
+ /**
208
+ * Cursor AgentService reconstructs the model prompt from `requestContext.rules`,
209
+ * not from the client-supplied `rootPromptMessagesJson` system blobs. Map each
210
+ * OMP system-prompt entry to a global CursorRule so always-apply rules survive
211
+ * that reconstruction.
212
+ */
213
+ export declare function buildCursorRequestContextRules(systemPrompt: readonly string[] | undefined): CursorRule[];
194
214
  export declare function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[];
195
215
  /**
196
216
  * Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
@@ -42,9 +42,15 @@ export interface Part {
42
42
  thought?: boolean;
43
43
  thoughtSignature?: string;
44
44
  inlineData?: InlineDataPart;
45
+ fileData?: FileDataPart;
45
46
  functionCall?: FunctionCallPart;
46
47
  functionResponse?: FunctionResponsePart;
47
48
  }
49
+ /** Remote media reference; the backend fetches `fileUri` server-side. */
50
+ export interface FileDataPart {
51
+ fileUri: string;
52
+ mimeType?: string;
53
+ }
48
54
  /** Conversation turn. Roles: `"user"`, `"model"`, optionally absent for system instructions. */
49
55
  export interface Content {
50
56
  role?: string;
@@ -118,6 +118,8 @@ export interface MockModelOptions {
118
118
  id?: string;
119
119
  /** Provider string used in the returned AssistantMessage. Defaults to `"mock"`. */
120
120
  provider?: string;
121
+ /** Base URL reported by the model. Defaults to `"mock://"`. */
122
+ baseUrl?: string;
121
123
  /** A sequence of responses, one per call. Accepts arrays, generators, or any iterable. */
122
124
  responses?: MockResponseSource;
123
125
  /** Fallback handler used when `responses` is exhausted. */
@@ -141,7 +143,7 @@ export declare class MockModel implements Model<MockApi> {
141
143
  readonly name: string;
142
144
  readonly api: MockApi;
143
145
  readonly provider: string;
144
- readonly baseUrl = "mock://";
146
+ readonly baseUrl: string;
145
147
  readonly reasoning: boolean;
146
148
  readonly input: ("text" | "image")[];
147
149
  readonly cost: Model["cost"];
@@ -520,7 +520,12 @@ export interface SequentialCutoffSummaryState {
520
520
  export declare function createSequentialCutoffSummaryState(): SequentialCutoffSummaryState;
521
521
  /** Chooses final reasoning text without making sequential-cutoff results disagree with emitted deltas. */
522
522
  export declare function finalizeReasoningThinking(item: ResponseReasoningItem, streamedThinking: string, cutoff?: SequentialCutoffSummaryState): string;
523
- export declare function appendReasoningSummaryTextDelta(item: ResponseReasoningItem, block: ThinkingContent, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
523
+ export declare function appendReasoningSummaryTextDelta(item: ResponseReasoningItem, block: ThinkingContent, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number, summaryIndex?: number): void;
524
+ /**
525
+ * Applies a completed reasoning-summary snapshot, including providers that omit
526
+ * the preceding summary-part event.
527
+ */
528
+ export declare function applyReasoningSummaryTextDone(item: ResponseReasoningItem, block: ThinkingContent, text: string, summaryIndex: number, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
524
529
  export declare function appendReasoningSummaryPartDone(item: ResponseReasoningItem, block: ThinkingContent, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
525
530
  /**
526
531
  * Applies an atomic `response.reasoning_summary_text.done` snapshot.
@@ -1,13 +1,9 @@
1
1
  import type { OAuthController, OAuthCredentials } from "./types.js";
2
+ /** Cloud Code Assist metadata sent by native Antigravity control-plane requests. */
2
3
  export declare const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA: Readonly<{
3
4
  ideType: "ANTIGRAVITY";
4
5
  }>;
5
- export interface AntigravityOnboardMetadata {
6
- ide_type: string;
7
- ide_version: string;
8
- ide_name: string;
9
- }
10
- export declare function getAntigravityOnboardMetadata(): AntigravityOnboardMetadata;
6
+ /** Authenticate an Antigravity account and resolve its Cloud Code Assist project. */
11
7
  export declare function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials>;
12
8
  /**
13
9
  * Refresh Antigravity token
@@ -19,10 +19,9 @@ export declare function getOAuthProvider(id: OAuthProviderId): OAuthProviderInte
19
19
  */
20
20
  export declare function unregisterOAuthProviders(sourceId: string): void;
21
21
  /**
22
- * Refresh token for any OAuth provider.
23
- * Saves the new credentials and returns the new access token.
22
+ * Refresh a built-in OAuth grant, cancelling provider work when refresh ownership ends.
24
23
  */
25
- export declare function refreshOAuthToken(provider: OAuthProvider, credentials: OAuthCredentials): Promise<OAuthCredentials>;
24
+ export declare function refreshOAuthToken(provider: OAuthProvider, credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
26
25
  /**
27
26
  * Build API-key bytes for a provider from an already-fresh OAuth credential.
28
27
  *
@@ -80,7 +80,8 @@ export interface OAuthProviderInterface {
80
80
  readonly name: string;
81
81
  readonly sourceId?: string;
82
82
  login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
83
- refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
83
+ /** Refresh a stored grant; the signal bounds provider network work to refresh ownership. */
84
+ refreshToken?(credentials: OAuthCredentials, signal?: AbortSignal): Promise<OAuthCredentials>;
84
85
  getApiKey?(credentials: OAuthCredentials): string;
85
86
  /** Store resulting OAuth credentials under a different provider id. */
86
87
  readonly storeCredentialsAs?: string;
@@ -41,6 +41,6 @@ export declare function loginXAIOAuth(ctrl: OAuthController): Promise<OAuthCrede
41
41
  * Refresh an xAI OAuth access token using a stored refresh_token.
42
42
  *
43
43
  * Re-runs OIDC discovery and re-validates the token endpoint before sending
44
- * the stored refresh token.
44
+ * the stored refresh token. Caller cancellation aborts every network request.
45
45
  */
46
- export declare function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl): Promise<OAuthCredentials>;
46
+ export declare function refreshXAIOAuthToken(refreshToken: string, fetchOverride?: FetchImpl, signal?: AbortSignal): Promise<OAuthCredentials>;
@@ -325,7 +325,7 @@ declare const ALL: ({
325
325
  readonly id: "xai-oauth";
326
326
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
327
327
  readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<import("./oauth/index.js").OAuthCredentials>;
328
- readonly refreshToken: (credentials: import("./oauth/index.js").OAuthCredentials) => Promise<import("./oauth/index.js").OAuthCredentials>;
328
+ readonly refreshToken: (credentials: import("./oauth/index.js").OAuthCredentials, signal?: AbortSignal) => Promise<import("./oauth/index.js").OAuthCredentials>;
329
329
  } | {
330
330
  readonly id: "xiaomi";
331
331
  readonly name: "Xiaomi MiMo";
@@ -64,7 +64,8 @@ export interface ProviderDefinition {
64
64
  /** Provider-owned authentication and endpoint setup for model discovery. */
65
65
  readonly prepareModelDiscovery?: ProviderModelDiscoveryPreparer;
66
66
  readonly login?: (callbacks: OAuthLoginCallbacks) => Promise<OAuthCredentials | string>;
67
- readonly refreshToken?: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
67
+ /** Refresh a stored grant; the signal bounds provider network work to refresh ownership. */
68
+ readonly refreshToken?: (credentials: OAuthCredentials, signal?: AbortSignal) => Promise<OAuthCredentials>;
68
69
  readonly getApiKey?: (credentials: OAuthCredentials) => string;
69
70
  /** Store OAuth credentials under a different provider id (e.g. `openai-codex-device` ⇒ `openai-codex`). */
70
71
  readonly storeCredentialsAs?: string;
@@ -3,5 +3,5 @@ export declare const xaiOauthProvider: {
3
3
  readonly id: "xai-oauth";
4
4
  readonly name: "xAI Grok OAuth (SuperGrok or X Premium+)";
5
5
  readonly login: (cb: OAuthLoginCallbacks) => Promise<OAuthCredentials>;
6
- readonly refreshToken: (credentials: OAuthCredentials) => Promise<OAuthCredentials>;
6
+ readonly refreshToken: (credentials: OAuthCredentials, signal?: AbortSignal) => Promise<OAuthCredentials>;
7
7
  };
@@ -317,6 +317,8 @@ export interface StreamOptions {
317
317
  providerSessionState?: Map<string, ProviderSessionState>;
318
318
  /** Canonical Codex compaction classification; ignored by other providers. */
319
319
  codexCompaction?: CodexCompactionRequestContext;
320
+ /** Codex Code Mode tool exposure snapshot emitted as `tool_namespaces_info` turn metadata; ignored by other providers. */
321
+ toolNamespacesInfo?: unknown;
320
322
  /**
321
323
  * Optional per-provider concurrent request cap for LLM stream calls. Keys are
322
324
  * provider ids (`model.provider`); positive numeric values cap in-flight
@@ -540,6 +542,13 @@ export interface AnthropicServerToolContent {
540
542
  [key: string]: unknown;
541
543
  };
542
544
  }
545
+ /** Provider-native uploaded file reference for image reuse without retransmitting bytes. */
546
+ export interface ProviderFileReference {
547
+ provider: "openai" | "anthropic" | "google";
548
+ id?: string;
549
+ uri?: string;
550
+ expiresAt?: number;
551
+ }
543
552
  export interface ImageContent {
544
553
  type: "image";
545
554
  data: string;
@@ -550,6 +559,18 @@ export interface ImageContent {
550
559
  * default `auto` downscale). Providers without a detail knob ignore it.
551
560
  */
552
561
  detail?: "auto" | "low" | "high" | "original";
562
+ /** Provider-native file reference preferred only by its matching provider. */
563
+ providerFile?: ProviderFileReference;
564
+ /**
565
+ * Optional https mirror of `data`, served by a caller-run blob server.
566
+ * Providers whose API fetches remote images send this URL instead of the
567
+ * base64 payload; every other provider ignores it. `data` remains the
568
+ * source of truth — the URL must serve exactly those bytes, and callers
569
+ * are responsible for keeping it stable across turns (prefix caches hash
570
+ * the URL string, and Anthropic silently forgets images when a resent
571
+ * turn differs byte-wise).
572
+ */
573
+ url?: string;
553
574
  }
554
575
  export type ComputerAction = {
555
576
  type: "click";
@@ -11,7 +11,7 @@ export interface HealedToolCall {
11
11
  readonly name: string;
12
12
  readonly arguments: string;
13
13
  }
14
- export type StreamMarkupHealingPattern = "kimi" | "dsml" | "thinking";
14
+ export type StreamMarkupHealingPattern = "kimi" | "dsml" | "qwen" | "thinking";
15
15
  export interface StreamMarkupHealingOptions {
16
16
  readonly pattern: StreamMarkupHealingPattern;
17
17
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.4.0",
4
+ "version": "17.4.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
- "author": "Can Boluk",
7
+ "author": "Stencil Labs, Inc.",
8
8
  "contributors": [
9
9
  "Mario Zechner"
10
10
  ],
@@ -37,10 +37,10 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/omptype": "17.4.0",
41
- "@oh-my-pi/pi-catalog": "17.4.0",
42
- "@oh-my-pi/pi-utils": "17.4.0",
43
- "@oh-my-pi/pi-wire": "17.4.0"
40
+ "@oh-my-pi/omptype": "17.4.2",
41
+ "@oh-my-pi/pi-catalog": "17.4.2",
42
+ "@oh-my-pi/pi-utils": "17.4.2",
43
+ "@oh-my-pi/pi-wire": "17.4.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/bun": "^1.3.14"
@@ -52,6 +52,8 @@
52
52
  "src",
53
53
  "README.md",
54
54
  "CHANGELOG.md",
55
+ "LICENSE",
56
+ "THIRD-PARTY-NOTICES.txt",
55
57
  "dist/types"
56
58
  ],
57
59
  "exports": {
package/src/auth-retry.ts CHANGED
@@ -130,6 +130,8 @@ export interface AuthRetryKeyState {
130
130
  refreshedCurrent: boolean;
131
131
  /** Whether the legacy non-usage auth path already switched to one sibling. */
132
132
  legacyAuthSwitchUsed: boolean;
133
+ /** Whether this operation already replayed once after an explicit token-refresh request. */
134
+ tokenRefreshReplayUsed?: boolean;
133
135
  /** Total outbound attempts accepted for this logical operation, including the initial request. */
134
136
  attempts: number;
135
137
  }
@@ -140,6 +142,7 @@ export function createAuthRetryKeyState(initialKey: string): AuthRetryKeyState {
140
142
  lastKey: initialKey,
141
143
  refreshedCurrent: false,
142
144
  legacyAuthSwitchUsed: false,
145
+ tokenRefreshReplayUsed: false,
143
146
  attempts: 1,
144
147
  };
145
148
  }
@@ -161,6 +164,14 @@ export async function resolveNextAuthRetryKey(
161
164
  ): Promise<string | undefined> {
162
165
  if (signal?.aborted) return undefined;
163
166
  if (state.attempts >= AUTH_RETRY_MAX_ATTEMPTS) return undefined;
167
+ if (error instanceof AIError.OAuthError && error.kind === "token-refresh") {
168
+ if (state.tokenRefreshReplayUsed) return undefined;
169
+ state.tokenRefreshReplayUsed = true;
170
+ const refreshed = await resolveRetryKey(resolver, false, error, signal, state.lastKey);
171
+ state.refreshedCurrent = true;
172
+ if (signal?.aborted || refreshed === undefined) return undefined;
173
+ return acceptRetryKey(state, refreshed, true);
174
+ }
164
175
  const directRotation = isDirectCredentialRotationError(error);
165
176
  if (!directRotation) {
166
177
  if (state.legacyAuthSwitchUsed) return undefined;
@@ -207,9 +218,11 @@ async function runOAuthAttempt<T>(
207
218
  * retry (identical to the legacy static-key path).
208
219
  * - A resolver → initial `attempt`, then resolver-driven retries until the
209
220
  * applicable policy is exhausted, the resolver declines or cycles, or the
210
- * operation reaches {@link AUTH_RETRY_MAX_ATTEMPTS}. Ordinary 401/auth
211
- * failures retain one refresh-same plus one sibling switch; 403/usage-limit
212
- * failures rotate directly through distinct siblings.
221
+ * operation reaches {@link AUTH_RETRY_MAX_ATTEMPTS}. An explicit typed
222
+ * token-refresh request gets exactly one refresh-current replay and never
223
+ * enters sibling rotation. Ordinary 401/auth failures retain one
224
+ * refresh-same plus one sibling switch; 403/usage-limit failures rotate
225
+ * directly through distinct siblings.
213
226
  *
214
227
  * Used by non-streaming consumers (image generation, web search, completion
215
228
  * helpers). The streaming driver in `stream.ts` implements the same policy with
@@ -296,6 +309,7 @@ export interface WithOAuthAccessOptions {
296
309
  * `projectId`, `enterpriseUrl`) instead of bare API-key bytes.
297
310
  *
298
311
  * - initial → `getOAuthAccess` (or `opts.seed`).
312
+ * - typed token-refresh request → one forced refresh-current replay, then stop.
299
313
  * - 401/auth failure → one `getOAuthAccess` with `forceRefresh: true` for the
300
314
  * current account, then sibling rotation.
301
315
  * - 403/usage-limit failure → `rotateSessionCredential` directly, without a
@@ -331,6 +345,7 @@ export async function withOAuthAccess<T>(
331
345
  let attemptCount = 1;
332
346
  let legacyAuthSwitchUsed = false;
333
347
  let refreshedCurrent = false;
348
+ let tokenRefreshReplayUsed = false;
334
349
  let attemptResult = await runOAuthAttempt(lastAccess, attempt, isAuthError);
335
350
  if (attemptResult.ok) return attemptResult.result;
336
351
 
@@ -338,6 +353,29 @@ export async function withOAuthAccess<T>(
338
353
  while (true) {
339
354
  let next: OAuthAccess | undefined;
340
355
  if (signal?.aborted || attemptCount >= AUTH_RETRY_MAX_ATTEMPTS) break;
356
+ const tokenRefreshReplay = lastError instanceof AIError.OAuthError && lastError.kind === "token-refresh";
357
+ if (tokenRefreshReplay) {
358
+ if (tokenRefreshReplayUsed) break;
359
+ tokenRefreshReplayUsed = true;
360
+ refreshedCurrent = true;
361
+ try {
362
+ next = await storage.getOAuthAccess(provider, sessionId, { forceRefresh: true, signal });
363
+ } catch {
364
+ next = undefined;
365
+ }
366
+ if (signal?.aborted || !next) break;
367
+ const bearer = next.accessToken;
368
+ if (attemptedBearers.has(bearer) || attemptCount >= AUTH_RETRY_MAX_ATTEMPTS) break;
369
+ attemptedCredentialIdentities.add(oauthCredentialIdentity(next));
370
+ attemptedBearers.add(bearer);
371
+ attemptCount += 1;
372
+ lastAccess = next;
373
+ attemptResult = await runOAuthAttempt(next, attempt, isAuthError);
374
+ if (attemptResult.ok) return attemptResult.result;
375
+ lastError = attemptResult.error;
376
+ continue;
377
+ }
378
+
341
379
  const directRotation = isDirectCredentialRotationError(lastError);
342
380
  if (!directRotation) {
343
381
  if (legacyAuthSwitchUsed) break;