Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/_core_features/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,9 @@ puts response.content # => {"name" => "Alice", "age" => 30}
puts response.content.class # => Hash
```

> RubyLLM::Schema classes automatically use their class name (e.g., `PersonSchema`) as the schema name in API requests, which can help the model better understand the expected output structure.
{: .note }

### Using Manual JSON Schemas

If you prefer not to use RubyLLM::Schema, you can provide a JSON Schema directly:
Expand Down Expand Up @@ -496,6 +499,33 @@ puts response.content
> **OpenAI Requirement:** When using manual JSON schemas with OpenAI, you must include `additionalProperties: false` in your schema objects. RubyLLM::Schema handles this automatically.
{: .warning }

#### Custom Schema Names

By default, schemas are named 'response' in API requests. You can provide a custom name that can influence model behavior and aid debugging:

```ruby
# Provide a custom name with the full format
person_schema = {
name: 'PersonSchema',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
},
required: ['name', 'age'],
additionalProperties: false
}
}

chat = RubyLLM.chat
response = chat.with_schema(person_schema).ask("Generate a person")
```

Custom schema names are useful for:
- **Influencing model behavior** - Descriptive names can help the model better understand the expected output structure
- **Debugging and logging** - Identifying which schema was used in API requests

### Complex Nested Schemas

Structured output supports complex nested objects and arrays:
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def with_schema(schema)

# Accept both RubyLLM::Schema instances and plain JSON schemas
@schema = if schema_instance.respond_to?(:to_json_schema)
schema_instance.to_json_schema[:schema]
schema_instance.to_json_schema
else
schema_instance
end
Expand Down
3 changes: 3 additions & 0 deletions lib/ruby_llm/providers/gemini/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ def parse_completion_response(response)
def convert_schema_to_gemini(schema)
return nil unless schema

# Extract inner schema if wrapper format (e.g., from RubyLLM::Schema.to_json_schema)
schema = schema[:schema] || schema

schema = normalize_any_of(schema) if schema[:anyOf]

build_base_schema(schema).tap do |result|
Expand Down
14 changes: 11 additions & 3 deletions lib/ruby_llm/providers/openai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,21 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema
payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any?

if schema
strict = schema[:strict] != false
# OpenAI's response_format requires a 'name' for the json_schema.
# Custom names are useful for debugging, logging, and clarity when using multiple schemas.
#
# Supports two formats:
# 1. Full: { name: 'custom_schema_name', schema: { type: 'object', properties: {...} } }
# 2. Schema only: { type: 'object', properties: {...} } - name defaults to 'response'
schema_name = schema[:name] || 'response'
schema_def = schema[:schema] || schema
strict = schema.fetch(:strict, true)

payload[:response_format] = {
type: 'json_schema',
json_schema: {
name: 'response',
schema: schema,
name: schema_name,
schema: schema_def,
strict: strict
}
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions spec/ruby_llm/chat_schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

require 'spec_helper'

# Define a test schema class for testing RubyLLM::Schema instances
class PersonSchemaClass < RubyLLM::Schema
string :name
number :age
end

RSpec.describe RubyLLM::Chat do
include_context 'with configured RubyLLM'

Expand Down Expand Up @@ -63,6 +69,19 @@
expect(response2.content).to be_a(String)
expect(response2.content).to include('Ruby')
end

it 'accepts RubyLLM::Schema class instances and returns structured output' do
skip 'Model does not support structured output' unless chat.model.structured_output?

response = chat
.with_schema(PersonSchemaClass)
.ask('Generate a person named Alice who is 28 years old')

# Content should already be parsed as a Hash when schema is used
expect(response.content).to be_a(Hash)
expect(response.content['name']).to eq('Alice')
expect(response.content['age']).to eq(28)
end
end
end

Expand All @@ -86,6 +105,19 @@
expect(response.content['name']).to eq('Jane')
expect(response.content['age']).to eq(25)
end

it 'accepts RubyLLM::Schema class instances and returns structured output' do
skip 'Model does not support structured output' unless chat.model.structured_output?

response = chat
.with_schema(PersonSchemaClass)
.ask('Generate a person named Carol who is 32 years old')

# Content should already be parsed as a Hash when schema is used
expect(response.content).to be_a(Hash)
expect(response.content['name']).to eq('Carol')
expect(response.content['age']).to eq(32)
end
end
end

Expand Down
21 changes: 21 additions & 0 deletions spec/ruby_llm/providers/gemini/chat_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@
end

describe '#convert_schema_to_gemini' do
it 'extracts inner schema from wrapper format' do
# Simulate what RubyLLM::Schema.to_json_schema returns
schema = {
name: 'PersonSchema',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
}
}
}

result = test_obj.send(:convert_schema_to_gemini, schema)

# Should extract the inner schema and convert it
expect(result[:type]).to eq('OBJECT')
expect(result[:properties][:name][:type]).to eq('STRING')
expect(result[:properties][:age][:type]).to eq('INTEGER')
end

it 'converts simple string schema' do
schema = { type: 'string' }
result = test_obj.send(:convert_schema_to_gemini, schema)
Expand Down
Loading