Skip to content

fix(chat): keep the structured-output opt-out exact on the native chat wire - #2042

Merged
lidge-jun merged 1 commit into
lidge-jun:devfrom
ntdatt812:fix/structured-output-optout-exact-on-chat-passthrough
Aug 19, 2026
Merged

fix(chat): keep the structured-output opt-out exact on the native chat wire#2042
lidge-jun merged 1 commit into
lidge-jun:devfrom
ntdatt812:fix/structured-output-optout-exact-on-chat-passthrough

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

noStructuredOutputModels is documented — in reference/configuration/providers.md and its fr/ja/ko/ru/tr/zh-cn translations — as:

Exact model IDs whose openai-chat endpoint rejects response_format. Only an exact requested-model match omits the field; structured-output translation stays enabled for every other openai-chat model.

The Responses ingress enforces that, and tests/openai-chat-hardening.test.ts already pins a :tag sibling keeping the field there.

The native Chat passthrough (buildOpenAIChatPassthroughRequest, added in #1467) matched through modelInList instead, which additionally matches the pre-colon prefix:

export function modelInList(list, modelId) {
  if (list.includes(modelId)) return true;
  const colon = modelId.indexOf(":");
  return colon > 0 && list.includes(modelId.slice(0, colon));   // <- prefix
}

Why it bites

ollama-cloud ships tagged ids verbatim — gpt-oss:120b, qwen3-coder:480b, qwen3.5:397b, gemma4:31b (src/providers/registry.ts). With

{ "noStructuredOutputModels": ["gpt-oss"] }

a request for gpt-oss:120b lost response_format on /v1/chat/completions but kept it on /v1/responses. Same provider, same config key, same upstream — the answer depended on which client API the caller used, and on the chat wire the caller asked for JSON and silently got prose, on a model the operator never opted out.

That is precisely the failure #1424 named when it chose the exact boundary: a wider match "would silently return prose for siblings that support JSON Schema."

Change

One gate, exact-matched, matching the Responses side. The neighbouring gates keep modelInList deliberately — noVisionModels is documented as "matching tolerates an Ollama :size tag", this option is documented the other way — so the comment now records why this one differs.

No documentation change: the docs already describe the behaviour this restores.

Tests

Four cases in the existing openai-chat response_format emission describe, placed directly after the Responses-side assertions so the two ingresses read as a pair:

case expected
exact listed id omits response_format
:tag sibling, only the base listed keeps it
full :tag id listed omits it
unrelated model keeps it

The :tag sibling case fails on current dev and passes with this change.

Verification

Run on the exact head of this branch:

bun run typecheck                                     clean

bun test tests/openai-chat-hardening.test.ts          54 pass / 0 fail
  (same file on unpatched dev: 53 pass / 1 fail — the new :tag sibling case)

bun scripts/test.ts <24 test files touching openai-chat,
  response_format, noStructuredOutputModels, passthrough>
                                                      414 pass / 0 fail

I did not run the full suite: this is a Windows machine and bun run test panics partway through on Bun 1.3.14 (index out of bounds: index 0, len 0), so a result from it would be a truncated log rather than a result. The batches above were run through scripts/test.ts so each file keeps its isolated home.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected structured-output handling for OpenAI Chat passthrough requests.
    • Response formatting is now disabled only for explicitly listed model IDs.
    • Related model variants retain response formatting unless their complete IDs are also excluded.
  • Tests

    • Added coverage for exact model matching, suffixed variants, and unrelated models.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

What box 1 covers here. Ticked at the request of @Ingwannu (review comment), who verified this exact head independently. To keep the record exact: "green on my local testing" means everything listed under Verification above ran green on this exact head — typecheck, the new tests, the same tests against unpatched dev to prove they fail there, and a batch of the surrounding suite. It does not mean I ran the full suite: this is a Windows machine, bun run test panics partway through on Bun 1.3.14 (index out of bounds: index 0, len 0), and the Windows baseline on clean dev is not green either (#1059). The exact-head CI gate is the authority on the full suite, which is what marking this Ready hands it.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed; the review readiness checklist is complete.

Review readiness checklist

  • ✅ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ✅ I resolved all correct Codex and CodeRabbit findings.
  • ✅ My PR is ready for review.

4/4 boxes ticked.

This pull request is already Ready for Review.
The review-ready label marks this PR as ready; review automation runs independently.
Maintainers: @lidge-jun @Ingwannu @Wibias

@github-actions
github-actions Bot marked this pull request as draft August 18, 2026 13:52
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5a3b5af2-ee03-449e-80fe-d3708caae156

📥 Commits

Reviewing files that changed from the base of the PR and between c42d1eb and 37cd730.

📒 Files selected for processing (2)
  • src/adapters/openai-chat.ts
  • tests/openai-chat-hardening.test.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The passthrough builder now removes response_format only for exact IDs in noStructuredOutputModels. Tests verify tagged variants, suffixed IDs, and unrelated models.

Changes

OpenAI passthrough response format

Layer / File(s) Summary
Exact model opt-out behavior
src/adapters/openai-chat.ts, tests/openai-chat-hardening.test.ts
At lines 119–123, the builder uses exact includes(modelId) matching. Tests at lines 795–826 verify exact opt-outs, :tag boundaries, full suffixed-ID opt-outs, and unchanged unrelated models.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 37cd7

The change limits structured-output opt-outs to exact model IDs and adds coverage for tagged siblings; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 exact-match fix for structured-output suppression in native chat requests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Ingwannu

Copy link
Copy Markdown
Owner

Confirmed against the exact current dev base: the native Chat passthrough was incorrectly using the colon-prefix helper even though noStructuredOutputModels is documented and implemented on the Responses ingress as an exact requested-model opt-out. The one-line gate change restores the shared contract without changing the neighboring options that intentionally tolerate tags.

I also verified this exact head locally under the 2-core limit:

  • bun test tests/openai-chat-hardening.test.ts — 54 passed
  • bun run typecheck — passed

I found no implementation blocker in the current diff. Please tick the readiness checklist and mark the PR Ready so the full exact-head CI/review gate runs; assuming it remains green and no new head is pushed, this should be an approval candidate.

@ntdatt812

Copy link
Copy Markdown
Contributor Author

Checklist is at 3/4. Box 1 is open on purpose rather than left over — I would rather ask than tick something I cannot stand behind.

"All CI tests are green on my local testing" — this is a Windows machine. bun run test panics partway through on Bun 1.3.14 (index out of bounds: index 0, len 0), so a "0 fail" from it would be a truncated log, not a result. Running in chunks through scripts/test.ts avoids the panic, but the Windows baseline on clean dev is not green either (#1059), so "all green" is not reachable here by any route.

What I did run on the exact head of this branch is listed under Verification in the description — typecheck clean, the new tests green, and a batch of the surrounding suite green, plus the same tests run against unpatched dev to show they actually fail there.

If box 1 is meant as "the tests you ran locally are green", that is true and I will tick it on a word from you. If it is meant literally as the full suite, it cannot be honestly ticked from Windows and I would rather it stay open than be wrong.

@ntdatt812
ntdatt812 marked this pull request as ready for review August 18, 2026 18:52
@github-actions
github-actions Bot marked this pull request as draft August 18, 2026 18:52
@ntdatt812

Copy link
Copy Markdown
Contributor Author

Thanks for verifying it independently — and for the note on the neighbouring options; that was the part I most wanted a second pair of eyes on, since noVisionModels documents the opposite tolerance one line away in the same registry row.

Checklist is now 4/4 and the PR is Ready. I ticked box 1 on your word, and rewrote the note under it so the record stays exact: what ran green here is typecheck, the new tests, the same tests against unpatched dev to show they fail there, and a batch of the surrounding suite — not the full suite, which this Windows machine cannot produce a trustworthy result for (bun run test panics on Bun 1.3.14, and the Windows baseline on clean dev is not green per #1059). The exact-head gate is the authority on that, which is what marking it Ready hands over.

No new head has been pushed since your verification — still 37cd730.

I opened one more in the same area, #2059: the compatibility behavior report matches those same no*Models lists through a local exact-match helper while all ten runtime gates it describes use modelInList, so for a tagged id the "authoritative" report contradicts the wire the adapter actually builds. It is also Ready, with the fingerprint blast radius measured in the description.

…t wire

`noStructuredOutputModels` is documented, in every locale, as "Exact model IDs
whose `openai-chat` endpoint rejects `response_format`. Only an exact
requested-model match omits the field; structured-output translation stays
enabled for every other `openai-chat` model." The Responses ingress enforces
that, and tests/openai-chat-hardening.test.ts already pins a `:tag` sibling
keeping the field there.

The native Chat passthrough added in lidge-jun#1467 matched through `modelInList`
instead, which also matches the pre-colon prefix. On a provider that serves
Ollama-style tags -- ollama-cloud ships `gpt-oss:120b`, `qwen3-coder:480b`,
`qwen3.5:397b`, `gemma4:31b` -- a `noStructuredOutputModels: ["gpt-oss"]`
entry therefore stripped `response_format` from `gpt-oss:120b` on
/v1/chat/completions while /v1/responses kept it. The caller asked for JSON
and silently got prose, on a model the operator never opted out.

That is the failure lidge-jun#1424 called out when it chose the exact boundary:
a wider match "would silently return prose for siblings that support JSON
Schema". The sibling gates on the lines above keep `modelInList` --
`noVisionModels` is documented as tolerating an Ollama `:size` tag, this one
is not -- so the comment now says why this gate differs.

Four tests, next to the existing Responses-side assertions so the two
ingresses read as a pair: exact id opts out, a `:tag` sibling does not, the
full `:tag` id does when listed, and an unrelated model is untouched. The
`:tag` sibling case fails on current dev.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ntdatt812
ntdatt812 force-pushed the fix/structured-output-optout-exact-on-chat-passthrough branch from 37cd730 to 333a24c Compare August 18, 2026 19:00
@ntdatt812
ntdatt812 marked this pull request as ready for review August 18, 2026 19:01
@github-actions
github-actions Bot marked this pull request as draft August 18, 2026 19:01
@ntdatt812
ntdatt812 marked this pull request as ready for review August 18, 2026 19:03
@lidge-jun
lidge-jun merged commit c472ad0 into lidge-jun:dev Aug 19, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working review-ready

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants