Fix malformed json_schema envelope for OpenAI-compatible providers#251
Fix malformed json_schema envelope for OpenAI-compatible providers#251alewolf wants to merge 3 commits into
Conversation
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>
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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_schemaenvelope with a default or title-derivedname. - 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.
| if (!is_array($outputSchema)) { | ||
| return [ | ||
| 'type' => 'json_object', | ||
| ]; | ||
| } |
There was a problem hiding this comment.
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'])) { |
There was a problem hiding this comment.
Done in b3c6845 — extracted the check into a named $isWrappedEnvelope variable split across lines for readability.
| 'name' => isset($outputSchema['title']) && is_string($outputSchema['title']) | ||
| ? $outputSchema['title'] | ||
| : 'response', |
There was a problem hiding this comment.
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.
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>
Summary
Fixes #229.
AbstractOpenAiCompatibleTextGenerationModel::prepareResponseFormatParam()placed the user-supplied schema directly under thejson_schemakey and never included the requirednamefield. Per the OpenAI Chat Completions API spec,json_schemamust contain anameand carry the schema under aschemakey. The malformed payload caused HTTP 400 errors on compliant endpoints (OpenRouter, Together, Fireworks, Groq, Azure OpenAI relays — third-party providers extending this base class) wheneverasJsonResponse($schema)was used.Changes
['type' => 'json_schema', 'json_schema' => ['name' => 'response_schema', 'schema' => $outputSchema]].name(response_schema), matching the dedicatedOpenAiTextGenerationModel. The OpenAI API constrainsnameto^[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.nameandschemakeys) unchanged, so callers supplying a full envelope (e.g. withstrict) keep working.nullschema still yields['type' => 'json_object'](unchanged).Notes / decisions
strictis intentionally not set by default. The API only requiresname;strictis optional. This base targets many OpenAI-compatible providers whosestrictsupport varies, and the SDK does no schema sanitization to guarantee strict-compliance (additionalProperties:false, all keysrequired), so forcingstrict: truehere could trigger 400s on otherwise-valid schemas. Callers who want it can pass a pre-wrapped envelope, which is passed through.Tests
title, and pass-through of an already-wrapped envelope.composer test:unit(1090 tests),composer phpcs(PSR-12) andcomposer phpstanall pass.🤖 Generated with Claude Code