Class: DSPy::LM::Adapters::OpenAI::SchemaConverter
- Inherits:
-
Object
- Object
- DSPy::LM::Adapters::OpenAI::SchemaConverter
- Extended by:
- T::Sig
- Defined in:
- lib/dspy/lm/adapters/openai/schema_converter.rb
Overview
Converts DSPy signatures to OpenAI structured output format
Constant Summary collapse
- STRUCTURED_OUTPUT_MODELS =
Models that support structured outputs as of July 2025
T.let([ "gpt-4o-mini", "gpt-4o-2024-08-06", "gpt-4o", "gpt-4-turbo", "gpt-4-turbo-2024-04-09" ].freeze, T::Array[String])
Class Method Summary collapse
- .all_have_discriminators?(schemas) ⇒ Boolean
- .convert_oneof_to_anyof_if_safe(schema) ⇒ Object
- .supports_structured_outputs?(model) ⇒ Boolean
- .to_openai_format(signature_class, name: nil, strict: true) ⇒ Object
- .validate_compatibility(schema) ⇒ Object
Class Method Details
.all_have_discriminators?(schemas) ⇒ Boolean
111 112 113 114 115 116 117 118 119 |
# File 'lib/dspy/lm/adapters/openai/schema_converter.rb', line 111 def self.all_have_discriminators?(schemas) schemas.all? do |schema| next false unless schema.is_a?(Hash) next false unless schema[:properties].is_a?(Hash) # Check if any property has a const value (our discriminator pattern) schema[:properties].any? { |_, prop| prop.is_a?(Hash) && prop[:const] } end end |
.convert_oneof_to_anyof_if_safe(schema) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/dspy/lm/adapters/openai/schema_converter.rb', line 63 def self.convert_oneof_to_anyof_if_safe(schema) return schema unless schema.is_a?(Hash) result = schema.dup # Check if this schema has oneOf that we can safely convert if result[:oneOf] if all_have_discriminators?(result[:oneOf]) # Safe to convert - discriminators ensure mutual exclusivity result[:anyOf] = result.delete(:oneOf).map { |s| convert_oneof_to_anyof_if_safe(s) } else # Unsafe conversion - raise error raise DSPy::UnsupportedSchemaError.new( "OpenAI structured outputs do not support oneOf schemas without discriminator fields. " \ "The schema contains union types that cannot be safely converted to anyOf. " \ "Please use enhanced_prompting strategy instead or add discriminator fields to union types." ) end end # Recursively process nested schemas if result[:properties].is_a?(Hash) result[:properties] = result[:properties].transform_values { |v| convert_oneof_to_anyof_if_safe(v) } end if result[:items].is_a?(Hash) result[:items] = convert_oneof_to_anyof_if_safe(result[:items]) end # Process arrays of schema items if result[:items].is_a?(Array) result[:items] = result[:items].map { |item| item.is_a?(Hash) ? convert_oneof_to_anyof_if_safe(item) : item } end # Process anyOf arrays (in case there are nested oneOf within anyOf) if result[:anyOf].is_a?(Array) result[:anyOf] = result[:anyOf].map { |item| item.is_a?(Hash) ? convert_oneof_to_anyof_if_safe(item) : item } end result end |
.supports_structured_outputs?(model) ⇒ Boolean
122 123 124 125 126 127 128 |
# File 'lib/dspy/lm/adapters/openai/schema_converter.rb', line 122 def self.supports_structured_outputs?(model) # Extract base model name without provider prefix base_model = model.sub(/^openai\//, "") # Check if it's a supported model or a newer version STRUCTURED_OUTPUT_MODELS.any? { |supported| base_model.start_with?(supported) } end |
.to_openai_format(signature_class, name: nil, strict: true) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/dspy/lm/adapters/openai/schema_converter.rb', line 23 def self.to_openai_format(signature_class, name: nil, strict: true) # Get the output JSON schema from the signature class output_schema = signature_class.output_json_schema # Convert oneOf to anyOf where safe, or raise error for unsupported cases output_schema = convert_oneof_to_anyof_if_safe(output_schema) # Build the complete schema with OpenAI-specific modifications dspy_schema = { "$schema": "http://json-schema.org/draft-06/schema#", type: "object", properties: output_schema[:properties] || {}, required: openai_required_fields(signature_class, output_schema) } # Generate a schema name if not provided schema_name = name || generate_schema_name(signature_class) # Remove the $schema field as OpenAI doesn't use it openai_schema = dspy_schema.except(:$schema) # Add additionalProperties: false for strict mode and fix nested struct schemas if strict openai_schema = add_additional_properties_recursively(openai_schema) openai_schema = fix_nested_struct_required_fields(openai_schema) end # Wrap in OpenAI's required format { type: "json_schema", json_schema: { name: schema_name, strict: strict, schema: openai_schema } } end |
.validate_compatibility(schema) ⇒ Object
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/dspy/lm/adapters/openai/schema_converter.rb', line 131 def self.validate_compatibility(schema) issues = [] # Check for deeply nested objects (OpenAI has depth limits) depth = calculate_depth(schema) if depth > 5 issues << "Schema depth (#{depth}) exceeds recommended limit of 5 levels" end # Check for unsupported JSON Schema features if contains_pattern_properties?(schema) issues << "Pattern properties are not supported in OpenAI structured outputs" end if contains_conditional_schemas?(schema) issues << "Conditional schemas (if/then/else) are not supported" end issues end |