@oh-my-pi/pi-ai 17.4.1 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.4.2] - 2026-08-21
6
+
7
+ ### Added
8
+
9
+ - Image content blocks accept an optional `url` mirror: providers whose APIs fetch remote images (Anthropic url sources, OpenAI/xAI Responses and Chat Completions `image_url`, Google `fileData`) send the URL instead of the inline base64 payload.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed Cursor thinking-effort selection being cosmetic: collapsed effort-routed families (GPT-5.6 Luna/Sol/Terra, Grok 4.5/4.6) now send the effort-routed wire model id instead of always pinning the `-none` off tier ([#9246](https://github.com/can1357/oh-my-pi/issues/9246)).
14
+ - Fixed OAuth preflight refresh stranding a peer-rotated credential: when a concurrent process rotated a rotating-refresh-token grant (e.g. Anthropic) during preflight, the resolve pass skipped the freshly reloaded row and failed the request with no credentials for single-account setups ([#9194](https://github.com/can1357/oh-my-pi/issues/9194)).
15
+ - Fixed Cursor reasoning-sibling models (e.g. `gpt-5.4-mini-low`, `gpt-5.6-sol-xhigh`) failing with `resource_exhausted` (errorId 528384): the per-effort GPT slug is now split into its base model id plus a `{ id: "reasoning", value: <effort> }` request parameter, matching the official `cursor-agent` wire shape, instead of sending the sibling slug as the wire model id with no parameters ([#9164](https://github.com/can1357/oh-my-pi/issues/9164)).
16
+
5
17
  ## [17.4.1] - 2026-08-21
6
18
 
7
19
  ### Added
@@ -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.
@@ -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;
@@ -542,6 +542,13 @@ export interface AnthropicServerToolContent {
542
542
  [key: string]: unknown;
543
543
  };
544
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
+ }
545
552
  export interface ImageContent {
546
553
  type: "image";
547
554
  data: string;
@@ -552,6 +559,18 @@ export interface ImageContent {
552
559
  * default `auto` downscale). Providers without a detail knob ignore it.
553
560
  */
554
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;
555
574
  }
556
575
  export type ComputerAction = {
557
576
  type: "click";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.4.1",
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
7
  "author": "Stencil Labs, Inc.",
@@ -37,10 +37,10 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/omptype": "17.4.1",
41
- "@oh-my-pi/pi-catalog": "17.4.1",
42
- "@oh-my-pi/pi-utils": "17.4.1",
43
- "@oh-my-pi/pi-wire": "17.4.1"
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"
@@ -4965,13 +4965,29 @@ export class AuthStorage {
4965
4965
  // so if this branch only blocked the row (like the transient case), the
4966
4966
  // definitive failure would never reach `#tryOAuthCredential`'s own
4967
4967
  // disable logic and the row would be retried forever instead of torn down.
4968
- await this.#disableDefinitiveOAuthFailure(
4968
+ const outcome = await this.#disableDefinitiveOAuthFailure(
4969
4969
  provider,
4970
4970
  credentialId,
4971
4971
  candidate.selection.credential,
4972
4972
  candidate.selection.index,
4973
4973
  errorMsg,
4974
4974
  );
4975
+ if (
4976
+ outcome !== "disabled" &&
4977
+ credentialId !== undefined &&
4978
+ this.#syncOAuthSelectionFromStore(provider, candidate.selection, credentialId)
4979
+ ) {
4980
+ // A peer rotated this row (or won the disable CAS) between our
4981
+ // snapshot and the refresh; the helper reloaded storage and the row
4982
+ // still exists, so it now holds a valid, freshly rotated credential.
4983
+ // Re-sync the candidate onto it and leave it eligible so the final
4984
+ // pass retries with the live token instead of stranding it (mirrors
4985
+ // #tryOAuthCredential's peer-rotated re-resolve). If the peer instead
4986
+ // deleted/disabled the row, the re-sync fails and we fall through to
4987
+ // preflightFailures — leaving a stale index could rebind the candidate
4988
+ // to a sibling account with the wrong prefetched usage/plan.
4989
+ return;
4990
+ }
4975
4991
  } else if (credentialId !== undefined) {
4976
4992
  const latestIndex = this.#getStoredCredentials(provider).findIndex(
4977
4993
  entry => entry.id === credentialId,
@@ -914,7 +914,15 @@ async function resizeAnthropicManyImageContent(
914
914
  let changed = false;
915
915
  const next = await Promise.all(
916
916
  content.map(async block => {
917
- if (block.type !== "image") return block;
917
+ // Remotely referenced blocks never put base64 on the wire, so their size
918
+ // cannot violate the many-image request budget — and resizing would
919
+ // desync fallback bytes from the advertised remote image.
920
+ if (
921
+ block.type !== "image" ||
922
+ block.url ||
923
+ (block.providerFile?.provider === "anthropic" && block.providerFile.id)
924
+ )
925
+ return block;
918
926
  let resized = anthropicManyImageResizeCache.get(block);
919
927
  if (resized === undefined) {
920
928
  resized = await limit(() => resizeAnthropicManyImageBlock(block));
@@ -971,19 +979,14 @@ async function prepareAnthropicManyImageContext(context: Context, supportsImages
971
979
  return { ...context, messages };
972
980
  }
973
981
 
982
+ type AnthropicImageSource =
983
+ | { type: "base64"; media_type: AnthropicImageMediaType; data: string }
984
+ | { type: "url"; url: string }
985
+ | { type: "file"; file_id: string };
986
+
974
987
  type AnthropicToolResultContent =
975
988
  | string
976
- | Array<
977
- | { type: "text"; text: string }
978
- | {
979
- type: "image";
980
- source: {
981
- type: "base64";
982
- media_type: AnthropicImageMediaType;
983
- data: string;
984
- };
985
- }
986
- >;
989
+ | Array<{ type: "text"; text: string } | { type: "image"; source: AnthropicImageSource }>;
987
990
 
988
991
  /**
989
992
  * Convert content blocks to Anthropic API format
@@ -992,17 +995,7 @@ function convertContentBlocks(
992
995
  content: (TextContent | ImageContent)[],
993
996
  supportsImages = true,
994
997
  ): AnthropicToolResultContent {
995
- const blocks: Array<
996
- | { type: "text"; text: string }
997
- | {
998
- type: "image";
999
- source: {
1000
- type: "base64";
1001
- media_type: AnthropicImageMediaType;
1002
- data: string;
1003
- };
1004
- }
1005
- > = [];
998
+ const blocks: Array<{ type: "text"; text: string } | { type: "image"; source: AnthropicImageSource }> = [];
1006
999
  let sawText = false;
1007
1000
  let sawImage = false;
1008
1001
 
@@ -1020,21 +1013,22 @@ function convertContentBlocks(
1020
1013
  continue;
1021
1014
  }
1022
1015
 
1023
- const mediaType = normalizeAnthropicImageMediaType(block.mimeType);
1024
- if (!mediaType) {
1025
- blocks.push({ type: "text", text: `[unsupported image: ${block.mimeType}]` });
1026
- continue;
1016
+ let source: AnthropicImageSource;
1017
+ if (block.providerFile?.provider === "anthropic" && block.providerFile.id) {
1018
+ source = { type: "file", file_id: block.providerFile.id };
1019
+ } else if (block.url) {
1020
+ source = { type: "url", url: block.url };
1021
+ } else {
1022
+ const mediaType = normalizeAnthropicImageMediaType(block.mimeType);
1023
+ if (!mediaType) {
1024
+ blocks.push({ type: "text", text: `[unsupported image: ${block.mimeType}]` });
1025
+ continue;
1026
+ }
1027
+ source = { type: "base64", media_type: mediaType, data: block.data };
1027
1028
  }
1028
1029
 
1029
1030
  sawImage = true;
1030
- blocks.push({
1031
- type: "image",
1032
- source: {
1033
- type: "base64",
1034
- media_type: mediaType,
1035
- data: block.data,
1036
- },
1037
- });
1031
+ blocks.push({ type: "image", source });
1038
1032
  }
1039
1033
 
1040
1034
  if (!supportsImages) {
@@ -1,7 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import http2 from "node:http2";
4
- import type { ConversationStep, CursorRule, McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
4
+ import type {
5
+ ConversationStep,
6
+ CursorRule,
7
+ McpToolDefinition,
8
+ RequestedModel_ModelParameterbytes,
9
+ } from "@oh-my-pi/pi-catalog/discovery/cursor-proto";
5
10
  import {
6
11
  AgentClientMessageSchema,
7
12
  AgentConversationTurnStructureSchema,
@@ -106,6 +111,7 @@ import {
106
111
  RequestContextResultSchema,
107
112
  RequestContextSchema,
108
113
  RequestContextSuccessSchema,
114
+ RequestedModel_ModelParameterbytesSchema,
109
115
  RequestedModelSchema,
110
116
  ResumeActionSchema,
111
117
  SelectedContextSchema,
@@ -151,7 +157,8 @@ import {
151
157
  toBinary,
152
158
  toJson,
153
159
  } from "@oh-my-pi/pi-catalog/discovery/protobuf";
154
- import { isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
160
+ import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
161
+ import { isKimiK3ModelId, parseOpenAIModel } from "@oh-my-pi/pi-catalog/identity";
155
162
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
156
163
  import {
157
164
  $env,
@@ -330,6 +337,8 @@ export interface CursorOptions extends StreamOptions {
330
337
  conversationId?: string;
331
338
  execHandlers?: CursorExecHandlers;
332
339
  onToolResult?: CursorToolResultHandler;
340
+ /** Wire model id selected after thinking-effort routing (`resolveWireModelId`). */
341
+ wireModelId?: string;
333
342
  }
334
343
 
335
344
  const CONNECT_END_STREAM_FLAG = 0b00000010;
@@ -5011,6 +5020,45 @@ function extractImages(content: (TextContent | ImageContent)[]) {
5011
5020
  );
5012
5021
  }
5013
5022
 
5023
+ /**
5024
+ * Resolve the Cursor Run wire model id and its parameter list.
5025
+ *
5026
+ * Cursor's `GetUsableModels` lists reasoning models as per-effort sibling
5027
+ * slugs (`gpt-5.4-mini-low`, `gpt-5.6-sol-high`), and OMP copies those ids 1:1.
5028
+ * The Run endpoint rejects a sibling slug as the wire `model_id` with
5029
+ * `resource_exhausted` (errorId 528384); the official `cursor-agent` splits the
5030
+ * slug into its base model id plus a `reasoning` effort parameter. Mirror that
5031
+ * for OpenAI-family ids: strip a trailing effort tier and emit
5032
+ * `{ id: "reasoning", value: <effort> }`.
5033
+ *
5034
+ * Non-OpenAI ids pass through unchanged — Cursor-native ids (`composer-*`,
5035
+ * `cursor-grok-*`, `default`) carry no effort suffix, and Claude/other siblings
5036
+ * need additional parameters (`thinking`, `context`) whose per-model values are
5037
+ * not exposed by the decoded `GetUsableModels` schema, so guessing them would
5038
+ * re-trigger 528384.
5039
+ */
5040
+ function resolveCursorWireModel(
5041
+ model: Model<"cursor-agent">,
5042
+ requestModelId?: string,
5043
+ ): {
5044
+ modelId: string;
5045
+ parameters: RequestedModel_ModelParameterbytes[];
5046
+ } {
5047
+ const wireModelId = requestModelId ?? model.requestModelId ?? model.id;
5048
+ // Cursor's fast lane follows the effort token (`-high-fast`), while the
5049
+ // standard lane ends at it (`-high`). Preserve the lane in the base id.
5050
+ const match = /^(.*)-(minimal|low|medium|high|xhigh|max)(-fast)?$/.exec(wireModelId);
5051
+ const base = match?.[1];
5052
+ const effort = match?.[2];
5053
+ if (base && effort && (THINKING_EFFORTS as readonly string[]).includes(effort) && parseOpenAIModel(base) !== null) {
5054
+ return {
5055
+ modelId: `${base}${match[3] ?? ""}`,
5056
+ parameters: [create(RequestedModel_ModelParameterbytesSchema, { id: "reasoning", value: effort })],
5057
+ };
5058
+ }
5059
+ return { modelId: wireModelId, parameters: [] };
5060
+ }
5061
+
5014
5062
  export async function buildGrpcRequest(
5015
5063
  model: Model<"cursor-agent">,
5016
5064
  context: Context,
@@ -5117,7 +5165,7 @@ export async function buildGrpcRequest(
5117
5165
  turns,
5118
5166
  });
5119
5167
 
5120
- const wireModelId = model.requestModelId ?? model.id;
5168
+ const { modelId: wireModelId, parameters: wireParameters } = resolveCursorWireModel(model, options?.wireModelId);
5121
5169
  const cursorMaxMode = model.cursorMaxMode === true;
5122
5170
  const modelDetails = create(ModelDetailsSchema, {
5123
5171
  modelId: wireModelId,
@@ -5128,6 +5176,7 @@ export async function buildGrpcRequest(
5128
5176
  const requestedModel = create(RequestedModelSchema, {
5129
5177
  modelId: wireModelId,
5130
5178
  maxMode: cursorMaxMode,
5179
+ parameters: wireParameters,
5131
5180
  });
5132
5181
 
5133
5182
  let runRequest = create(AgentRunRequestSchema, {
@@ -53,6 +53,15 @@ export { normalizeSchemaForGoogle };
53
53
 
54
54
  type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
55
55
 
56
+ function convertGoogleImagePart(image: ImageContent): Part {
57
+ if (image.providerFile?.provider === "google" && image.providerFile.uri) {
58
+ return { fileData: { fileUri: image.providerFile.uri, mimeType: image.mimeType } };
59
+ }
60
+ return image.url
61
+ ? { fileData: { fileUri: image.url, mimeType: image.mimeType } }
62
+ : { inlineData: { mimeType: image.mimeType, data: image.data } };
63
+ }
64
+
56
65
  /**
57
66
  * Thinking level for Gemini 3 models. Mirrors Google's `ThinkingLevel` enum values.
58
67
  * Defined here (not in any specific provider) so all Google providers can reference it
@@ -211,12 +220,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
211
220
  if (text.trim().length === 0) continue;
212
221
  parts.push({ text });
213
222
  } else if (supportsImages) {
214
- parts.push({
215
- inlineData: {
216
- mimeType: item.mimeType,
217
- data: item.data,
218
- },
219
- });
223
+ parts.push(convertGoogleImagePart(item));
220
224
  } else {
221
225
  omittedImages = true;
222
226
  }
@@ -315,12 +319,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
315
319
  ? "(see attached image)"
316
320
  : "";
317
321
 
318
- const imageParts: Part[] = imageContent.map(imageBlock => ({
319
- inlineData: {
320
- mimeType: imageBlock.mimeType,
321
- data: imageBlock.data,
322
- },
323
- }));
322
+ const imageParts = imageContent.map(convertGoogleImagePart);
324
323
 
325
324
  const includeId = supportsFunctionPartId(model);
326
325
  const emittedName = emittedToolCallNames.get(msg.toolCallId);
@@ -65,10 +65,17 @@ export interface Part {
65
65
  thought?: boolean;
66
66
  thoughtSignature?: string;
67
67
  inlineData?: InlineDataPart;
68
+ fileData?: FileDataPart;
68
69
  functionCall?: FunctionCallPart;
69
70
  functionResponse?: FunctionResponsePart;
70
71
  }
71
72
 
73
+ /** Remote media reference; the backend fetches `fileUri` server-side. */
74
+ export interface FileDataPart {
75
+ fileUri: string;
76
+ mimeType?: string;
77
+ }
78
+
72
79
  /** Conversation turn. Roles: `"user"`, `"model"`, optionally absent for system instructions. */
73
80
  export interface Content {
74
81
  role?: string;
@@ -4503,7 +4503,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
4503
4503
 
4504
4504
  function normalizeInputMessageContent(
4505
4505
  model: Model<"openai-codex-responses">,
4506
- content: string | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }>,
4506
+ content:
4507
+ | string
4508
+ | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string; url?: string }>,
4507
4509
  ): ResponseInputContent[] {
4508
4510
  // gpt-5.x codex rejects reserved Harmony control-token spellings in input
4509
4511
  // data; escape the transport copy of untrusted user text so ordinary docs or
@@ -1950,7 +1950,7 @@ export function convertMessages(
1950
1950
  content.push({
1951
1951
  type: "image_url",
1952
1952
  image_url: {
1953
- url: `data:${item.mimeType};base64,${item.data}`,
1953
+ url: item.url ?? `data:${item.mimeType};base64,${item.data}`,
1954
1954
  // Chat Completions has no "original"; omit it (provider default).
1955
1955
  ...(item.detail && item.detail !== "original" ? { detail: item.detail } : {}),
1956
1956
  },
@@ -2257,7 +2257,7 @@ export function convertMessages(
2257
2257
  imageBlocks.push({
2258
2258
  type: "image_url",
2259
2259
  image_url: {
2260
- url: `data:${block.mimeType};base64,${block.data}`,
2260
+ url: block.url ?? `data:${block.mimeType};base64,${block.data}`,
2261
2261
  },
2262
2262
  });
2263
2263
  }
@@ -1630,6 +1630,18 @@ function clampResponsesImageDetail(
1630
1630
  return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
1631
1631
  }
1632
1632
 
1633
+ function convertResponsesInputImage(image: ImageContent, supportsImageDetailOriginal: boolean): ResponseInputImage {
1634
+ const detail = clampResponsesImageDetail(image.detail, supportsImageDetailOriginal);
1635
+ if (image.providerFile?.provider === "openai" && image.providerFile.id) {
1636
+ return { type: "input_image", detail, file_id: image.providerFile.id };
1637
+ }
1638
+ return {
1639
+ type: "input_image",
1640
+ detail,
1641
+ image_url: image.url ?? `data:${image.mimeType};base64,${image.data}`,
1642
+ };
1643
+ }
1644
+
1633
1645
  export function convertResponsesInputContent(
1634
1646
  content: string | Array<TextContent | ImageContent>,
1635
1647
  supportsImages: boolean,
@@ -1659,11 +1671,7 @@ export function convertResponsesInputContent(
1659
1671
  } satisfies ResponseInputText);
1660
1672
  }
1661
1673
  for (const item of imageBlocks) {
1662
- normalizedContent.push({
1663
- type: "input_image",
1664
- detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
1665
- image_url: `data:${item.mimeType};base64,${item.data}`,
1666
- } satisfies ResponseInputImage);
1674
+ normalizedContent.push(convertResponsesInputImage(item, supportsImageDetailOriginal));
1667
1675
  }
1668
1676
  if (omittedImages) {
1669
1677
  normalizedContent.push({
@@ -2338,11 +2346,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
2338
2346
  ];
2339
2347
  for (const block of toolResult.content) {
2340
2348
  if (block.type === "image") {
2341
- contentParts.push({
2342
- type: "input_image",
2343
- detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
2344
- image_url: `data:${block.mimeType};base64,${block.data}`,
2345
- } satisfies ResponseInputImage);
2349
+ contentParts.push(convertResponsesInputImage(block, supportsImageDetailOriginal));
2346
2350
  }
2347
2351
  }
2348
2352
  const imageMessage = { role: "user", content: contentParts } satisfies ResponseInput[number];
package/src/stream.ts CHANGED
@@ -2309,10 +2309,16 @@ function mapOptionsForApi<TApi extends Api>(
2309
2309
  case "cursor-agent": {
2310
2310
  const execHandlers = options?.cursorExecHandlers ?? options?.execHandlers;
2311
2311
  const onToolResult = options?.cursorOnToolResult ?? execHandlers?.onToolResult;
2312
+ const cursorModel = model as Model<"cursor-agent">;
2313
+ const effort =
2314
+ options?.reasoning && !options.disableReasoning && !options.forceReasoningOff && cursorModel.reasoning
2315
+ ? requireSupportedEffort(cursorModel, options.reasoning)
2316
+ : undefined;
2312
2317
  return castApi<"cursor-agent">({
2313
2318
  ...base,
2314
2319
  execHandlers,
2315
2320
  onToolResult,
2321
+ wireModelId: resolveWireModelId(cursorModel, effort),
2316
2322
  });
2317
2323
  }
2318
2324
 
package/src/types.ts CHANGED
@@ -740,6 +740,14 @@ export interface AnthropicServerToolContent {
740
740
  };
741
741
  }
742
742
 
743
+ /** Provider-native uploaded file reference for image reuse without retransmitting bytes. */
744
+ export interface ProviderFileReference {
745
+ provider: "openai" | "anthropic" | "google";
746
+ id?: string;
747
+ uri?: string;
748
+ expiresAt?: number;
749
+ }
750
+
743
751
  export interface ImageContent {
744
752
  type: "image";
745
753
  data: string; // base64 encoded image data
@@ -750,6 +758,18 @@ export interface ImageContent {
750
758
  * default `auto` downscale). Providers without a detail knob ignore it.
751
759
  */
752
760
  detail?: "auto" | "low" | "high" | "original";
761
+ /** Provider-native file reference preferred only by its matching provider. */
762
+ providerFile?: ProviderFileReference;
763
+ /**
764
+ * Optional https mirror of `data`, served by a caller-run blob server.
765
+ * Providers whose API fetches remote images send this URL instead of the
766
+ * base64 payload; every other provider ignores it. `data` remains the
767
+ * source of truth — the URL must serve exactly those bytes, and callers
768
+ * are responsible for keeping it stable across turns (prefix caches hash
769
+ * the URL string, and Anthropic silently forgets images when a resent
770
+ * turn differs byte-wise).
771
+ */
772
+ url?: string;
753
773
  }
754
774
 
755
775
  export type ComputerAction =