Skip to content

[agent] fix: refuse lossy media conversion at the final adapter boundary - #4562

Merged
lidge-jun merged 1 commit into
devfrom
agent/provider-parity-05-media-guard
Sep 14, 2026
Merged

[agent] fix: refuse lossy media conversion at the final adapter boundary#4562
lidge-jun merged 1 commit into
devfrom
agent/provider-parity-05-media-guard

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Summary

Close the remaining silent-success part of the provider input audit: recognized audio/file attachments must not become placeholder text and then be reported as a successful translated request.

This layer was implemented directly by ChatGPT in a separate worktree. It does not add native audio/file transport, fetch attachment URLs, decode content, change credentials or enable vendor CLI tools.

  • Inspect only typed input content and typed function/custom-tool output arrays. Never parse ordinary text, function arguments or schemas as attachments; never include filenames, URLs or payload bytes in the new errors.
  • Guard the final registered translated adapter: buildRequest follows the existing 400 response path; runTurn emits one typed, nonretryable unsupported_input_modality error before transport; localTerminal declines its success shortcut so the guarded builder can reject.
  • Preserve the original body and exclude effective Responses passthrough, including both Azure aliases. This retains existing native media semantics without claiming every upstream model supports them.
  • Native Chat keeps its own wire. Actual Chat-to-Responses projection rejects recognized audio/file content before losing it.
  • Fix the parent follow-up's legacy function-image regression: native eligibility diverts it, but the converter does not implement legacy call/result pairing. Return an explicit migration error instead of silently discarding that result.
  • Synchronize public Pi guidance and source-owner documentation, distinguishing implementation limits from provider capability claims.

Stack — review and merge bottom-up

Layer PR Base
1 #4534 — image normalization and explicit reasoning disable dev
2 #4535 — final-target control preservation and plaintext replay layer 1
3 #4536 — Google schema and Anthropic parallel-tool controls layer 2
4 #4539 — tool-image carriers, modality markers and integration fixes layer 3
5 this PR — explicit rejection instead of lossy media success agent/provider-parity-04-modality

This is a manual dependent PR chain, not native GitHub stack registration. No merge or automatic merge is requested.

Deliberate limits

The change closes silent-success behavior, not native media transport. Translated video markers remain adapter-specific. Native wire/model support remains upstream-dependent. Legacy image results must use modern tool_calls / role: "tool" / tool_call_id; this does not invent a legacy function-call bridge. The full CodeBuddy/Qoder client-tool bridge and unverified gateway vision declarations remain outside this patch.

Verification

Connected-Mac product checks: NOT RUN BY USER INSTRUCTION. No local test suite, typecheck, build, lint, structure/privacy validation or pre-push suite was executed for this layer. Commits and pushes use --no-verify; no repository workflow, dependency or global Git configuration was changed.

This is a draft pending exact-head hosted CI and the completed independent static review described below. Neither a written test nor a pending/skipped job is claimed as passing.

Separate ChatGPT Linux container: copied the exact two new production modules, verified their SHA-256 against commit c1b34d4532c8e7e55cfdf0f8cc73b4ac2d82f3fc, transpiled them with the container's existing TypeScript compiler, then ran node --test guard.test.mjs: 12 pass, 0 fail. This is scanner/guard runtime proof, not the full Bun suite or repository-wide type checking. A negative control removed the compiled guard and made 5 tests fail (exit 1); restoring it returned 12 pass, 0 fail. The original production TypeScript remained unchanged. No connected-Mac product command ran.

Hosted evidence at that head: gates passed; all 10 new adapter-input-media-guard.test.ts cases and all 5 new chat-media-translation.test.ts cases passed, including real registry/Azure passthrough and the real HTTP zero-upstream-send regression. Shards 1 and 3 completed successfully. Full test 4/4 failed later in the inherited release-version-line.test.ts: the stack carries package version 2.54.0, and tag v2.54.0 was published after the parent stack was prepared. Current dev carries 2.55.0. No test was disabled and no artificial version bump was made in this feature patch. That historical version-line failure is not current-head proof: the stack has now been synchronized with current dev; new-head hosted jobs are pending.

Desired regressions added:

  • tests/adapters/adapter-input-media-guard.test.ts: content-only scanning; file-ID-only images; ordinary text/arguments/schema exclusion; builder and local-terminal ordering; typed runTurn failure without underlying execution; actual registry guards; raw Responses and Azure preservation using the same request after a rejected translated attempt.
  • tests/responses/chat-media-translation.test.ts: user/tool audio and files rejected before conversion; native Chat messages unchanged; legacy function images explicitly rejected; real HTTP translated Responses requests return 400 with no upstream sends and no payload in the error.

Both files are registered in the existing layout map and expected fixture. Source/diff inspection and hook configuration inspection were performed without executing product validation. The independent read-only Aside review completed with no blocking bugs found in the inspected scope. It inspected the scanner, guard, registry and Chat projection plus both test files and the narrow error/stream owners. It ran no product code. Optional additional cases were identified, not represented as executed tests.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults: no new fetch/decoding, credential mutation, payload logging or tool-execution permission. Independent static review is complete; fresh hosted validation remains pending as stated above.

Current stack synchronization

The manual stack was synchronized bottom-up with dev@246b5cab432b03cbec1766c2faffac13d6e39321.
The repository's existing 2.55.0 version change came from that parent; no artificial feature-branch version bump or release-test suppression was used.

Order: #4534#4535#4536#4539#4562.
Current head: fff3aa4047f3b0e0eea0df08cd983f3776721cfa. Current base: agent/provider-parity-04-modality.
Each parent is an ancestor of its child. All five branches were pushed using git push --no-verify.

Fresh hosted CI is required at these new heads. Earlier green jobs or the historical 2.54.0 release-line failure are not represented as new-head results. The PR remains draft; no merge or release was performed. No product validation ran on the connected Mac.

Independent review result

The Aside reviewer completed its source-only review of the direct media-guard implementation with no blocking bugs found. It independently confirmed final-wire alias handling, native-Chat exclusion, preprojection rejection, legacy-image explicit failure, build/runTurn/local-terminal ownership and content-free errors. Optional coverage suggestions remain distinct from proven regressions; the review is not a live-provider or full new-head CI result.

The two directly authored scanner/guard modules retain exactly the same SHA-256 at synchronized head fff3aa4047f3b0e0eea0df08cd983f3776721cfa as the hash-checked container-tested implementation. Integration with the newly incorporated dev changes still requires that new head's hosted CI. No product tests ran on the connected Mac.

Summary by CodeRabbit

  • Bug Fixes

    • Unsupported audio and file attachments now produce clear conversion errors instead of being silently dropped.
    • Legacy image results from function responses are rejected explicitly.
    • Native provider routes continue to preserve supported media without exposing sensitive attachment details.
  • Documentation

    • Added guidance on attachment compatibility, native passthrough, translated requests, and media-related errors.
  • Tests

    • Added coverage for media detection, rejection behavior, native preservation, error safety, and prevention of unnecessary upstream requests.

@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 345b149a-3562-4242-aea0-02031c82e683

📥 Commits

Reviewing files that changed from the base of the PR and between 0a62878 and c8e7370.

📒 Files selected for processing (21)
  • devlog/_plan/260914_provider_parity_stack/050_residuals.md
  • devlog/_plan/260914_provider_parity_stack/060_direct_media_guard.md
  • docs-site/src/content/docs/guides/pi.md
  • scripts/test-layout/layout.json
  • src/adapters/input-media-guard.ts
  • src/adapters/registry.ts
  • src/chat/inbound.ts
  • src/responses/input-media.ts
  • structure/adapters/registry.md
  • structure/data-planes/inbound-compat.md
  • structure/providers/chat-compat.md
  • structure/providers/cursor.md
  • structure/providers/kiro.md
  • structure/providers/xai-grok.md
  • structure/runtime.md
  • structure/transports/byte-accounting.md
  • structure/transports/inventory.md
  • structure/transports/responses.md
  • tests/adapters/adapter-input-media-guard.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/chat-media-translation.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The change adds shared detection for untranslated audio and file inputs. Translated adapters and Chat projection now reject unsupported media explicitly, while native Responses, Azure, and Chat paths preserve existing media handling.

Changes

Input media admission

Layer / File(s) Summary
Detect and guard untranslated media
src/responses/input-media.ts, src/adapters/input-media-guard.ts, src/adapters/registry.ts
The scanner identifies audio, file, document, and file-ID-only image inputs. Non-Responses registered adapters reject these inputs during build, runTurn, or local-terminal handling.
Reject lossy Chat projection
src/chat/inbound.ts
Chat-to-Responses conversion rejects recognized audio and file content. Legacy function messages with image results now return an explicit conversion error.
Validate rejection and passthrough behavior
tests/adapters/adapter-input-media-guard.test.ts, tests/responses/chat-media-translation.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json
Tests cover detection boundaries, sanitized 400 errors, registry-installed guards, native media preservation, text passthrough, HTTP rejection before upstream calls, and test-layout mappings.
Document media admission contract
devlog/_plan/260914_provider_parity_stack/*.md, docs-site/src/content/docs/guides/pi.md, structure/**/*.md
Documentation describes explicit errors for unsupported translated media and separates translated-adapter admission from native raw passthrough.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ChatInbound
  participant RegisteredAdapter
  participant Upstream
  Client->>ChatInbound: submit request with media
  ChatInbound->>ChatInbound: inspect translatable content
  ChatInbound-->>Client: return sanitized 400 for lossy media
  Client->>RegisteredAdapter: submit translated request
  RegisteredAdapter->>RegisteredAdapter: inspect original _rawBody
  RegisteredAdapter-->>Client: return unsupported_input_modality
  Client->>Upstream: submit native Responses or Chat request
  Upstream-->>Client: preserve native media handling
Loading

Merge Risk: ⚪ Minimal · up to c8e73

Translated routes now explicitly reject unsupported media while native passthrough behavior remains preserved. No actionable merge-blocking issue is identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 6 files. (15 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing lossy media conversion at the final adapter boundary.
Full details: Docstring Coverage

Explanation

Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 6 files. (15 skipped: 15 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/provider-parity-05-media-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 62 / 80

이 PR은 provider-parity 스택의 다섯 번째 층입니다. 지금 dev(HEAD 246b5cab4, 패키지 2.55.0)에는 아직 src/adapters/input-media-guard.tssrc/responses/input-media.ts가 없습니다. 그 결과 번역(translated) 경로에서는 오디오·파일 첨부가 텍스트 마커로 바뀐 뒤에도 요청이 성공처럼 보일 수 있습니다. 이 변경은 그 조용한 성공을 막습니다. 인식된 audio/file(그리고 file_id만 있는 이미지)은 최종 등록된 번역 어댑터 경계에서 명시적으로 거절하고, 원본 Responses 패스스루(Azure 별칭 포함)와 네이티브 Chat 와이어는 그대로 둡니다.

구체적으로 src/responses/input-media.ts는 typed content 배열과 function/custom-tool output만 훑습니다. 일반 텍스트, function arguments, 스키마 속성은 첨부물로 해석하지 않고, 파일명·URL·페이로드 바이트를 에러 메시지에 넣지 않습니다. src/adapters/input-media-guard.tsbuildRequest에서 기존 400 경로로 막고, runTurn에서는 unsupported_input_modality를 한 번만 내보낸 뒤 전송을 시작하지 않으며, localTerminal의 성공 지름길을 막아 가드된 빌더가 거절하게 합니다. src/adapters/registry.ts에서는 effective wire가 openai-responses가 아닐 때만 가드를 씌웁니다. Chat 쪽은 src/chat/inbound.tschatCompletionsToResponsesBody에서 실제 투영이 일어날 때만 audio/file을 거절하고, 레거시 function 역할 이미지 결과는 조용히 버리지 않고 마이그레이션 에러를 냅니다.

테스트는 tests/adapters/adapter-input-media-guard.test.tstests/responses/chat-media-translation.test.ts로 스캐너·훅 순서·레지스트리 가드·실제 HTTP에서 업스트림 전송 0회를 잡습니다. Pi 가이드와 structure 문서도 변환 한계와 프로바이더 능력 주장을 구분합니다. 다만 베이스는 dev가 아니라 agent/provider-parity-04-modality(#4539)이고, 아래층 #4534#4535#4536→#4539가 열린 수동 체인입니다. 스택 패키지는 2.54.0인데 현재 dev는 2.55.0이라 test 4/4release-version-line 실패가 이미 본문에 적혀 있고, hosted CI에서도 test 4/4·macos 2/2·집계 ci가 실패한 상태입니다. Draft이며 로컬 제품 검증은 의도적으로 돌리지 않았습니다.

라인 45 - src/adapters/input-media-guard.ts / withInputMediaGuard - 어댑터를 제자리에서 감싸므로 반환값 미사용은 동작상 문제 없음. 다만 runTurn/localTerminal이 없는 어댑터는 buildRequest만 가드됨(의도된 최소 경계).
라인 registry createRegisteredAdapter - 가드가 tier-metadata buildRequest 래핑보다 안쪽에 들어가서 미디어 거절이 실제 직렬화보다 먼저 일어남. 순서는 맞음.
경로 package.json 버전 스큐 - 스택이 2.54.0을 들고 dev 2.55.0과 어긋나 release-version-line이 깨짐. 기능 패치와 별개로 부모 스택을 dev에 맞춘 뒤에야 이 층 CI가 깨끗해질 수 있음.
경로 CI test 4/4 / macos 2/2 / ci - draft 헤드에서 실패. 새 가드 테스트는 shards 1·3에서 통과했다는 본문 주장과 맞는지, 실패가 버전 스큐뿐인지 확인 필요.
경로 스택 베이스 - dev와 diverged(ahead 20 / behind 17). 단독 머지 불가. #4534부터 바닥부터 올려야 함.
경로 video - 비디오 변환 한계는 어댑터별로 남겨 둔 채라 이 PR이 닫는 범위는 audio/file(및 file_id-only image) 조용한 성공뿐. 그 잔여는 문서에 남아 있어야 함.

메인테이너의 판단이 필요한 지점

너의 추천
지금은 머지하지 마세요. #4534부터 바닥 층을 dev(2.55.0)에 맞춘 뒤 #4539까지 통과시키고, 이 PR을 그 위에 리베이스한 다음 test 4/4·macos 2/2가 초록일 때 ready로 전환하세요. 범위와 테스트 설계는 유지할 가치가 큽니다. 스택이 무너지면 close-don't-rebase가 아니라 부모부터 다시 맞추는 쪽이 맞습니다.

이 댓글은 grok-bot이 작성했습니다

Restacked onto the squashed #4539 landing; tree identical to pre-restack head fff3aa4.
@lidge-jun
lidge-jun marked this pull request as ready for review September 14, 2026 01:02
@lidge-jun
lidge-jun force-pushed the agent/provider-parity-05-media-guard branch from fff3aa4 to c8e7370 Compare September 14, 2026 01:02
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 01:02
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T01:11:25.770258Z c8e7370 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8e737004b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +43 to +45
} else if (item.type === "message" || item.type === undefined) {
const kind = contentMedia(item.content);
if (kind) return kind;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scan agent_message media before translating it

When a Responses request carries a Codex agent_message whose content contains input_audio, input_file, or a file-ID-only input_image, this branch skips it because its type is neither message nor undefined. The explicit agent_message path in src/responses/parser.ts:215-225 then passes that content through inputContentParts, which reduces populated attachments to presence text, allowing a translated adapter to return success without the attachment. Include agent_message among the content-bearing item types and add a regression covering this established replay shape.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

const build = adapter.buildRequest.bind(adapter);
adapter.buildRequest = (parsed, incoming) => {
const kind = untranslatedResponsesInputMedia(parsed._rawBody);
if (kind) throw new Error(untranslatedInputMediaMessage(kind));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve media rejection status through bridge loops

When the same request activates the web-search or image bridge, those loops call this guarded builder at src/web-search/loop.ts:467 and src/images/loop.ts:575 before the normal core build-error catch. Because the guard throws a plain Error, both loops classify it as 502 Provider unreachable at lines 607 and 702; the runTurn image path similarly discards the emitted 400 status at src/images/loop.ts:758. A known nonretryable client input error is therefore reported as an upstream outage, inviting retries and misleading diagnostics. Use a shared typed client-input failure that the bridges preserve, or perform admission before entering them.

AGENTS.md reference: src/AGENTS.md:L17-L19

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer integration per MAINTAINERS.md: landing this maintainer-authored PR on dev without a second maintainer approval.\n\nExact-head verification on c8e7370 (restacked onto the #4539 squash commit; tree identical to reviewed head fff3aa4): 25 checks pass, 2 skipped, 0 failed/cancelled. Run set: 34794607350 plus metadata workflows.\n\nLocal suite/typecheck/build: NOT RUN (hosted exact-head CI is the evidence).

@lidge-jun
lidge-jun merged commit d08d11f into dev Sep 14, 2026
30 of 31 checks passed
@lidge-jun
lidge-jun deleted the agent/provider-parity-05-media-guard branch September 14, 2026 01:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant