Skip to content

Fix malformed json_schema envelope for OpenAI-compatible providers#251

Open
alewolf wants to merge 3 commits into
WordPress:trunkfrom
alewolf:claude/thirsty-dewdney-1b6f95
Open

Fix malformed json_schema envelope for OpenAI-compatible providers#251
alewolf wants to merge 3 commits into
WordPress:trunkfrom
alewolf:claude/thirsty-dewdney-1b6f95

Conversation

@alewolf

@alewolf alewolf commented Jun 4, 2026

Copy link
Copy Markdown

Summary

Fixes #229.

AbstractOpenAiCompatibleTextGenerationModel::prepareResponseFormatParam() placed the user-supplied schema directly under the json_schema key and never included the required name field. Per the OpenAI Chat Completions API spec, json_schema must contain a name and carry the schema under a schema key. The malformed payload caused HTTP 400 errors on compliant endpoints (OpenRouter, Together, Fireworks, Groq, Azure OpenAI relays — third-party providers extending this base class) whenever asJsonResponse($schema) was used.

Changes

  • Wrap the output schema in the required envelope: ['type' => 'json_schema', 'json_schema' => ['name' => 'response_schema', 'schema' => $outputSchema]].
  • Use a fixed, spec-valid name (response_schema), matching the dedicated OpenAiTextGenerationModel. The OpenAI API constrains name to ^[a-zA-Z0-9_-]{1,64}$, so deriving it from a user-supplied schema title (which may contain spaces or exceed 64 chars) would risk reintroducing the exact 400 this fixes.
  • Pass through payloads that are already wrapped (arrays containing both name and schema keys) unchanged, so callers supplying a full envelope (e.g. with strict) keep working.
  • null schema still yields ['type' => 'json_object'] (unchanged).

Notes / decisions

  • strict is intentionally not set by default. The API only requires name; strict is optional. This base targets many OpenAI-compatible providers whose strict support varies, and the SDK does no schema sanitization to guarantee strict-compliance (additionalProperties:false, all keys required), so forcing strict: true here could trigger 400s on otherwise-valid schemas. Callers who want it can pass a pre-wrapped envelope, which is passed through.
  • No provider in this repo extends the abstract class (the dedicated OpenAI/Anthropic providers roll their own), so there's no in-repo integration path for this code — only unit coverage. Worth keeping in mind for future test coverage.

Tests

  • Updated the assertions that expected the old (malformed) structure.
  • Added coverage for: a fixed name being used even when the schema has an invalid title, and pass-through of an already-wrapped envelope.
  • composer test:unit (1090 tests), composer phpcs (PSR-12) and composer phpstan all pass.

🤖 Generated with Claude Code

prepareResponseFormatParam() placed the user schema directly under the
json_schema key and never included the required name field, causing HTTP
400 errors on compliant OpenAI Chat Completions endpoints (OpenRouter,
Together, Fireworks, Groq, Azure OpenAI relays) when using asJsonResponse().

Wrap the schema in the required envelope (name + schema), deriving name
from the schema title when present and falling back to "response". Already
wrapped payloads (arrays containing both name and schema keys) are passed
through unchanged.

Fixes WordPress#229.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 4, 2026 11:56
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: alewolf <alekv@git.wordpress.org>
Co-authored-by: benridane <presents111@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.16%. Comparing base (99a65e8) to head (b3c6845).

Additional details and impacted files
@@             Coverage Diff              @@
##              trunk     #251      +/-   ##
============================================
+ Coverage     88.12%   88.16%   +0.03%     
- Complexity     1213     1216       +3     
============================================
  Files            60       60              
  Lines          3934     3945      +11     
============================================
+ Hits           3467     3478      +11     
  Misses          467      467              
Flag Coverage Δ
unit 88.16% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates the OpenAI-compatible response_format handling so JSON output schemas are sent in the OpenAI Chat Completions “json_schema envelope” shape ({ name, schema }), and expands unit tests to cover the new wrapping behavior and pass-through cases.

Changes:

  • Wraps provided output JSON Schema into the OpenAI json_schema envelope with a default or title-derived name.
  • Adds pass-through logic for callers that already provide a wrapped { name, schema } envelope.
  • Updates and extends unit tests to reflect the new response format structure and edge cases.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php Updates existing assertions and adds tests for title-based naming and envelope pass-through.
src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php Implements envelope wrapping / pass-through for response_format when an output schema is provided.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +520 to +524
if (!is_array($outputSchema)) {
return [
'type' => 'json_object',
];
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in b3c6845 — switched to a strict $outputSchema === null check. Since the parameter is ?array, this states the intent more clearly.

}

// Pass through payloads that are already wrapped in the json_schema envelope.
if (isset($outputSchema['name']) && isset($outputSchema['schema']) && is_array($outputSchema['schema'])) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in b3c6845 — extracted the check into a named $isWrappedEnvelope variable split across lines for readability.

Comment on lines +535 to +537
'name' => isset($outputSchema['title']) && is_string($outputSchema['title'])
? $outputSchema['title']
: 'response',

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, and already addressed: the title-derived name was removed in 96b5dda. The OpenAI API constrains json_schema.name to ^[a-zA-Z0-9_-]{1,64}$, so a user-supplied title could be invalid (spaces, punctuation, or >64 chars) and trigger a fresh 400 — exactly the failure this PR fixes. The envelope now uses a fixed, always-valid name (response_schema), matching the dedicated OpenAiTextGenerationModel.

alewolf and others added 2 commits June 4, 2026 14:34
The OpenAI API requires the json_schema name to match ^[a-zA-Z0-9_-]{1,64}$.
Deriving it from the user-supplied schema title risked producing an invalid
name (e.g. titles with spaces or over 64 characters), which would itself cause
the HTTP 400 the fix is meant to prevent. Use a fixed identifier instead,
matching the dedicated OpenAiTextGenerationModel ("response_schema").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use a strict null check (the parameter is ?array) and extract the
json_schema envelope detection into a named variable for readability.
No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

OpenAI-compatible providers fail with 400 on structured output (missing json_schema envelope)

2 participants