@@ -349,18 +349,41 @@ func (b *ChatBuilder) ResponseJSON() *ChatBuilder {
349349// This enables structured output mode where the model produces JSON conforming to the schema.
350350// The schema parameter defines the structure the output must conform to.
351351//
352+ // This always forces schema.Strict = true. Strict mode requires the schema to
353+ // set "additionalProperties": false and list every property in "required" at
354+ // every object node; validate() rejects non-compliant schemas with
355+ // ErrInvalidSchema before the request is sent. Use ResponseJSONSchemaNonStrict
356+ // to opt out of strict mode for schemas that cannot meet those constraints.
357+ //
352358// Example:
353359//
354360// schema := &core.JSONSchemaDefinition{
355361// Name: "person",
356- // Strict: true,
357- // Schema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}`),
362+ // Schema: json.RawMessage(`{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}`),
358363// }
359364// resp, err := client.Chat(model).
360365// User("Extract: John is 30 years old").
361366// ResponseJSONSchema(schema).
362367// GetResponse(ctx)
363368func (b * ChatBuilder ) ResponseJSONSchema (schema * JSONSchemaDefinition ) * ChatBuilder {
369+ if schema != nil {
370+ schema .Strict = true
371+ }
372+ b .req .ResponseFormat = ResponseFormatJSONSchema
373+ b .req .JSONSchema = schema
374+ return b
375+ }
376+
377+ // ResponseJSONSchemaNonStrict constrains the model output to match a specific
378+ // JSON Schema without enforcing strict mode. This forces schema.Strict = false,
379+ // skipping the strict-schema validation that ResponseJSONSchema applies. Use
380+ // this when a schema cannot satisfy strict mode's requirements (every object
381+ // node needs "additionalProperties": false and a "required" array covering
382+ // all declared properties) and the provider still accepts loose schemas.
383+ func (b * ChatBuilder ) ResponseJSONSchemaNonStrict (schema * JSONSchemaDefinition ) * ChatBuilder {
384+ if schema != nil {
385+ schema .Strict = false
386+ }
364387 b .req .ResponseFormat = ResponseFormatJSONSchema
365388 b .req .JSONSchema = schema
366389 return b
@@ -477,6 +500,51 @@ func (b *ChatBuilder) validate() error {
477500 }
478501 }
479502
503+ // Capability gate: reject schema-based structured output requests
504+ // (ResponseFormatJSONSchema) against a provider/model that does not
505+ // support core.FeatureStructuredOutput. This must run before the
506+ // strict-schema check below so an unsupported provider fails with
507+ // ErrStructuredOutputUnsupported rather than a schema validation error.
508+ //
509+ // Plain JSON mode (ResponseFormatJSON / json_object) is intentionally
510+ // NOT hard-gated here: it has no schema/shape contract to silently
511+ // violate, and many models support json_object without being tagged
512+ // with FeatureStructuredOutput (e.g. OpenAI's gpt-3.5-turbo, gpt-4, and
513+ // gpt-4-turbo). Gating it would reject requests that previously worked.
514+ // If a provider genuinely can't do json_object, its descriptive API
515+ // error surfaces instead.
516+ if b .req .ResponseFormat == ResponseFormatJSONSchema {
517+ if ! b .client .provider .Supports (FeatureStructuredOutput ) {
518+ return fmt .Errorf ("%w: provider %s model %s" ,
519+ ErrStructuredOutputUnsupported , b .client .provider .ID (), b .req .Model )
520+ }
521+
522+ // Model-level gate: the provider supports structured output overall,
523+ // but the specific requested model may not (e.g. OpenAI supports it,
524+ // but gpt-3.5-turbo / gpt-4 do not). If the model is present in the
525+ // provider's catalog and lacks the capability, reject it. If the
526+ // model is NOT present in the catalog (unknown, brand-new, or a
527+ // custom deployment not yet reflected in the static catalog), fall
528+ // back to allowing it since the provider-level check already passed.
529+ for _ , m := range b .client .provider .Models () {
530+ if m .ID == b .req .Model {
531+ if ! m .HasCapability (FeatureStructuredOutput ) {
532+ return fmt .Errorf ("%w: provider %s model %s" ,
533+ ErrStructuredOutputUnsupported , b .client .provider .ID (), b .req .Model )
534+ }
535+ break
536+ }
537+ }
538+ }
539+
540+ // Strict structured output requires a schema shape the provider can
541+ // enforce exactly.
542+ if b .req .ResponseFormat == ResponseFormatJSONSchema && b .req .JSONSchema != nil && b .req .JSONSchema .Strict {
543+ if err := validateStrictSchema (b .req .JSONSchema .Schema ); err != nil {
544+ return err
545+ }
546+ }
547+
480548 return nil
481549}
482550
0 commit comments