feat(llm): compat endpoints declare their JSON mode - #145
Merged
Conversation
NewCompat sent json_schema unconditionally and Capabilities() hardcoded true for both features, so a gateway publishing only json_object 400'd on every structured call after passing the construction-time check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two overlapping defects in the OpenAI adapter's compatible-endpoint path.
1.
json_schemawas sent unconditionally.GenerateStreamsetresponse_format {type:"json_schema"}whenever aStructuredOutputwas on the context. Endpoints that publish only the older{type:"json_object"}reject that outright, so every schema-constrained call 400'd while plain chat and tool calling kept working — the failure lands exactly on planner/extractor/judge stages and nowhere else.2.
Capabilities()claimed features on every gateway's behalf. It was a method on the shared*Providertype returning a hardcoded{ImageInput: true, StructuredOutput: true}, andNewCompatreturns that same type.CapabilityProviderexists so a consumer can reject an unsuitable provider at construction instead of discovering the gap from a confident, wrong answer — for compatible endpoints it produced exactly that: a pre-flight check that passed, then a failure mid-run.Change
JSONModewith three values, declared per endpoint:JSONModeSchema(zero value)response_format {type:"json_schema"}, schema inline, enforced server-sideJSONModeObjectresponse_format {type:"json_object"}+ schema appended as a trailing system messageJSONModeNoneCapabilities()derives from configuration (StructuredOutput: p.jsonMode != JSONModeNone), so one source of truth governs both the claim and the wire — they cannot drift.Newis unchanged and still reports both;NewCompatprependsJSONModeNone+ no image claim, with caller options winning.Two details are load-bearing and commented as such:
Breaking
Callers using structured output through
NewCompatmust addWithJSONMode(...). No in-tree callers outside the package. The alternative — defaulting compat tojson_schema— preserves both the false capability claim and the 400.JSONModeObjectguarantees only that the reply parses as JSON;Strictdegrades from a rule to a request. Documented indocs/providers.md. The adapter deliberately does not validate-and-retry: how many attempts a malformed reply is worth is caller policy.Tests
Wire-level via
captureOpenAIRequest: object mode sendsjson_objectand notjson_schema; the trailing system message carries schema + description + the word JSON; noresponse_formatwithout a schema in any mode;JSONModeNonefails before reaching the httptest server (asserts the server was never called, not merely that an error came back); capability table-test across all three modes plus caller-option precedence.gofmt -l .empty ·make vet·make lint(0 issues) ·make build·make test·make test-race— all clean.