@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 +12 -0
- package/dist/types/providers/cursor.d.ts +2 -0
- package/dist/types/providers/google-types.d.ts +6 -0
- package/dist/types/types.d.ts +19 -0
- package/package.json +5 -5
- package/src/auth-storage.ts +17 -1
- package/src/providers/anthropic.ts +29 -35
- package/src/providers/cursor.ts +52 -3
- package/src/providers/google-shared.ts +11 -12
- package/src/providers/google-types.ts +7 -0
- package/src/providers/openai-codex-responses.ts +3 -1
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-shared.ts +14 -10
- package/src/stream.ts +6 -0
- package/src/types.ts +20 -0
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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.4.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.4.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.4.
|
|
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"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1024
|
-
if (
|
|
1025
|
-
|
|
1026
|
-
|
|
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) {
|
package/src/providers/cursor.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
|
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:
|
|
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 =
|