diff --git a/Appraisals b/Appraisals index d74f4180..197d4b27 100644 --- a/Appraisals +++ b/Appraisals @@ -25,7 +25,8 @@ OPTIONAL_GEMS = { "ruby_llm" => { "1.8" => {constraint: "~> 1.8.0", deps: {}}, "1.9" => {constraint: "~> 1.9.0", deps: {}}, - "latest" => {constraint: ">= 1.9", deps: {}} + "2.0" => {constraint: "~> 2.0.0", deps: {}}, + "latest" => {constraint: ">= 2.0", deps: {}} } } @@ -68,7 +69,7 @@ end appraise "contrib" do gem "openai", ">= 0.34" gem "anthropic", ">= 1.11" - gem "ruby_llm", ">= 1.9" + gem "ruby_llm", ">= 2.0" gem "base64" # needed for openai gem on Ruby 3.4+ gem "cgi" # needed for openai/anthropic gems on Ruby 4.0+ end @@ -77,7 +78,7 @@ end appraise "server" do gem "openai", ">= 0.34" gem "anthropic", ">= 1.11" - gem "ruby_llm", ">= 1.9" + gem "ruby_llm", ">= 2.0" gem "base64" # needed for openai gem on Ruby 3.4+ gem "cgi" # needed for openai/anthropic gems on Ruby 4.0+ gem "rack", "~> 3.0" diff --git a/gemfiles/contrib.gemfile b/gemfiles/contrib.gemfile index d7d9a50f..e5aceca1 100644 --- a/gemfiles/contrib.gemfile +++ b/gemfiles/contrib.gemfile @@ -16,7 +16,7 @@ gem "webmock", "~> 3.0" gem "yard", "~> 0.9" gem "openai", ">= 0.34" gem "anthropic", ">= 1.11" -gem "ruby_llm", ">= 1.9" +gem "ruby_llm", ">= 2.0" gem "base64" gem "cgi" diff --git a/gemfiles/ruby_llm.gemfile b/gemfiles/ruby_llm.gemfile index f4431206..c3530227 100644 --- a/gemfiles/ruby_llm.gemfile +++ b/gemfiles/ruby_llm.gemfile @@ -14,6 +14,6 @@ gem "standard", "~> 1.0" gem "vcr", "~> 6.0" gem "webmock", "~> 3.0" gem "yard", "~> 0.9" -gem "ruby_llm", ">= 1.9" +gem "ruby_llm", ">= 2.0" gemspec path: "../" diff --git a/gemfiles/ruby_llm_2_0.gemfile b/gemfiles/ruby_llm_2_0.gemfile new file mode 100644 index 00000000..14af9682 --- /dev/null +++ b/gemfiles/ruby_llm_2_0.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "appraisal", "~> 2.5" +gem "climate_control", "~> 1.2" +gem "kramdown", "~> 2.0" +gem "minitest-reporters", "~> 1.6" +gem "minitest-stub-const", "~> 0.6" +gem "minitest", "~> 5.0" +gem "rake", "~> 13.0" +gem "simplecov", "~> 0.22" +gem "standard", "~> 1.0" +gem "vcr", "~> 6.0" +gem "webmock", "~> 3.0" +gem "yard", "~> 0.9" +gem "ruby_llm", "~> 2.0.0" + +gemspec path: "../" diff --git a/gemfiles/server.gemfile b/gemfiles/server.gemfile index c0390122..c6a2afda 100644 --- a/gemfiles/server.gemfile +++ b/gemfiles/server.gemfile @@ -16,7 +16,7 @@ gem "webmock", "~> 3.0" gem "yard", "~> 0.9" gem "openai", ">= 0.34" gem "anthropic", ">= 1.11" -gem "ruby_llm", ">= 1.9" +gem "ruby_llm", ">= 2.0" gem "base64" gem "cgi" gem "rack", "~> 3.0" diff --git a/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb b/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb index 00a68f22..bdb2d3e2 100644 --- a/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb +++ b/lib/braintrust/contrib/ruby_llm/instrumentation/chat.rb @@ -186,17 +186,17 @@ def format_tool_schema(tool, provider) ::RubyLLM::Providers::OpenAI::Tools.tool_for(tool) elsif defined?(::RubyLLM::Providers::Anthropic) && provider.is_a?(::RubyLLM::Providers::Anthropic) ::RubyLLM::Providers::Anthropic::Tools.tool_for(tool) - elsif tool.respond_to?(:params_schema) && tool.params_schema + elsif tool_params_schema(tool) build_basic_tool_schema(tool) else build_minimal_tool_schema(tool) end rescue NameError, ArgumentError => e Braintrust::Log.debug("Failed to extract tool schema using provider-specific method: #{e.class.name}: #{e.message}") - tool_schema = (tool.respond_to?(:params_schema) && tool.params_schema) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool) + tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool) end else - tool_schema = (tool.respond_to?(:params_schema) && tool.params_schema) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool) + tool_schema = tool_params_schema(tool) ? build_basic_tool_schema(tool) : build_minimal_tool_schema(tool) end # Strip RubyLLM-specific fields to match native OpenAI format @@ -217,6 +217,19 @@ def format_tool_schema(tool, provider) tool_schema end + # A tool's JSON Schema, across ruby_llm versions. + # 1.x exposes it as params_schema; 2.0 renamed it to parameters_schema. + # @param tool [Object] the RubyLLM tool + # @return [Hash, nil] the schema, or nil when the tool declares none + def tool_params_schema(tool) + %i[params_schema parameters_schema].each do |name| + next unless tool.respond_to?(name) + schema = tool.public_send(name) + return schema if schema + end + nil + end + # Build a basic tool schema with parameters def build_basic_tool_schema(tool) { @@ -224,7 +237,7 @@ def build_basic_tool_schema(tool) "function" => { "name" => tool.name.to_s, "description" => tool.description, - "parameters" => tool.params_schema + "parameters" => tool_params_schema(tool) } } end @@ -255,10 +268,11 @@ def format_message_for_input(msg) # Handle content if msg.respond_to?(:content) && msg.content raw_content = msg.content + attachments = extract_attachments(msg) - # Check if content is a Content object with attachments (issue #71) - formatted["content"] = if raw_content.respond_to?(:text) && raw_content.respond_to?(:attachments) && raw_content.attachments&.any? - format_multipart_content(raw_content) + # Include attachments alongside the text when present (issue #71) + formatted["content"] = if attachments.any? + format_multipart_content(content_text(raw_content), attachments) else format_simple_content(raw_content, msg.role.to_s) end @@ -278,17 +292,43 @@ def format_message_for_input(msg) formatted end + # Collect a message's attachments. + # + # ruby_llm 1.x wraps text and attachments in a Content object hanging off + # the message; 2.0 removed Content, leaving content a plain String and + # exposing attachments on the message itself. Support both. + # + # @param msg [Object] the RubyLLM message + # @return [Array] the message's attachments, empty when there are none + def extract_attachments(msg) + content = msg.content if msg.respond_to?(:content) + if content.respond_to?(:attachments) + from_content = Array(content.attachments) + return from_content if from_content.any? + end + + msg.respond_to?(:attachments) ? Array(msg.attachments) : [] + end + + # Extract the plain text of a message's content. + # @param raw_content [Object] String, or a 1.x Content object + # @return [Object] the text + def content_text(raw_content) + raw_content.respond_to?(:text) ? raw_content.text : raw_content + end + # Format multipart content with text and attachments - # @param content_obj [Object] Content object with text and attachments + # @param text [Object] the message text + # @param attachments [Array] the message's attachments # @return [Array] array of content parts - def format_multipart_content(content_obj) + def format_multipart_content(text, attachments) content_parts = [] # Add text part - content_parts << {"type" => "text", "text" => content_obj.text} if content_obj.text + content_parts << {"type" => "text", "text" => text} if text # Add attachment parts (convert to Braintrust format) - content_obj.attachments.each do |attachment| + attachments.each do |attachment| content_parts << format_attachment_for_input(attachment) end diff --git a/test/braintrust/contrib/ruby_llm/deprecated_test.rb b/test/braintrust/contrib/ruby_llm/deprecated_test.rb index 0abf3afe..643e73b1 100644 --- a/test/braintrust/contrib/ruby_llm/deprecated_test.rb +++ b/test/braintrust/contrib/ruby_llm/deprecated_test.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true require "test_helper" +require_relative "integration_helper" require "braintrust/contrib/ruby_llm/deprecated" class Braintrust::Contrib::RubyLLM::DeprecatedTest < Minitest::Test + include Braintrust::Contrib::RubyLLM::IntegrationHelper + # --- .wrap --- def test_wrap_delegates_to_instrument @@ -105,12 +108,10 @@ def test_unwrap_returns_chat def test_unwrap_disables_instrumentation_for_subsequent_requests skip "RubyLLM gem not available" unless defined?(::RubyLLM::Chat) - VCR.use_cassette("contrib/ruby_llm/basic_chat") do + VCR.use_cassette(ruby_llm_cassette("basic_chat")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") diff --git a/test/braintrust/contrib/ruby_llm/instrumentation/chat_test.rb b/test/braintrust/contrib/ruby_llm/instrumentation/chat_test.rb index 788f36ae..9e46f969 100644 --- a/test/braintrust/contrib/ruby_llm/instrumentation/chat_test.rb +++ b/test/braintrust/contrib/ruby_llm/instrumentation/chat_test.rb @@ -69,8 +69,14 @@ def with_weather_test_tool tool_class = Class.new(RubyLLM::Tool) do description "Get the current weather for a location" - param :location, type: :string, desc: "The city and state, e.g. San Francisco, CA" - param :unit, type: :string, desc: "Temperature unit (celsius or fahrenheit)" + # ruby_llm 2.0 renamed the `param ..., desc:` DSL to `parameter ..., description:`. + if respond_to?(:parameter) + parameter :location, type: :string, description: "The city and state, e.g. San Francisco, CA" + parameter :unit, type: :string, description: "Temperature unit (celsius or fahrenheit)" + else + param :location, type: :string, desc: "The city and state, e.g. San Francisco, CA" + param :unit, type: :string, desc: "Temperature unit (celsius or fahrenheit)" + end def execute(location:, unit: "fahrenheit") {location: location, temperature: 72, unit: unit, conditions: "sunny"} @@ -82,6 +88,30 @@ def execute(location:, unit: "fahrenheit") end end + # Builds a user message carrying an attachment, across ruby_llm versions. + # 1.x wraps text + attachments in a RubyLLM::Content object; 2.0 removed + # Content and attaches directly to the message. + def user_message_with_attachment(text, path) + if defined?(::RubyLLM::Content) + content = ::RubyLLM::Content.new(text) + content.add_attachment(path) + ::RubyLLM::Message.new(role: :user, content: content) + else + ::RubyLLM::Message.new(role: :user, content: text, attachments: [path]) + end + end + + # Appends a user message carrying an attachment to a chat, across versions. + def add_user_message_with_attachment(chat, text, path) + if defined?(::RubyLLM::Content) + content = ::RubyLLM::Content.new(text) + content.add_attachment(path) + chat.add_message(role: :user, content: content) + else + chat.add_message(::RubyLLM::Message.new(role: :user, content: text, attachments: [path])) + end + end + def setup skip_unless_ruby_llm! end @@ -89,12 +119,10 @@ def setup # --- #complete (non-streaming) --- def test_complete_creates_span_with_correct_attributes - VCR.use_cassette("contrib/ruby_llm/basic_chat") do + VCR.use_cassette(ruby_llm_cassette("basic_chat")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") Braintrust.instrument!(:ruby_llm, target: chat, tracer_provider: rig.tracer_provider) @@ -142,12 +170,10 @@ def test_complete_creates_span_with_correct_attributes # --- #complete (streaming) --- def test_streaming_complete_creates_span - VCR.use_cassette("contrib/ruby_llm/streaming_chat") do + VCR.use_cassette(ruby_llm_cassette("streaming_chat")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") Braintrust.instrument!(:ruby_llm, target: chat, tracer_provider: rig.tracer_provider) @@ -198,16 +224,15 @@ def test_tool_calling_creates_nested_spans skip "Tool calling test requires ruby_llm >= 1.9" unless self.class.supports_tool_calling? with_weather_test_tool do |tool_class| - VCR.use_cassette("contrib/ruby_llm/tool_calling") do + VCR.use_cassette(ruby_llm_cassette("tool_calling")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") Braintrust.instrument!(:ruby_llm, target: chat, tracer_provider: rig.tracer_provider) - chat.with_tool(tool_class) + # with_tools is present in both 1.x and 2.0; 2.0 dropped the singular with_tool. + chat.with_tools(tool_class) response = chat.ask("What's the weather like in San Francisco?") @@ -268,12 +293,10 @@ def test_tool_calling_creates_nested_spans # --- Direct complete() --- def test_direct_complete_creates_span - VCR.use_cassette("contrib/ruby_llm/direct_complete") do + VCR.use_cassette(ruby_llm_cassette("direct_complete")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") Braintrust.instrument!(:ruby_llm, target: chat, tracer_provider: rig.tracer_provider) @@ -304,12 +327,10 @@ def test_direct_complete_creates_span # --- Idempotency --- def test_wrapping_is_idempotent - VCR.use_cassette("contrib/ruby_llm/basic_chat") do + VCR.use_cassette(ruby_llm_cassette("basic_chat")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr chat = RubyLLM.chat(model: "gpt-4o-mini") @@ -331,12 +352,10 @@ def test_wrapping_is_idempotent # --- Class-level instrumentation --- def test_class_level_instrumentation_traces_new_instances - VCR.use_cassette("contrib/ruby_llm/basic_chat") do + VCR.use_cassette(ruby_llm_cassette("basic_chat")) do rig = setup_otel_test_rig - RubyLLM.configure do |config| - config.openai_api_key = get_openai_key - end + configure_ruby_llm_for_vcr # Set the default tracer provider for class-level instrumentation Braintrust::Contrib.init(tracer_provider: rig.tracer_provider) @@ -377,16 +396,7 @@ def test_format_message_for_input_handles_content_object_with_attachments skip "RubyLLM gem not available" unless defined?(::RubyLLM) with_tmp_file(data: "This is test content") do |tmpfile| - # Create a Content object with an attachment (this triggers the Content object return) - content = ::RubyLLM::Content.new("Hello, this is the actual text content") - content.add_attachment(tmpfile.path) - - # Create a message with the Content object (simulates message with attachment) - msg = ::RubyLLM::Message.new(role: :user, content: content) - - # Verify the precondition: msg.content returns a Content object, not a string - assert msg.content.is_a?(::RubyLLM::Content), - "Precondition failed: Expected Content object when message has attachments" + msg = user_message_with_attachment("Hello, this is the actual text content", tmpfile.path) # Create a minimal chat-like object to test the helper method chat_class = Class.new do @@ -425,18 +435,7 @@ def test_format_message_for_input_handles_image_attachment skip "RubyLLM gem not available" unless defined?(::RubyLLM) with_png_file do |tmpfile| - # Create a Content object with an image attachment - content = ::RubyLLM::Content.new("What's in this image?") - content.add_attachment(tmpfile.path) - - # Create a message with the Content object - msg = ::RubyLLM::Message.new(role: :user, content: content) - - # Verify the precondition: msg.content returns a Content object - assert msg.content.is_a?(::RubyLLM::Content), - "Precondition failed: Expected Content object when message has image attachment" - assert_equal 1, msg.content.attachments.count, - "Expected one attachment" + msg = user_message_with_attachment("What's in this image?", tmpfile.path) # Create a minimal chat-like object to test the helper method chat_class = Class.new do @@ -486,9 +485,7 @@ def test_build_input_messages_handles_content_objects_with_attachments chat = ::RubyLLM.chat(model: "gpt-4o-mini") # Add message with image attachment using RubyLLM's API - content = ::RubyLLM::Content.new("Describe this image") - content.add_attachment(tmpfile.path) - chat.add_message(role: :user, content: content) + add_user_message_with_attachment(chat, "Describe this image", tmpfile.path) # Instrument the chat to get access to the helper method Braintrust.instrument!(:ruby_llm, target: chat) diff --git a/test/braintrust/contrib/ruby_llm/integration_helper.rb b/test/braintrust/contrib/ruby_llm/integration_helper.rb index c2a97885..a110818f 100644 --- a/test/braintrust/contrib/ruby_llm/integration_helper.rb +++ b/test/braintrust/contrib/ruby_llm/integration_helper.rb @@ -23,6 +23,32 @@ def load_ruby_llm_if_available require "ruby_llm" unless defined?(::RubyLLM) end end + + # Configures RubyLLM for cassette playback. + # + # RubyLLM.configure is global, so every cassette-backed test must call this + # rather than relying on whichever test happened to configure it first. + def configure_ruby_llm_for_vcr + ::RubyLLM.configure do |config| + config.openai_api_key = get_openai_key + end + end + + # Major version of the loaded ruby_llm gem. + def ruby_llm_major + Gem.loaded_specs["ruby_llm"]&.version&.segments&.first || 1 + end + + # Cassette path for a ruby_llm interaction. + # + # Breaking wire changes land on majors: 2.0 moved the OpenAI provider from + # Chat Completions to the Responses API. Recording per major means each + # version replays the endpoint it actually calls, and versions that share a + # wire format share a recording. A new major needs its own cassettes, which + # are recorded by running its appraisal with a real OPENAI_API_KEY. + def ruby_llm_cassette(name) + "contrib/ruby_llm/v#{ruby_llm_major}/#{name}" + end end end end diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/basic_chat.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/basic_chat.yml similarity index 100% rename from test/fixtures/vcr_cassettes/contrib/ruby_llm/basic_chat.yml rename to test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/basic_chat.yml diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/direct_complete.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/direct_complete.yml similarity index 100% rename from test/fixtures/vcr_cassettes/contrib/ruby_llm/direct_complete.yml rename to test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/direct_complete.yml diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/streaming_chat.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/streaming_chat.yml similarity index 100% rename from test/fixtures/vcr_cassettes/contrib/ruby_llm/streaming_chat.yml rename to test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/streaming_chat.yml diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/tool_calling.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/tool_calling.yml similarity index 100% rename from test/fixtures/vcr_cassettes/contrib/ruby_llm/tool_calling.yml rename to test/fixtures/vcr_cassettes/contrib/ruby_llm/v1/tool_calling.yml diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/basic_chat.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/basic_chat.yml new file mode 100644 index 00000000..384bbb41 --- /dev/null +++ b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/basic_chat.yml @@ -0,0 +1,342 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"Say ''test''"}],"stream":false,"store":false,"include":["reasoning.encrypted_content"]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:33 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f3761edf7ab5b3-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '1613' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999970' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_fc8f756495e6401481759f4885a47ad9 + Set-Cookie: + - __cf_bm=wIYVrv2j0_jAnmkCcx6R9KMofaagTFxufrh4UqHwdxY-1790102671.1734025-1.0.1.1-aOFcZk0O4nhnLqX3Wd770nviy3RAn0Iww3plkvy2TTZYr7BxFMwD.ru_N.qECNtKa3x4FPh_p7sf3TpjvqxxSEStKDJns8jA5GpY1is1uO9PAIBFDSKNEbP9kF8ZlA_F; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:33 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0f8264dd3939c6aa016ab2cc8ff07087d294d03b6974e07019", + "object": "response", + "created_at": 1790102671, + "status": "completed", + "access_programs": null, + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1790102673, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "msg_0f8264dd3939c6aa016ab2cc9178d087d2ab23d76f50f8fb5a", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Test." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 11, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 3, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 14 + }, + "user": null, + "metadata": {} + } + recorded_at: Tue, 22 Sep 2026 18:44:33 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"Say ''test''"},{"role":"assistant","content":[{"type":"output_text","text":"Test."}]},{"role":"user","content":"Say + ''test''"}],"stream":false,"store":false,"include":["reasoning.encrypted_content"]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:34 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f3762e0bf2258a-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '1167' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999957' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_6fe13a3f9ee241f9af880bc3c101ef0e + Set-Cookie: + - __cf_bm=72FqMEVsTszsI_ABCfiiDA1PYPQ4vbPSNPdnk7WrPZA-1790102673.6058316-1.0.1.1-uR1TN3bpJUqDZQKuyZdHF.zBeKnJY7AljD.oSxteqYxJQxM9v6ZLmL1VzOQwy7sbVB.Nn4vQM4jfZ7rlapokU5TWnpFarlSjt578WdF0rNnqRGI3ehG9toglqEOyFgYl; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:34 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_06bd5e88c80f9c83016ab2cc91b7cc87d2a7204d313efa1f4a", + "object": "response", + "created_at": 1790102673, + "status": "completed", + "access_programs": null, + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1790102674, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "msg_06bd5e88c80f9c83016ab2cc92cbb087d2b42d63371ed119b3", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Test." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 25, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 3, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 28 + }, + "user": null, + "metadata": {} + } + recorded_at: Tue, 22 Sep 2026 18:44:34 GMT +recorded_with: VCR 6.4.0 diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/direct_complete.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/direct_complete.yml new file mode 100644 index 00000000..be8b8519 --- /dev/null +++ b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/direct_complete.yml @@ -0,0 +1,172 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"Say ''hello''"}],"stream":false,"store":false,"include":["reasoning.encrypted_content"]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:43 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f3763679c270b8-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '8251' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999970' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_16f8e9336d8549b79e22d82ec07543d1 + Set-Cookie: + - __cf_bm=0GGLY1KQ1loE2PVfkLmaK.CLYAAZpVNDffm8kYwoCzk-1790102674.9519687-1.0.1.1-l8a_fWRxjE.CXiU44VSE5n1b5mC_5jTFj7V8QYSVoHYNjuJ744i2Qwp1CfoOQ1zUdNFweQ42Lqf_CBM0v3FoVVdV9CjZOD0LLjYfWsEEELFePsSfyl49GpRE83v4xdeE; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:43 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0c8f121f8a01ff50016ab2cc98e2a887d2bee79d68136dfe0a", + "object": "response", + "created_at": 1790102681, + "status": "completed", + "access_programs": null, + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1790102683, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "msg_0c8f121f8a01ff50016ab2cc9af13887d2b761586f0c1da945", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Hello! How can I assist you today?" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 11, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 10, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 21 + }, + "user": null, + "metadata": {} + } + recorded_at: Tue, 22 Sep 2026 18:44:43 GMT +recorded_with: VCR 6.4.0 diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/streaming_chat.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/streaming_chat.yml new file mode 100644 index 00000000..4c9dbffd --- /dev/null +++ b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/streaming_chat.yml @@ -0,0 +1,129 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"Count to + 3"}],"stream":true,"store":false,"include":["reasoning.encrypted_content"]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:49 GMT + Content-Type: + - text/event-stream; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f376885a07f8ab-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '1357' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999970' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_0d7c1ef2abf845cc9376f7e86d6f6783 + Set-Cookie: + - __cf_bm=M4T182Yi1WHa04fWrtRaqMZcTvs4MZpJ_.siMc98cnY-1790102688.0546997-1.0.1.1-hXMH9U1FgoyT_W3CGHh0XXAhI9k5ORlZMhe.jeI5DYQt7IqmhA0Q9IxzJ2kwBKo2FrKWalnLWeUlSBjW3NSCP66.QnvwQn_S24BlUVlyqlvzI.0Z_OK.pcBYNXmQxh1u; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:49 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_0a74d810176200d3016ab2cca02eac87d28f54265a46f5627d","object":"response","created_at":1790102688,"status":"in_progress","access_programs":null,"background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_0a74d810176200d3016ab2cca02eac87d28f54265a46f5627d","object":"response","created_at":1790102688,"status":"in_progress","access_programs":null,"background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"1","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"ZKUkGpUxO8mIt2h","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"Y641JrfYnCAXfyb","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" ","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"RX8GlVREe6Nbzbu","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"2","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"JZrJ1x19gMXnVJf","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":",","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"4LkzoJ0FHmmfgNc","output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" ","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"KEng23ExLv5uqrQ","output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"3","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"wMzqcDonRpuRrx9","output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"obfuscation":"9DCBI6n9z8NddX8","output_index":0,"sequence_number":11} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","logprobs":[],"output_index":0,"sequence_number":12,"text":"1, 2, 3."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"1, 2, 3."},"sequence_number":13} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"1, 2, 3."}],"role":"assistant"},"output_index":0,"sequence_number":14} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_0a74d810176200d3016ab2cca02eac87d28f54265a46f5627d","object":"response","created_at":1790102688,"status":"completed","access_programs":null,"background":false,"completed_at":1790102689,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_0a74d810176200d3016ab2cca183b087d2bc2b2810dc79d613","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"1, 2, 3."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tool_usage":{"image_gen":{"input_tokens":0,"input_tokens_details":{"image_tokens":0,"text_tokens":0},"output_tokens":0,"output_tokens_details":{"image_tokens":0,"text_tokens":0},"total_tokens":0},"web_search":{"num_requests":0}},"tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":11,"input_tokens_details":{"cache_write_tokens":0,"cached_tokens":0},"output_tokens":9,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":20},"user":null,"metadata":{}},"sequence_number":15} + + recorded_at: Tue, 22 Sep 2026 18:44:49 GMT +recorded_with: VCR 6.4.0 +... diff --git a/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/tool_calling.yml b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/tool_calling.yml new file mode 100644 index 00000000..a7d0d9d6 --- /dev/null +++ b/test/fixtures/vcr_cassettes/contrib/ruby_llm/v2/tool_calling.yml @@ -0,0 +1,395 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"What''s the + weather like in San Francisco?"}],"stream":false,"store":false,"include":["reasoning.encrypted_content"],"tools":[{"type":"function","name":"weather_test","description":"Get + the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"},"unit":{"type":"string","description":"Temperature + unit (celsius or fahrenheit)"}},"required":["location","unit"]},"strict":false}]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:45 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f3766e293f89da-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '1701' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999710' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_ce6b3919a39c444f9932a6e9d3c42481 + Set-Cookie: + - __cf_bm=NIZ2bxvMi_sUIWnr5FwS306i8qPgRFiDz502T_q7cJA-1790102683.868785-1.0.1.1-DSdQDmI.aNe.3sUyu9Ug_CFSNVO6iCdYlCAvi8XTJ7WtT4JhzhOngcbnnHd_XsGTU.2kEjNOezUk9.NRQfIkjf57z5RoDQm0tpD97t6bbrX4E1ABjUgEeDXA2ZWJEqHW; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:45 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_090caa56d5ac08be016ab2cc9bf85487d29205a93172706409", + "object": "response", + "created_at": 1790102684, + "status": "completed", + "access_programs": null, + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1790102685, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "fc_090caa56d5ac08be016ab2cc9d7aac87d28006b1291cd5222b", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"San Francisco, CA\",\"unit\":\"celsius\"}", + "call_id": "call_jsU5nzNJky4Dc8x97edOrzfs", + "name": "weather_test" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [ + { + "type": "function", + "description": "Get the current weather for a location", + "name": "weather_test", + "output_schema": null, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "description": "Temperature unit (celsius or fahrenheit)" + } + }, + "required": [ + "location", + "unit" + ] + }, + "strict": false + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 74, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 23, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 97 + }, + "user": null, + "metadata": {} + } + recorded_at: Tue, 22 Sep 2026 18:44:45 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","input":[{"role":"user","content":"What''s the + weather like in San Francisco?"},{"type":"function_call","call_id":"call_jsU5nzNJky4Dc8x97edOrzfs","name":"weather_test","arguments":"{\"location\":\"San + Francisco, CA\",\"unit\":\"celsius\"}"},{"type":"function_call_output","call_id":"call_jsU5nzNJky4Dc8x97edOrzfs","output":"{\"location\":\"San + Francisco, CA\",\"temperature\":72,\"unit\":\"celsius\",\"conditions\":\"sunny\"}"}],"stream":false,"store":false,"include":["reasoning.encrypted_content"],"tools":[{"type":"function","name":"weather_test","description":"Get + the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"},"unit":{"type":"string","description":"Temperature + unit (celsius or fahrenheit)"}},"required":["location","unit"]},"strict":false}]}' + headers: + User-Agent: + - Faraday v2.14.4 + 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: + - Tue, 22 Sep 2026 18:44:47 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Ray: + - a3f37679bdab07b2-CMH + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Access-Control-Expose-Headers: + - CF-Ray + - CF-Ray + - X-Request-ID + Openai-Organization: + - braintrust-data + Openai-Processing-Ms: + - '2079' + Openai-Project: + - proj_vsCSXafhhByzWOThMrJcZiw9 + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999660' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_99252bd369074093922bccefba651b5e + Set-Cookie: + - __cf_bm=cVICidGTgaad3MmQwd9QI.gUTjmO_qiXJ51_o6RUP8Q-1790102685.7204123-1.0.1.1-_zM36sKYFXWqO3o1yV3_ZyS.U9KWUF3rc._GlgkVez6K4L07Mf1u71cWWYpQm2vdgR._Rj7RkDKiOMeFj1e36eAucwwnYqy5LEenLusb8cVYaqSEh56LRa7HgOlb0a6L; + HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, + 22 Sep 2026 19:14:47 GMT + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05cdbed1ff989cfe016ab2cc9dd95c87d2ae50bbd3f1fc7e0e", + "object": "response", + "created_at": 1790102685, + "status": "completed", + "access_programs": null, + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1790102687, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "output": [ + { + "id": "msg_05cdbed1ff989cfe016ab2cc9f939487d28582fa8417e30738", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The weather in San Francisco is currently 22\u00b0C (72\u00b0F) and sunny." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": false, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [ + { + "type": "function", + "description": "Get the current weather for a location", + "name": "weather_test", + "output_schema": null, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "description": "Temperature unit (celsius or fahrenheit)" + } + }, + "required": [ + "location", + "unit" + ] + }, + "strict": false + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 124, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 19, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 143 + }, + "user": null, + "metadata": {} + } + recorded_at: Tue, 22 Sep 2026 18:44:47 GMT +recorded_with: VCR 6.4.0