-
Notifications
You must be signed in to change notification settings - Fork 633
fix(vision): sync captions into Responses passthrough #1047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,6 +275,68 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten | |
| }; | ||
| } | ||
|
|
||
| function isPlainRecord(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
|
|
||
| /** | ||
| * Keep the native Responses passthrough body aligned with image replacements made in the parsed | ||
| * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize | ||
| * `context.messages`; updating only the latter would send the original pixels to a text-only | ||
| * Responses upstream even after the vision sidecar produced a caption. | ||
| * | ||
| * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses | ||
| * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally | ||
| * untouched. | ||
| */ | ||
| function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void { | ||
| const rawBody = parsed._rawBody; | ||
| if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input) || descriptions.length === 0) return; | ||
|
|
||
| let nextDescription = 0; | ||
| const rewriteImages = (value: unknown, nonEmptyImageUrlsOnly: boolean): unknown => { | ||
| if (Array.isArray(value)) { | ||
| let changed = false; | ||
| const rewritten = value.map(entry => { | ||
| const next = rewriteImages(entry, nonEmptyImageUrlsOnly); | ||
| if (next !== entry) changed = true; | ||
| return next; | ||
| }); | ||
| return changed ? rewritten : value; | ||
| } | ||
| if (!isPlainRecord(value)) return value; | ||
| if (value.type === "input_image" && typeof value.image_url === "string") { | ||
| if (nonEmptyImageUrlsOnly && value.image_url.length === 0) return value; | ||
| const description = descriptions[nextDescription++]; | ||
| return description === undefined ? value : { type: "input_text", text: description }; | ||
|
Comment on lines
+308
to
+311
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Remove empty On Line 309, an empty Remove the empty part or replace it with the omission marker. Apply the same result when As per path instructions, flag provider/adapter contract drift in 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
| return value; | ||
| }; | ||
|
|
||
| let changed = false; | ||
| const input = rawBody.input.map(item => { | ||
| if (!isPlainRecord(item)) return item; | ||
| const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : ""); | ||
| const role = typeof item.role === "string" ? item.role : ""; | ||
| const isMessageContent = ( | ||
| (type === "message" && (role === "user" || role === "developer")) | ||
| || type === "agent_message" | ||
| ); | ||
| const field = isMessageContent | ||
| ? "content" | ||
| : (type === "function_call_output" || type === "custom_tool_call_output") | ||
| ? "output" | ||
| : undefined; | ||
| if (!field) return item; | ||
| const rewritten = rewriteImages(item[field], isMessageContent); | ||
| if (rewritten === item[field]) return item; | ||
| changed = true; | ||
| return { ...item, [field]: rewritten }; | ||
| }); | ||
|
|
||
| if (changed) rawBody.input = input; | ||
| } | ||
|
|
||
| function sha256(value: string | Uint8Array): string { | ||
| return createHash("sha256").update(value).digest("hex"); | ||
| } | ||
|
|
@@ -425,6 +487,7 @@ export async function describeImagesInPlace( | |
|
|
||
| // 3. Rebuild each message, replacing image parts with their descriptions in order. | ||
| let oi = 0; | ||
| const descriptions: string[] = []; | ||
| for (const { msg, parts } of targets) { | ||
| const newParts: OcxContentPart[] = []; | ||
| for (const p of parts) { | ||
|
|
@@ -433,6 +496,7 @@ export async function describeImagesInPlace( | |
| continue; | ||
| } | ||
| const replacement = renderDescription(outcomes[oi++]); | ||
| descriptions.push(replacement.text); | ||
| const reservation = translatorBudget?.reserveTransient( | ||
| descriptionEncoder.encode(replacement.text).byteLength, | ||
| { kind: "request_copies" }, | ||
|
|
@@ -442,6 +506,7 @@ export async function describeImagesInPlace( | |
| } | ||
| msg.content = newParts; | ||
| } | ||
| syncRawBodyImageDescriptions(parsed, descriptions); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -452,13 +517,15 @@ export async function describeImagesInPlace( | |
| */ | ||
| export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean { | ||
| let stripped = false; | ||
| const descriptions: string[] = []; | ||
| for (const msg of parsed.context.messages) { | ||
| if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue; | ||
| const parts = msg.content as OcxContentPart[]; | ||
| if (!parts.some(p => p.type === "image")) continue; | ||
| msg.content = parts.map(p => { | ||
| if (p.type !== "image") return p; | ||
| const replacement = { type: "text", text: "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]" } as OcxContentPart; | ||
| descriptions.push((replacement as OcxTextContent).text); | ||
| const reservation = translatorBudget?.reserveTransient( | ||
| descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength, | ||
| { kind: "request_copies" }, | ||
|
|
@@ -468,5 +535,6 @@ export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: | |
| }); | ||
| stripped = true; | ||
| } | ||
| if (stripped) syncRawBodyImageDescriptions(parsed, descriptions); | ||
| return stripped; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.