From 6ce08cabba115da40bcf9f8bff7ae44680ee8c6c Mon Sep 17 00:00:00 2001 From: Ryan O'Donnell Date: Thu, 23 Oct 2025 21:20:45 -0700 Subject: [PATCH 1/3] Add custom schema name support for OpenAI provider OpenAI's response_format requires a 'name' for json_schema definitions. This change enables custom schema names which are useful for debugging, logging, and clarity when using multiple schemas. Supports two formats: 1. Full: { name: 'custom_name', schema: { type: 'object', ... } } 2. Schema only: { type: 'object', ... } - defaults to 'response' Changes: - Updated OpenAI provider to extract schema name from wrapper format - Added unit tests for schema name functionality - Maintained backward compatibility (plain schemas default to 'response') Related to schema naming discussion for better debugging and observability. --- lib/ruby_llm/providers/openai/chat.rb | 14 +- spec/ruby_llm/providers/openai/chat_spec.rb | 134 ++++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 spec/ruby_llm/providers/openai/chat_spec.rb diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index 68522811e..250a67a9c 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -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 } } diff --git a/spec/ruby_llm/providers/openai/chat_spec.rb b/spec/ruby_llm/providers/openai/chat_spec.rb new file mode 100644 index 000000000..95d160a9e --- /dev/null +++ b/spec/ruby_llm/providers/openai/chat_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Providers::OpenAI::Chat do + describe '.render_payload' do + let(:model) { instance_double(RubyLLM::Model::Info, id: 'gpt-4o') } + let(:messages) { [RubyLLM::Message.new(role: :user, content: 'Hello')] } + + context 'with schema' do + it 'uses custom schema name when provided in full format' do + schema = { + name: 'department_classification', + schema: { + type: 'object', + properties: { + department_id: { type: 'string' } + } + } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:name]).to eq('department_classification') + expect(payload[:response_format][:json_schema][:schema]).to eq(schema[:schema]) + expect(payload[:response_format][:json_schema][:strict]).to eq(true) + end + + it 'defaults schema name to "response" for plain schema' do + schema = { + type: 'object', + properties: { + name: { type: 'string' } + } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:name]).to eq('response') + expect(payload[:response_format][:json_schema][:schema]).to eq(schema) + expect(payload[:response_format][:json_schema][:strict]).to eq(false) + end + + it 'defaults schema name to "response" when full format has no name' do + schema = { + schema: { + type: 'object', + properties: { + name: { type: 'string' } + } + } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:name]).to eq('response') + expect(payload[:response_format][:json_schema][:schema]).to eq(schema[:schema]) + end + + it 'respects strict: false when provided' do + schema = { + name: 'custom', + schema: { type: 'object', properties: {} }, + strict: false + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:strict]).to eq(false) + end + + it 'defaults strict to true when not specified in full format' do + schema = { + name: 'custom', + schema: { type: 'object', properties: {} } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:strict]).to eq(true) + end + end + + context 'without schema' do + it 'does not include response_format' do + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: nil + ) + + expect(payload).not_to have_key(:response_format) + end + end + end +end From 0bb9172d7ab73816a82e367f07fc1ecc42c64dd7 Mon Sep 17 00:00:00 2001 From: Ryan O'Donnell Date: Fri, 24 Oct 2025 11:33:13 -0700 Subject: [PATCH 2/3] Enable automatic schema names for RubyLLM::Schema classes Previously, Chat#with_schema extracted only the [:schema] portion from RubyLLM::Schema objects, discarding the class name. Now it preserves the full hash, allowing schema class names to be automatically used in OpenAI API. Changes: - Updated Chat#with_schema to keep full to_json_schema result - RubyLLM::Schema classes now automatically use their class name - Added test for RubyLLM::Schema format with class name - Manual wrappers and plain schemas still work as before Example: class PersonSchema < RubyLLM::Schema string :name integer :age end chat.with_schema(PersonSchema) # OpenAI API receives: name: 'PersonSchema' (instead of 'response') --- lib/ruby_llm/chat.rb | 2 +- lib/ruby_llm/providers/gemini/chat.rb | 3 + ...nstances_and_returns_structured_output.yml | 82 +++++++++++ ...nstances_and_returns_structured_output.yml | 117 +++++++++++++++ spec/ruby_llm/chat_schema_spec.rb | 32 +++++ spec/ruby_llm/providers/gemini/chat_spec.rb | 21 +++ spec/ruby_llm/providers/open_ai/chat_spec.rb | 107 ++++++++++++++ spec/ruby_llm/providers/openai/chat_spec.rb | 134 ------------------ spec/spec_helper.rb | 1 + 9 files changed, 364 insertions(+), 135 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/chat_with_schema_with_gemini_gemini-2_5-flash_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_schema_with_openai_gpt-4_1-nano_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml delete mode 100644 spec/ruby_llm/providers/openai/chat_spec.rb diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index da123d020..fdb0415d5 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -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 diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index 3233ce939..284c4b1d9 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -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| diff --git a/spec/fixtures/vcr_cassettes/chat_with_schema_with_gemini_gemini-2_5-flash_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml b/spec/fixtures/vcr_cassettes/chat_with_schema_with_gemini_gemini-2_5-flash_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml new file mode 100644 index 000000000..8b3f1e922 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_schema_with_gemini_gemini-2_5-flash_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml @@ -0,0 +1,82 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"Generate a person named + Carol who is 32 years old"}]}],"generationConfig":{"responseMimeType":"application/json","responseSchema":{"type":"OBJECT","properties":{"name":{"type":"STRING"},"age":{"type":"NUMBER"}},"required":["name","age"]}}}' + headers: + User-Agent: + - Faraday v2.14.0 + X-Goog-Api-Key: + - "" + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Fri, 24 Oct 2025 23:50:20 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=996 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "{\n \"name\": \"Carol\",\n \"age\": 32\n}" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 13, + "candidatesTokenCount": 19, + "totalTokenCount": 96, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 13 + } + ], + "thoughtsTokenCount": 64 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "vBD8aNaDFNTQz7IP-tu8OQ" + } + recorded_at: Fri, 24 Oct 2025 23:50:20 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_schema_with_openai_gpt-4_1-nano_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml b/spec/fixtures/vcr_cassettes/chat_with_schema_with_openai_gpt-4_1-nano_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml new file mode 100644 index 000000000..23a89bf0a --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_schema_with_openai_gpt-4_1-nano_accepts_rubyllm_schema_class_instances_and_returns_structured_output.yml @@ -0,0 +1,117 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"Generate + a person named Alice who is 28 years old"}],"stream":false,"response_format":{"type":"json_schema","json_schema":{"name":"PersonSchemaClass","schema":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"number"}},"required":["name","age"],"additionalProperties":false,"strict":true,"$defs":{}},"strict":true}}}' + headers: + User-Agent: + - Faraday v2.14.0 + Authorization: + - Bearer + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 24 Oct 2025 23:50:19 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '708' + Openai-Project: + - proj_FwwKCZJVE4Cq8VD0ojnE99VR + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '722' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999985' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - "" + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CULpyuW7owwTWNZBhmQJCUg03cbwj", + "object": "chat.completion", + "created": 1761349818, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "{\"name\":\"Alice\",\"age\":28}", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 66, + "completion_tokens": 9, + "total_tokens": 75, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_950f36939b" + } + recorded_at: Fri, 24 Oct 2025 23:50:19 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_schema_spec.rb b/spec/ruby_llm/chat_schema_spec.rb index 2ba4d62c4..5404808cd 100644 --- a/spec/ruby_llm/chat_schema_spec.rb +++ b/spec/ruby_llm/chat_schema_spec.rb @@ -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' @@ -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 @@ -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 diff --git a/spec/ruby_llm/providers/gemini/chat_spec.rb b/spec/ruby_llm/providers/gemini/chat_spec.rb index 719f1bae5..5cdbd91cb 100644 --- a/spec/ruby_llm/providers/gemini/chat_spec.rb +++ b/spec/ruby_llm/providers/gemini/chat_spec.rb @@ -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) diff --git a/spec/ruby_llm/providers/open_ai/chat_spec.rb b/spec/ruby_llm/providers/open_ai/chat_spec.rb index cbf4b0293..a911be24c 100644 --- a/spec/ruby_llm/providers/open_ai/chat_spec.rb +++ b/spec/ruby_llm/providers/open_ai/chat_spec.rb @@ -33,4 +33,111 @@ expect(message.cache_creation_tokens).to eq(0) end end + + describe '.render_payload' do + let(:model) { instance_double(RubyLLM::Model::Info, id: 'gpt-4o') } + let(:messages) { [RubyLLM::Message.new(role: :user, content: 'Hello')] } + + before do + allow(described_class).to receive(:format_messages).and_return([{ role: 'user', content: 'Hello' }]) + end + + context 'with schema' do + it 'defaults schema name to "response" for plain schema' do + schema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' } + } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:name]).to eq('response') + expect(payload[:response_format][:json_schema][:schema]).to eq(schema) + expect(payload[:response_format][:json_schema][:strict]).to be(true) + end + + it 'uses custom schema name when provided in full format' do + schema = { + name: 'PersonSchema', + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' } + } + } + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema + ) + + expect(payload[:response_format][:json_schema][:name]).to eq('PersonSchema') + expect(payload[:response_format][:json_schema][:schema]).to eq(schema[:schema]) + expect(payload[:response_format][:json_schema][:strict]).to be(true) + end + + it 'respects explicit strict: false for both formats' do + # Full format with strict: false + schema_full = { + name: 'PersonSchema', + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' } + } + }, + strict: false + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema_full + ) + + expect(payload[:response_format][:json_schema][:strict]).to be(false) + + # Plain format with strict: false + schema_plain = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'integer' } + }, + strict: false + } + + payload = described_class.render_payload( + messages, + tools: {}, + temperature: nil, + model: model, + stream: false, + schema: schema_plain + ) + + expect(payload[:response_format][:json_schema][:strict]).to be(false) + end + end + end end diff --git a/spec/ruby_llm/providers/openai/chat_spec.rb b/spec/ruby_llm/providers/openai/chat_spec.rb deleted file mode 100644 index 95d160a9e..000000000 --- a/spec/ruby_llm/providers/openai/chat_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe RubyLLM::Providers::OpenAI::Chat do - describe '.render_payload' do - let(:model) { instance_double(RubyLLM::Model::Info, id: 'gpt-4o') } - let(:messages) { [RubyLLM::Message.new(role: :user, content: 'Hello')] } - - context 'with schema' do - it 'uses custom schema name when provided in full format' do - schema = { - name: 'department_classification', - schema: { - type: 'object', - properties: { - department_id: { type: 'string' } - } - } - } - - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: schema - ) - - expect(payload[:response_format][:json_schema][:name]).to eq('department_classification') - expect(payload[:response_format][:json_schema][:schema]).to eq(schema[:schema]) - expect(payload[:response_format][:json_schema][:strict]).to eq(true) - end - - it 'defaults schema name to "response" for plain schema' do - schema = { - type: 'object', - properties: { - name: { type: 'string' } - } - } - - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: schema - ) - - expect(payload[:response_format][:json_schema][:name]).to eq('response') - expect(payload[:response_format][:json_schema][:schema]).to eq(schema) - expect(payload[:response_format][:json_schema][:strict]).to eq(false) - end - - it 'defaults schema name to "response" when full format has no name' do - schema = { - schema: { - type: 'object', - properties: { - name: { type: 'string' } - } - } - } - - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: schema - ) - - expect(payload[:response_format][:json_schema][:name]).to eq('response') - expect(payload[:response_format][:json_schema][:schema]).to eq(schema[:schema]) - end - - it 'respects strict: false when provided' do - schema = { - name: 'custom', - schema: { type: 'object', properties: {} }, - strict: false - } - - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: schema - ) - - expect(payload[:response_format][:json_schema][:strict]).to eq(false) - end - - it 'defaults strict to true when not specified in full format' do - schema = { - name: 'custom', - schema: { type: 'object', properties: {} } - } - - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: schema - ) - - expect(payload[:response_format][:json_schema][:strict]).to eq(true) - end - end - - context 'without schema' do - it 'does not include response_format' do - payload = described_class.render_payload( - messages, - tools: {}, - temperature: nil, - model: model, - stream: false, - schema: nil - ) - - expect(payload).not_to have_key(:response_format) - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0b60aa315..33fcf0271 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ require 'bundler/setup' require 'fileutils' require 'ruby_llm' +require 'ruby_llm/schema' require 'webmock/rspec' require 'active_support' require 'active_support/core_ext' From 673bfa5d4c8934e0ba31c8a7bb0db34865b63b52 Mon Sep 17 00:00:00 2001 From: Ryan O'Donnell Date: Fri, 24 Oct 2025 17:24:28 -0700 Subject: [PATCH 3/3] Add documentation for custom schema names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the new custom schema name feature in the structured output section, explaining how to use the full format with custom names and when they're useful (influencing model behavior and debugging). Also adds a note in the RubyLLM::Schema section explaining that schema classes automatically use their class name in API requests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/_core_features/chat.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/_core_features/chat.md b/docs/_core_features/chat.md index 456d24adb..90803bfdb 100644 --- a/docs/_core_features/chat.md +++ b/docs/_core_features/chat.md @@ -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: @@ -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: