From f722348afb86c0b37f413a1750f9bfd0b3404d43 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 29 Jun 2025 02:03:13 -0400 Subject: [PATCH 1/4] Add RubyLLM::Chat#with_options to pass options to the underlying API payload --- lib/ruby_llm/chat.rb | 9 +- lib/ruby_llm/provider.rb | 23 +++- ...-20241022_supports_service_tier_option.yml | 81 +++++++++++++ ...ku-20241022-v1_0_supports_top_k_option.yml | 54 +++++++++ ...k-chat_supports_response_format_option.yml | 57 +++++++++ ...0-flash_supports_responseschema_option.yml | 87 ++++++++++++++ ..._qwen3_supports_response_format_option.yml | 37 ++++++ ...1-nano_supports_response_format_option.yml | 113 ++++++++++++++++++ ...-haiku_supports_response_format_option.yml | 54 +++++++++ spec/ruby_llm/chat_options_spec.rb | 91 ++++++++++++++ 10 files changed, 599 insertions(+), 7 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml create mode 100644 spec/ruby_llm/chat_options_spec.rb diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index 3b5bfa83a..a83e1e0c1 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -11,7 +11,7 @@ module RubyLLM class Chat include Enumerable - attr_reader :model, :messages, :tools + attr_reader :model, :messages, :tools, :options def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider @@ -25,6 +25,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n @temperature = 0.7 @messages = [] @tools = {} + @options = {} @on = { new_message: nil, end_message: nil @@ -78,6 +79,11 @@ def with_context(context) self end + def with_options(**options) + @options = options + self + end + def on_new_message(&block) @on[:new_message] = block self @@ -100,6 +106,7 @@ def complete(&) temperature: @temperature, model: @model.id, connection: @connection, + options: @options, & ) @on[:end_message]&.call(response) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 2b09cdee1..43c4a2ec2 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -10,14 +10,19 @@ module Provider module Methods extend Streaming - def complete(messages, tools:, temperature:, model:, connection:, &) + def complete(messages, tools:, temperature:, model:, connection:, options: {}, &) normalized_temperature = maybe_normalize_temperature(temperature, model) - payload = render_payload(messages, - tools: tools, - temperature: normalized_temperature, - model: model, - stream: block_given?) + payload = deep_merge( + options, + render_payload( + messages, + tools: tools, + temperature: normalized_temperature, + model: model, + stream: block_given? + ) + ) if block_given? stream_response connection, payload, & @@ -26,6 +31,12 @@ def complete(messages, tools:, temperature:, model:, connection:, &) end end + def deep_merge(options, payload) + options.merge(payload) do |key, options_value, payload_value| + options_value.is_a?(Hash) && payload_value.is_a?(Hash) ? deep_merge(options_value, payload_value) : payload_value + end + end + def list_models(connection:) response = connection.get models_url parse_list_models_response response, slug, capabilities diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml new file mode 100644 index 000000000..924a9ecfb --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"service_tier":"standard_only","model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What + is the square root of 64? Answer with a JSON object with the key `result`."}]}],"temperature":0.7,"stream":false,"max_tokens":8192}' + headers: + User-Agent: + - Faraday v2.13.1 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + 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: + - Sun, 29 Jun 2025 04:50:59 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '25000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '25000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-06-29T04:50:58Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '5000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '5000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-06-29T04:50:59Z' + Anthropic-Ratelimit-Requests-Limit: + - '5' + Anthropic-Ratelimit-Requests-Remaining: + - '4' + Anthropic-Ratelimit-Requests-Reset: + - '2025-06-29T04:51:09Z' + Anthropic-Ratelimit-Tokens-Limit: + - '30000' + Anthropic-Ratelimit-Tokens-Remaining: + - '30000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-06-29T04:50:58Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 40072724-019e-4d79-83c0-8afb86354812 + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01UZyKC52tfiHLRy1rvMXg7N","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"{\n \"result\": + 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":27,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13,"service_tier":"standard"}}' + recorded_at: Sun, 29 Jun 2025 04:50:59 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml new file mode 100644 index 000000000..9f0eb5c2b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml @@ -0,0 +1,54 @@ +--- +http_interactions: +- request: + method: post + uri: https://bedrock-runtime..amazonaws.com/model/anthropic.claude-3-5-haiku-20241022-v1:0/invoke + body: + encoding: UTF-8 + string: '{"top_k":5,"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"What + is the square root of 64? Answer with a JSON object with the key `result`."}]}],"temperature":0.7,"max_tokens":4096}' + headers: + User-Agent: + - Faraday v2.13.1 + Host: + - bedrock-runtime..amazonaws.com + X-Amz-Date: + - 20250629T054919Z + X-Amz-Content-Sha256: + - 2737f3d02cae7cb6849cf4092f7029eb8a68bd328033e5dbf3d7a08f6819bcf6 + Authorization: + - AWS4-HMAC-SHA256 Credential=/20250629//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ae978f3d73488d6c716c2af3bd74741c8e495583b4bba38f3266309f643ba26f + Content-Type: + - application/json + Accept: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Sun, 29 Jun 2025 05:49:19 GMT + Content-Type: + - application/json + Content-Length: + - '268' + Connection: + - keep-alive + X-Amzn-Requestid: + - 146785ac-7eee-4c55-9ec7-b524a01365b3 + X-Amzn-Bedrock-Invocation-Latency: + - '533' + X-Amzn-Bedrock-Output-Token-Count: + - '13' + X-Amzn-Bedrock-Input-Token-Count: + - '27' + body: + encoding: UTF-8 + string: '{"id":"msg_bdrk_01CfsXd7H3GjFAHtTPU9Fm9W","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"{\n \"result\": + 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":27,"output_tokens":13}}' + recorded_at: Sun, 29 Jun 2025 05:49:19 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml new file mode 100644 index 000000000..e9526fbcd --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml @@ -0,0 +1,57 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"response_format":{"type":"json_object"},"model":"deepseek-chat","messages":[{"role":"user","content":"What + is the square root of 64? Answer with a JSON object with the key `result`."}],"stream":false,"temperature":0.7}' + headers: + User-Agent: + - Faraday v2.13.1 + 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: + - Sun, 29 Jun 2025 04:19:56 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - b881c21fbf8c99f00c9ea57b0d2342a6 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"e9d187a6-6a6b-43da-a445-8456c7e46545","object":"chat.completion","created":1751170796,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + 8\n}"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":39,"completion_tokens":13,"total_tokens":52,"prompt_tokens_details":{"cached_tokens":0},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":39},"system_fingerprint":"fp_8802369eaa_prod0623_fp8_kvcache"}' + recorded_at: Sun, 29 Jun 2025 04:20:00 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml new file mode 100644 index 000000000..2a2035259 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml @@ -0,0 +1,87 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + body: + encoding: UTF-8 + string: '{"generationConfig":{"responseMimeType":"application/json","responseSchema":{"type":"OBJECT","properties":{"result":{"type":"NUMBER"}}},"temperature":0.7},"contents":[{"role":"user","parts":[{"text":"What + is the square root of 64? Answer with a JSON object with the key `result`."}]}]}' + headers: + User-Agent: + - Faraday v2.13.1 + 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: + - Sun, 29 Jun 2025 05:24:48 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=548 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "{\n \"result\": 8\n}" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "avgLogprobs": -0.0243309885263443 + } + ], + "usageMetadata": { + "promptTokenCount": 24, + "candidatesTokenCount": 10, + "totalTokenCount": 34, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 24 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.0-flash", + "responseId": "IM5gaOycBMGRhMIPo4HP-Qk" + } + recorded_at: Sun, 29 Jun 2025 05:24:48 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml new file mode 100644 index 000000000..0d5b6505d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: "/chat/completions" + body: + encoding: UTF-8 + string: '{"response_format":{"type":"json_object"},"model":"qwen3","messages":[{"role":"user","content":"What + is the square root of 64? Answer with a JSON object with the key `result`."}],"stream":false,"temperature":0.7}' + headers: + User-Agent: + - Faraday v2.13.1 + 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 + Date: + - Sun, 29 Jun 2025 03:59:18 GMT + Content-Length: + - '301' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-570","object":"chat.completion","created":1751169558,"model":"qwen3","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + 8\n}"},"finish_reason":"stop"}],"usage":{"prompt_tokens":29,"completion_tokens":10,"total_tokens":39}} + + ' + recorded_at: Sun, 29 Jun 2025 03:59:18 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml new file mode 100644 index 000000000..0d1f20269 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml @@ -0,0 +1,113 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"response_format":{"type":"json_object"},"model":"gpt-4.1-nano","messages":[{"role":"user","content":"What + is the square root of 64? Answer with a JSON object with the key `result`."}],"stream":false,"temperature":0.7}' + headers: + User-Agent: + - Faraday v2.13.1 + 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: + - Sun, 29 Jun 2025 04:12:34 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '141' + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '144' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199977' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 6ms + X-Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-Bndh48hI90SJjTfRqmkqK6ZWnVykz", + "object": "chat.completion", + "created": 1751170354, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "{\n \"result\": 8\n}", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 27, + "completion_tokens": 9, + "total_tokens": 36, + "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_38343a2f8f" + } + recorded_at: Sun, 29 Jun 2025 04:12:34 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml new file mode 100644 index 000000000..51c22c415 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml @@ -0,0 +1,54 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"response_format":{"type":"json_object"},"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What + is the square root of 64? Answer with a JSON object with the key `result`."}],"stream":false,"temperature":0.7}' + headers: + User-Agent: + - Faraday v2.13.1 + 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: + - Sun, 29 Jun 2025 04:04:44 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + X-Clerk-Auth-Message: + - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, + token-carrier=header) + X-Clerk-Auth-Reason: + - token-invalid + X-Clerk-Auth-Status: + - signed-out + Vary: + - Accept-Encoding + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1751169884-9UMTTOHBjnu40hPVmq0i\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1751169884,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"{\\n + \ \\\"result\\\": 8\\n}\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":27,\"completion_tokens\":13,\"total_tokens\":40}}" + recorded_at: Sun, 29 Jun 2025 04:04:44 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_options_spec.rb b/spec/ruby_llm/chat_options_spec.rb new file mode 100644 index 000000000..307258ce2 --- /dev/null +++ b/spec/ruby_llm/chat_options_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Chat do + include_context 'with configured RubyLLM' + + describe 'with options' do + CHAT_MODELS.select { |model_info| [:deepseek, :openai, :openrouter, :ollama].include?(model_info[:provider])}.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports response_format option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations + chat = RubyLLM + .chat(model: model, provider: provider) + .with_options(response_format: {type: "json_object"}) + + response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + + json_object = JSON.parse(response.content) + expect(json_object).to eq({"result" => 8}) + + expect(response.role).to eq(:assistant) + expect(response.input_tokens).to be_positive + expect(response.output_tokens).to be_positive + end + end + + CHAT_MODELS.select { |model_info| [:anthropic].include?(model_info[:provider])}.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports service_tier option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations + chat = RubyLLM + .chat(model: model, provider: provider) + .with_options(service_tier: "standard_only") + + response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + + json_object = JSON.parse(response.content) + expect(json_object).to eq({"result" => 8}) + + expect(response.role).to eq(:assistant) + expect(response.input_tokens).to be_positive + expect(response.output_tokens).to be_positive + end + end + + CHAT_MODELS.select { |model_info| [:gemini].include?(model_info[:provider])}.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports responseSchema option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations + chat = RubyLLM + .chat(model: model, provider: provider) + .with_options(generationConfig: { + responseMimeType: "application/json", + responseSchema: { + type: "OBJECT", + properties: { result: {type: "NUMBER"} } + } + }) + + response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + + json_object = JSON.parse(response.content) + expect(json_object).to eq({"result" => 8}) + + expect(response.role).to eq(:assistant) + expect(response.input_tokens).to be_positive + expect(response.output_tokens).to be_positive + end + end + + CHAT_MODELS.select { |model_info| [:bedrock].include?(model_info[:provider])}.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports top_k option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations + chat = RubyLLM + .chat(model: model, provider: provider) + .with_options(top_k: 5) + + response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + + json_object = JSON.parse(response.content) + expect(json_object).to eq({"result" => 8}) + + expect(response.role).to eq(:assistant) + expect(response.input_tokens).to be_positive + expect(response.output_tokens).to be_positive + end + end + end +end From 1bfa342af89e0d33539bd388dc207a2bebf192cd Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 29 Jun 2025 02:35:31 -0400 Subject: [PATCH 2/4] Make rubocop happier --- lib/ruby_llm/provider.rb | 10 ++++-- spec/ruby_llm/chat_options_spec.rb | 52 +++++++++++++++--------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 43c4a2ec2..3cd283446 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -10,7 +10,7 @@ module Provider module Methods extend Streaming - def complete(messages, tools:, temperature:, model:, connection:, options: {}, &) + def complete(messages, tools:, temperature:, model:, connection:, options: {}, &) # rubocop:disable Metrics/ParameterLists normalized_temperature = maybe_normalize_temperature(temperature, model) payload = deep_merge( @@ -32,8 +32,12 @@ def complete(messages, tools:, temperature:, model:, connection:, options: {}, & end def deep_merge(options, payload) - options.merge(payload) do |key, options_value, payload_value| - options_value.is_a?(Hash) && payload_value.is_a?(Hash) ? deep_merge(options_value, payload_value) : payload_value + options.merge(payload) do |_key, options_value, payload_value| + if options_value.is_a?(Hash) && payload_value.is_a?(Hash) + deep_merge(options_value, payload_value) + else + payload_value + end end end diff --git a/spec/ruby_llm/chat_options_spec.rb b/spec/ruby_llm/chat_options_spec.rb index 307258ce2..164a0e3fb 100644 --- a/spec/ruby_llm/chat_options_spec.rb +++ b/spec/ruby_llm/chat_options_spec.rb @@ -6,18 +6,18 @@ include_context 'with configured RubyLLM' describe 'with options' do - CHAT_MODELS.select { |model_info| [:deepseek, :openai, :openrouter, :ollama].include?(model_info[:provider])}.each do |model_info| + CHAT_MODELS.select { |model_info| %i[deepseek openai openrouter ollama].include?(model_info[:provider]) }.each do |model_info| # rubocop:disable Layout/LineLength model = model_info[:model] provider = model_info[:provider] it "#{provider}/#{model} supports response_format option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(response_format: {type: "json_object"}) + .chat(model: model, provider: provider) + .with_options(response_format: { type: 'json_object' }) - response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') json_object = JSON.parse(response.content) - expect(json_object).to eq({"result" => 8}) + expect(json_object).to eq({ 'result' => 8 }) expect(response.role).to eq(:assistant) expect(response.input_tokens).to be_positive @@ -25,18 +25,18 @@ end end - CHAT_MODELS.select { |model_info| [:anthropic].include?(model_info[:provider])}.each do |model_info| + CHAT_MODELS.select { |model_info| model_info[:provider] == :anthropic }.each do |model_info| model = model_info[:model] provider = model_info[:provider] it "#{provider}/#{model} supports service_tier option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(service_tier: "standard_only") + .chat(model: model, provider: provider) + .with_options(service_tier: 'standard_only') - response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') json_object = JSON.parse(response.content) - expect(json_object).to eq({"result" => 8}) + expect(json_object).to eq({ 'result' => 8 }) expect(response.role).to eq(:assistant) expect(response.input_tokens).to be_positive @@ -44,24 +44,24 @@ end end - CHAT_MODELS.select { |model_info| [:gemini].include?(model_info[:provider])}.each do |model_info| + CHAT_MODELS.select { |model_info| model_info[:provider] == :gemini }.each do |model_info| model = model_info[:model] provider = model_info[:provider] it "#{provider}/#{model} supports responseSchema option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(generationConfig: { - responseMimeType: "application/json", - responseSchema: { - type: "OBJECT", - properties: { result: {type: "NUMBER"} } - } - }) + .chat(model: model, provider: provider) + .with_options(generationConfig: { + responseMimeType: 'application/json', + responseSchema: { + type: 'OBJECT', + properties: { result: { type: 'NUMBER' } } + } + }) - response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') json_object = JSON.parse(response.content) - expect(json_object).to eq({"result" => 8}) + expect(json_object).to eq({ 'result' => 8 }) expect(response.role).to eq(:assistant) expect(response.input_tokens).to be_positive @@ -69,18 +69,18 @@ end end - CHAT_MODELS.select { |model_info| [:bedrock].include?(model_info[:provider])}.each do |model_info| + CHAT_MODELS.select { |model_info| model_info[:provider] == :bedrock }.each do |model_info| model = model_info[:model] provider = model_info[:provider] it "#{provider}/#{model} supports top_k option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(top_k: 5) + .chat(model: model, provider: provider) + .with_options(top_k: 5) - response = chat.ask("What is the square root of 64? Answer with a JSON object with the key `result`.") + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') json_object = JSON.parse(response.content) - expect(json_object).to eq({"result" => 8}) + expect(json_object).to eq({ 'result' => 8 }) expect(response.role).to eq(:assistant) expect(response.input_tokens).to be_positive From d781d0e267495f1d603c20d06f9af7540dac3b44 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Wed, 2 Jul 2025 23:09:27 -0400 Subject: [PATCH 3/4] Rename #with_options to #with_request_options; update specs and docs --- docs/guides/chat.md | 15 ++- lib/ruby_llm/active_record/acts_as.rb | 5 + lib/ruby_llm/chat.rb | 10 +- lib/ruby_llm/provider.rb | 4 +- ...-haiku_supports_response_format_option.yml | 54 --------- ...20241022_supports_service_tier_option.yml} | 20 ++-- ...u-20241022-v1_0_supports_top_k_option.yml} | 28 ++--- ...-chat_supports_response_format_option.yml} | 8 +- ...-flash_supports_responseschema_option.yml} | 18 +-- ...qwen3_supports_response_format_option.yml} | 6 +- ...-nano_supports_response_format_option.yml} | 12 +- ...claude-3_5-haiku_supports_top_k_option.yml | 47 ++++++++ spec/ruby_llm/chat_options_spec.rb | 91 --------------- spec/ruby_llm/chat_request_options_spec.rb | 110 ++++++++++++++++++ spec/spec_helper.rb | 3 + 15 files changed, 232 insertions(+), 199 deletions(-) delete mode 100644 spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml rename spec/fixtures/vcr_cassettes/{chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml => chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml} (76%) rename spec/fixtures/vcr_cassettes/{chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml => chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml} (65%) rename spec/fixtures/vcr_cassettes/{chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml => chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml} (87%) rename spec/fixtures/vcr_cassettes/{chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml => chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml} (84%) rename spec/fixtures/vcr_cassettes/{chat_with_options_ollama_qwen3_supports_response_format_option.yml => chat_with_request_options_ollama_qwen3_supports_response_format_option.yml} (82%) rename spec/fixtures/vcr_cassettes/{chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml => chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml} (93%) create mode 100644 spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml delete mode 100644 spec/ruby_llm/chat_options_spec.rb create mode 100644 spec/ruby_llm/chat_request_options_spec.rb diff --git a/docs/guides/chat.md b/docs/guides/chat.md index 10ca78304..804d6f47f 100644 --- a/docs/guides/chat.md +++ b/docs/guides/chat.md @@ -246,6 +246,19 @@ puts response2.content You can set the temperature using `with_temperature`, which returns the `Chat` instance for chaining. +## Custom Request Options + +You can configure additional provider-specific features by adding custom fields to each API request. Use the `with_request_options` method. + +```ruby +# response_format parameter is supported by :openai, :ollama, :deepseek +chat = RubyLLM.chat.with_request_options(response_format: { type: 'json_object' }) +response = chat.ask "What is the square root of 64? Answer with a JSON object with the key `result`." +puts JSON.parse(response.content) +``` + +Allowed parameters vary widely by provider and model. + ## Tracking Token Usage Understanding token usage is important for managing costs and staying within context limits. Each `RubyLLM::Message` returned by `ask` includes token counts. @@ -311,4 +324,4 @@ This guide covered the core `Chat` interface. Now you might want to explore: * [Using Tools]({% link guides/tools.md %}): Enable the AI to call your Ruby code. * [Streaming Responses]({% link guides/streaming.md %}): Get real-time feedback from the AI. * [Rails Integration]({% link guides/rails.md %}): Persist your chat conversations easily. -* [Error Handling]({% link guides/error-handling.md %}): Build robust applications that handle API issues. \ No newline at end of file +* [Error Handling]({% link guides/error-handling.md %}): Build robust applications that handle API issues. diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 5b915391c..adf2f0681 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -130,6 +130,11 @@ def with_context(...) self end + def with_request_options(...) + to_llm.with_request_options(...) + self + end + def on_new_message(...) to_llm.on_new_message(...) self diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index a83e1e0c1..f42d50f3f 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -11,7 +11,7 @@ module RubyLLM class Chat include Enumerable - attr_reader :model, :messages, :tools, :options + attr_reader :model, :messages, :tools, :request_options def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider @@ -25,7 +25,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n @temperature = 0.7 @messages = [] @tools = {} - @options = {} + @request_options = {} @on = { new_message: nil, end_message: nil @@ -79,8 +79,8 @@ def with_context(context) self end - def with_options(**options) - @options = options + def with_request_options(**request_options) + @request_options = request_options self end @@ -106,7 +106,7 @@ def complete(&) temperature: @temperature, model: @model.id, connection: @connection, - options: @options, + request_options: @request_options, & ) @on[:end_message]&.call(response) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 3cd283446..992596973 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -10,11 +10,11 @@ module Provider module Methods extend Streaming - def complete(messages, tools:, temperature:, model:, connection:, options: {}, &) # rubocop:disable Metrics/ParameterLists + def complete(messages, tools:, temperature:, model:, connection:, request_options: {}, &) # rubocop:disable Metrics/ParameterLists normalized_temperature = maybe_normalize_temperature(temperature, model) payload = deep_merge( - options, + request_options, render_payload( messages, tools: tools, diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml deleted file mode 100644 index 51c22c415..000000000 --- a/spec/fixtures/vcr_cassettes/chat_with_options_openrouter_anthropic_claude-3_5-haiku_supports_response_format_option.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -http_interactions: -- request: - method: post - uri: https://openrouter.ai/api/v1/chat/completions - body: - encoding: UTF-8 - string: '{"response_format":{"type":"json_object"},"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What - is the square root of 64? Answer with a JSON object with the key `result`."}],"stream":false,"temperature":0.7}' - headers: - User-Agent: - - Faraday v2.13.1 - 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: - - Sun, 29 Jun 2025 04:04:44 GMT - Content-Type: - - application/json - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Access-Control-Allow-Origin: - - "*" - X-Clerk-Auth-Message: - - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, - token-carrier=header) - X-Clerk-Auth-Reason: - - token-invalid - X-Clerk-Auth-Status: - - signed-out - Vary: - - Accept-Encoding - Server: - - cloudflare - Cf-Ray: - - "" - body: - encoding: ASCII-8BIT - string: "\n \n{\"id\":\"gen-1751169884-9UMTTOHBjnu40hPVmq0i\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1751169884,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"{\\n - \ \\\"result\\\": 8\\n}\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":27,\"completion_tokens\":13,\"total_tokens\":40}}" - recorded_at: Sun, 29 Jun 2025 04:04:44 GMT -recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml similarity index 76% rename from spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml index 924a9ecfb..02cac0fba 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml @@ -6,7 +6,7 @@ http_interactions: body: encoding: UTF-8 string: '{"service_tier":"standard_only","model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What - is the square root of 64? Answer with a JSON object with the key `result`."}]}],"temperature":0.7,"stream":false,"max_tokens":8192}' + is the square root of 64? Answer with a JSON object with the key `result`."}]},{"role":"assistant","content":[{"type":"text","text":"{"}]}],"temperature":0.7,"stream":false,"max_tokens":8192}' headers: User-Agent: - Faraday v2.13.1 @@ -26,7 +26,7 @@ http_interactions: message: OK headers: Date: - - Sun, 29 Jun 2025 04:50:59 GMT + - Thu, 03 Jul 2025 02:43:26 GMT Content-Type: - application/json Transfer-Encoding: @@ -38,31 +38,31 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '25000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-06-29T04:50:58Z' + - '2025-07-03T02:43:26Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '5000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '5000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-06-29T04:50:59Z' + - '2025-07-03T02:43:26Z' Anthropic-Ratelimit-Requests-Limit: - '5' Anthropic-Ratelimit-Requests-Remaining: - '4' Anthropic-Ratelimit-Requests-Reset: - - '2025-06-29T04:51:09Z' + - '2025-07-03T02:43:37Z' Anthropic-Ratelimit-Tokens-Limit: - '30000' Anthropic-Ratelimit-Tokens-Remaining: - '30000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-06-29T04:50:58Z' + - '2025-07-03T02:43:26Z' Request-Id: - "" Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - - 40072724-019e-4d79-83c0-8afb86354812 + - "" Via: - 1.1 google Cf-Cache-Status: @@ -75,7 +75,7 @@ http_interactions: - "" body: encoding: ASCII-8BIT - string: '{"id":"msg_01UZyKC52tfiHLRy1rvMXg7N","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"{\n \"result\": - 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":27,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":13,"service_tier":"standard"}}' - recorded_at: Sun, 29 Jun 2025 04:50:59 GMT + string: '{"id":"msg_01TXa1GCLZd1eXkopxEdsXGc","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": + 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":28,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":12,"service_tier":"standard"}}' + recorded_at: Thu, 03 Jul 2025 02:43:26 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml similarity index 65% rename from spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml index 9f0eb5c2b..8e96b59e7 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml @@ -6,19 +6,19 @@ http_interactions: body: encoding: UTF-8 string: '{"top_k":5,"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"What - is the square root of 64? Answer with a JSON object with the key `result`."}]}],"temperature":0.7,"max_tokens":4096}' + is the square root of 64? Answer with a JSON object with the key `result`."}]},{"role":"assistant","content":[{"type":"text","text":"{"}]}],"temperature":0.7,"max_tokens":4096}' headers: User-Agent: - Faraday v2.13.1 Host: - bedrock-runtime..amazonaws.com X-Amz-Date: - - 20250629T054919Z + - 20250703T023014Z X-Amz-Content-Sha256: - - 2737f3d02cae7cb6849cf4092f7029eb8a68bd328033e5dbf3d7a08f6819bcf6 + - 6e66b0df343074bb4e2b11bf211a10441f15ad68ce5af9849ddf69242979563a Authorization: - - AWS4-HMAC-SHA256 Credential=/20250629//bedrock/aws4_request, - SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ae978f3d73488d6c716c2af3bd74741c8e495583b4bba38f3266309f643ba26f + - AWS4-HMAC-SHA256 Credential=/20250703//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=fcd9e32b652d60a37b7cc97d6fcba77ada3b58fdd75025df3e6285dcb4aff15c Content-Type: - application/json Accept: @@ -31,24 +31,24 @@ http_interactions: message: OK headers: Date: - - Sun, 29 Jun 2025 05:49:19 GMT + - Thu, 03 Jul 2025 02:30:15 GMT Content-Type: - application/json Content-Length: - - '268' + - '267' Connection: - keep-alive X-Amzn-Requestid: - - 146785ac-7eee-4c55-9ec7-b524a01365b3 + - 9e3152dd-eee4-4058-bc15-3b5b02aa2156 X-Amzn-Bedrock-Invocation-Latency: - - '533' + - '568' X-Amzn-Bedrock-Output-Token-Count: - - '13' + - '12' X-Amzn-Bedrock-Input-Token-Count: - - '27' + - '28' body: encoding: UTF-8 - string: '{"id":"msg_bdrk_01CfsXd7H3GjFAHtTPU9Fm9W","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"{\n \"result\": - 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":27,"output_tokens":13}}' - recorded_at: Sun, 29 Jun 2025 05:49:19 GMT + string: '{"id":"msg_bdrk_01KcHJWHGoWa5hRNBV8QL35e","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": + 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":28,"output_tokens":12}}' + recorded_at: Thu, 03 Jul 2025 02:30:15 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml similarity index 87% rename from spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml index e9526fbcd..55b0340cf 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_deepseek_deepseek-chat_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml @@ -24,7 +24,7 @@ http_interactions: message: OK headers: Date: - - Sun, 29 Jun 2025 04:19:56 GMT + - Thu, 03 Jul 2025 01:20:55 GMT Content-Type: - application/json Transfer-Encoding: @@ -36,7 +36,7 @@ http_interactions: Access-Control-Allow-Credentials: - 'true' X-Ds-Trace-Id: - - b881c21fbf8c99f00c9ea57b0d2342a6 + - 56c757d1f9be60608da5d92e8cfc7a44 Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload X-Content-Type-Options: @@ -51,7 +51,7 @@ http_interactions: - "" body: encoding: ASCII-8BIT - string: '{"id":"e9d187a6-6a6b-43da-a445-8456c7e46545","object":"chat.completion","created":1751170796,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + string: '{"id":"0d2a51f8-56cc-4c6a-b6f1-fbc73fbd098f","object":"chat.completion","created":1751505655,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": 8\n}"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":39,"completion_tokens":13,"total_tokens":52,"prompt_tokens_details":{"cached_tokens":0},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":39},"system_fingerprint":"fp_8802369eaa_prod0623_fp8_kvcache"}' - recorded_at: Sun, 29 Jun 2025 04:20:00 GMT + recorded_at: Thu, 03 Jul 2025 01:20:58 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml similarity index 84% rename from spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml index 2a2035259..9298aa891 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml @@ -30,7 +30,7 @@ http_interactions: - Referer - X-Origin Date: - - Sun, 29 Jun 2025 05:24:48 GMT + - Thu, 03 Jul 2025 01:21:15 GMT Server: - scaffolding on HTTPServer2 X-Xss-Protection: @@ -40,7 +40,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=548 + - gfet4t7; dur=572 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -54,19 +54,19 @@ http_interactions: "content": { "parts": [ { - "text": "{\n \"result\": 8\n}" + "text": "{\n\"result\": 8\n}" } ], "role": "model" }, "finishReason": "STOP", - "avgLogprobs": -0.0243309885263443 + "avgLogprobs": -0.17098161909315321 } ], "usageMetadata": { "promptTokenCount": 24, - "candidatesTokenCount": 10, - "totalTokenCount": 34, + "candidatesTokenCount": 9, + "totalTokenCount": 33, "promptTokensDetails": [ { "modality": "TEXT", @@ -76,12 +76,12 @@ http_interactions: "candidatesTokensDetails": [ { "modality": "TEXT", - "tokenCount": 10 + "tokenCount": 9 } ] }, "modelVersion": "gemini-2.0-flash", - "responseId": "IM5gaOycBMGRhMIPo4HP-Qk" + "responseId": "C9tlaOnYFPCrqsMPvqfz4Qw" } - recorded_at: Sun, 29 Jun 2025 05:24:48 GMT + recorded_at: Thu, 03 Jul 2025 01:21:15 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml similarity index 82% rename from spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml index 0d5b6505d..31be35c45 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_ollama_qwen3_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml @@ -24,14 +24,14 @@ http_interactions: Content-Type: - application/json Date: - - Sun, 29 Jun 2025 03:59:18 GMT + - Thu, 03 Jul 2025 01:21:14 GMT Content-Length: - '301' body: encoding: UTF-8 - string: '{"id":"chatcmpl-570","object":"chat.completion","created":1751169558,"model":"qwen3","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + string: '{"id":"chatcmpl-758","object":"chat.completion","created":1751505674,"model":"qwen3","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": 8\n}"},"finish_reason":"stop"}],"usage":{"prompt_tokens":29,"completion_tokens":10,"total_tokens":39}} ' - recorded_at: Sun, 29 Jun 2025 03:59:18 GMT + recorded_at: Thu, 03 Jul 2025 01:21:14 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml similarity index 93% rename from spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml index 0d1f20269..77eb648c4 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_options_openai_gpt-4_1-nano_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml @@ -24,7 +24,7 @@ http_interactions: message: OK headers: Date: - - Sun, 29 Jun 2025 04:12:34 GMT + - Thu, 03 Jul 2025 01:20:59 GMT Content-Type: - application/json Transfer-Encoding: @@ -36,11 +36,11 @@ http_interactions: Openai-Organization: - "" Openai-Processing-Ms: - - '141' + - '160' Openai-Version: - '2020-10-01' X-Envoy-Upstream-Service-Time: - - '144' + - '162' X-Ratelimit-Limit-Requests: - '500' X-Ratelimit-Limit-Tokens: @@ -74,9 +74,9 @@ http_interactions: encoding: ASCII-8BIT string: | { - "id": "chatcmpl-Bndh48hI90SJjTfRqmkqK6ZWnVykz", + "id": "chatcmpl-Bp2vDJ6we2xCrUhBondp1eCMg6VAE", "object": "chat.completion", - "created": 1751170354, + "created": 1751505659, "model": "gpt-4.1-nano-2025-04-14", "choices": [ { @@ -109,5 +109,5 @@ http_interactions: "service_tier": "default", "system_fingerprint": "fp_38343a2f8f" } - recorded_at: Sun, 29 Jun 2025 04:12:34 GMT + recorded_at: Thu, 03 Jul 2025 01:20:59 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml b/spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml new file mode 100644 index 000000000..58c59be35 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"top_k":5,"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What + is the square root of 64? Answer with a JSON object with the key `result`."},{"role":"assistant","content":"{"}],"stream":false,"temperature":0.7}' + headers: + User-Agent: + - Faraday v2.13.1 + 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: + - Thu, 03 Jul 2025 02:35:13 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n\n \n{\"id\":\"gen-1751510112-xLg5OxX2KgL0qJOHgQAo\",\"provider\":\"Anthropic\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1751510112,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\\n + \ \\\"result\\\": 8\\n}\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":28,\"completion_tokens\":12,\"total_tokens\":40}}" + recorded_at: Thu, 03 Jul 2025 02:35:13 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_options_spec.rb b/spec/ruby_llm/chat_options_spec.rb deleted file mode 100644 index 164a0e3fb..000000000 --- a/spec/ruby_llm/chat_options_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe RubyLLM::Chat do - include_context 'with configured RubyLLM' - - describe 'with options' do - CHAT_MODELS.select { |model_info| %i[deepseek openai openrouter ollama].include?(model_info[:provider]) }.each do |model_info| # rubocop:disable Layout/LineLength - model = model_info[:model] - provider = model_info[:provider] - it "#{provider}/#{model} supports response_format option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations - chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(response_format: { type: 'json_object' }) - - response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') - - json_object = JSON.parse(response.content) - expect(json_object).to eq({ 'result' => 8 }) - - expect(response.role).to eq(:assistant) - expect(response.input_tokens).to be_positive - expect(response.output_tokens).to be_positive - end - end - - CHAT_MODELS.select { |model_info| model_info[:provider] == :anthropic }.each do |model_info| - model = model_info[:model] - provider = model_info[:provider] - it "#{provider}/#{model} supports service_tier option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations - chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(service_tier: 'standard_only') - - response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') - - json_object = JSON.parse(response.content) - expect(json_object).to eq({ 'result' => 8 }) - - expect(response.role).to eq(:assistant) - expect(response.input_tokens).to be_positive - expect(response.output_tokens).to be_positive - end - end - - CHAT_MODELS.select { |model_info| model_info[:provider] == :gemini }.each do |model_info| - model = model_info[:model] - provider = model_info[:provider] - it "#{provider}/#{model} supports responseSchema option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations - chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(generationConfig: { - responseMimeType: 'application/json', - responseSchema: { - type: 'OBJECT', - properties: { result: { type: 'NUMBER' } } - } - }) - - response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') - - json_object = JSON.parse(response.content) - expect(json_object).to eq({ 'result' => 8 }) - - expect(response.role).to eq(:assistant) - expect(response.input_tokens).to be_positive - expect(response.output_tokens).to be_positive - end - end - - CHAT_MODELS.select { |model_info| model_info[:provider] == :bedrock }.each do |model_info| - model = model_info[:model] - provider = model_info[:provider] - it "#{provider}/#{model} supports top_k option" do # rubocop:disable RSpec/ExampleLength,RSpec/MultipleExpectations - chat = RubyLLM - .chat(model: model, provider: provider) - .with_options(top_k: 5) - - response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') - - json_object = JSON.parse(response.content) - expect(json_object).to eq({ 'result' => 8 }) - - expect(response.role).to eq(:assistant) - expect(response.input_tokens).to be_positive - expect(response.output_tokens).to be_positive - end - end - end -end diff --git a/spec/ruby_llm/chat_request_options_spec.rb b/spec/ruby_llm/chat_request_options_spec.rb new file mode 100644 index 000000000..02a05b560 --- /dev/null +++ b/spec/ruby_llm/chat_request_options_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Chat do + include_context 'with configured RubyLLM' + + describe 'with request_options' do + # Supported request_options vary by provider, and to lesser degree, by model. + + # Providers [:openai, :ollama, :deepseek] support {response_format: {type: 'json_object'}} + # to guarantee a JSON object is returned. + # (Note that :openrouter may accept the parameter but silently ignore it.) + CHAT_MODELS.select { |model_info| %i[openai ollama deepseek].include?(model_info[:provider]) }.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports response_format option" do # rubocop:disable RSpec/ExampleLength + chat = RubyLLM + .chat(model: model, provider: provider) + .with_request_options(response_format: { type: 'json_object' }) + + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') + + json_response = JSON.parse(response.content) + expect(json_response).to eq({ 'result' => 8 }) + end + end + + # Provider [:gemini] supports a {generationConfig: {responseMimeType: ..., responseSchema: ...} } option, + # which can specify a JSON schema, requiring a deep_merge of request_options into the payload. + CHAT_MODELS.select { |model_info| model_info[:provider] == :gemini }.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports responseSchema option" do # rubocop:disable RSpec/ExampleLength + chat = RubyLLM + .chat(model: model, provider: provider) + .with_request_options( + generationConfig: { + responseMimeType: 'application/json', + responseSchema: { + type: 'OBJECT', + properties: { result: { type: 'NUMBER' } } + } + } + ) + + response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') + + json_response = JSON.parse(response.content) + expect(json_response).to eq({ 'result' => 8 }) + end + end + + # Provider [:anthropic] supports a service_tier option. + CHAT_MODELS.select { |model_info| model_info[:provider] == :anthropic }.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports service_tier option" do # rubocop:disable RSpec/ExampleLength + chat = RubyLLM + .chat(model: model, provider: provider) + .with_request_options(service_tier: 'standard_only') + + chat.add_message( + role: :user, + content: 'What is the square root of 64? Answer with a JSON object with the key `result`.' + ) + + # :anthropic does not support {response_format: {type: 'json_object'}}, + # but can be steered this way by adding a leading '{' as assistant. + # (This leading '{' must be prepended to response.content before parsing.) + chat.add_message( + role: :assistant, + content: '{' + ) + + response = chat.complete + + json_response = JSON.parse('{' + response.content) # rubocop:disable Style/StringConcatenation + expect(json_response).to eq({ 'result' => 8 }) + end + end + + # Providers [:openrouter, :bedrock] supports a {top_k: ...} option to remove low-probability next tokens. + CHAT_MODELS.select { |model_info| %i[openrouter bedrock].include?(model_info[:provider]) }.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + it "#{provider}/#{model} supports top_k option" do # rubocop:disable RSpec/ExampleLength + chat = RubyLLM + .chat(model: model, provider: provider) + .with_request_options(top_k: 5) + + chat.add_message( + role: :user, + content: 'What is the square root of 64? Answer with a JSON object with the key `result`.' + ) + + # See comment on :anthropic example above for explanation of steering the model toward a JSON object response. + chat.add_message( + role: :assistant, + content: '{' + ) + + response = chat.complete + + json_response = JSON.parse('{' + response.content) # rubocop:disable Style/StringConcatenation + expect(json_response).to eq({ 'result' => 8 }) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e25f67e88..e68be5720 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -76,6 +76,9 @@ config.filter_sensitive_data('') do |interaction| interaction.response.headers['Openai-Organization']&.first end + config.filter_sensitive_data('') do |interaction| + interaction.response.headers['Anthropic-Organization-Id']&.first + end config.filter_sensitive_data('') { |interaction| interaction.response.headers['X-Request-Id']&.first } config.filter_sensitive_data('') { |interaction| interaction.response.headers['Request-Id']&.first } config.filter_sensitive_data('') { |interaction| interaction.response.headers['Cf-Ray']&.first } From b2a7c6996aec0a9e8633edd3c340cb31108ec380 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sat, 19 Jul 2025 16:01:14 -0400 Subject: [PATCH 4/4] Rename #with_request_options to #with_params --- docs/guides/chat.md | 6 ++-- lib/ruby_llm/active_record/acts_as.rb | 4 +-- lib/ruby_llm/chat.rb | 10 +++---- lib/ruby_llm/provider.rb | 12 ++++---- ...-20241022_supports_service_tier_param.yml} | 14 +++++----- ...ku-20241022-v1_0_supports_top_k_param.yml} | 16 +++++------ ...k-chat_supports_response_format_param.yml} | 8 +++--- ...0-flash_supports_responseschema_param.yml} | 18 ++++++------ ..._qwen3_supports_response_format_param.yml} | 6 ++-- ...1-nano_supports_response_format_param.yml} | 16 ++++++----- ...claude-3_5-haiku_supports_top_k_param.yml} | 13 +++++++-- spec/ruby_llm/chat_request_options_spec.rb | 28 +++++++++---------- 12 files changed, 80 insertions(+), 71 deletions(-) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml => chat_with_params_anthropic_claude-3-5-haiku-20241022_supports_service_tier_param.yml} (89%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml => chat_with_params_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_param.yml} (82%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml => chat_with_params_deepseek_deepseek-chat_supports_response_format_param.yml} (87%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml => chat_with_params_gemini_gemini-2_0-flash_supports_responseschema_param.yml} (84%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_ollama_qwen3_supports_response_format_option.yml => chat_with_params_ollama_qwen3_supports_response_format_param.yml} (82%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml => chat_with_params_openai_gpt-4_1-nano_supports_response_format_param.yml} (90%) rename spec/fixtures/vcr_cassettes/{chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml => chat_with_params_openrouter_anthropic_claude-3_5-haiku_supports_top_k_param.yml} (61%) diff --git a/docs/guides/chat.md b/docs/guides/chat.md index 804d6f47f..e365e0fa9 100644 --- a/docs/guides/chat.md +++ b/docs/guides/chat.md @@ -246,13 +246,13 @@ puts response2.content You can set the temperature using `with_temperature`, which returns the `Chat` instance for chaining. -## Custom Request Options +## Custom Request Parameters -You can configure additional provider-specific features by adding custom fields to each API request. Use the `with_request_options` method. +You can configure additional provider-specific features by adding custom fields to each API request. Use the `with_params` method. ```ruby # response_format parameter is supported by :openai, :ollama, :deepseek -chat = RubyLLM.chat.with_request_options(response_format: { type: 'json_object' }) +chat = RubyLLM.chat.with_params(response_format: { type: 'json_object' }) response = chat.ask "What is the square root of 64? Answer with a JSON object with the key `result`." puts JSON.parse(response.content) ``` diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index adf2f0681..25abfefa9 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -130,8 +130,8 @@ def with_context(...) self end - def with_request_options(...) - to_llm.with_request_options(...) + def with_params(...) + to_llm.with_params(...) self end diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index f42d50f3f..661bc29c6 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -11,7 +11,7 @@ module RubyLLM class Chat include Enumerable - attr_reader :model, :messages, :tools, :request_options + attr_reader :model, :messages, :tools, :params def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider @@ -25,7 +25,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n @temperature = 0.7 @messages = [] @tools = {} - @request_options = {} + @params = {} @on = { new_message: nil, end_message: nil @@ -79,8 +79,8 @@ def with_context(context) self end - def with_request_options(**request_options) - @request_options = request_options + def with_params(**params) + @params = params self end @@ -106,7 +106,7 @@ def complete(&) temperature: @temperature, model: @model.id, connection: @connection, - request_options: @request_options, + params: @params, & ) @on[:end_message]&.call(response) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 992596973..aead450f0 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -10,11 +10,11 @@ module Provider module Methods extend Streaming - def complete(messages, tools:, temperature:, model:, connection:, request_options: {}, &) # rubocop:disable Metrics/ParameterLists + def complete(messages, tools:, temperature:, model:, connection:, params: {}, &) # rubocop:disable Metrics/ParameterLists normalized_temperature = maybe_normalize_temperature(temperature, model) payload = deep_merge( - request_options, + params, render_payload( messages, tools: tools, @@ -31,10 +31,10 @@ def complete(messages, tools:, temperature:, model:, connection:, request_option end end - def deep_merge(options, payload) - options.merge(payload) do |_key, options_value, payload_value| - if options_value.is_a?(Hash) && payload_value.is_a?(Hash) - deep_merge(options_value, payload_value) + def deep_merge(params, payload) + params.merge(payload) do |_key, params_value, payload_value| + if params_value.is_a?(Hash) && payload_value.is_a?(Hash) + deep_merge(params_value, payload_value) else payload_value end diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_anthropic_claude-3-5-haiku-20241022_supports_service_tier_param.yml similarity index 89% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_anthropic_claude-3-5-haiku-20241022_supports_service_tier_param.yml index 02cac0fba..1d421fe8d 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_anthropic_claude-3-5-haiku-20241022_supports_service_tier_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_anthropic_claude-3-5-haiku-20241022_supports_service_tier_param.yml @@ -26,7 +26,7 @@ http_interactions: message: OK headers: Date: - - Thu, 03 Jul 2025 02:43:26 GMT + - Sat, 19 Jul 2025 19:53:47 GMT Content-Type: - application/json Transfer-Encoding: @@ -38,25 +38,25 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '25000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-07-03T02:43:26Z' + - '2025-07-19T19:53:46Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '5000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '5000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-07-03T02:43:26Z' + - '2025-07-19T19:53:47Z' Anthropic-Ratelimit-Requests-Limit: - '5' Anthropic-Ratelimit-Requests-Remaining: - '4' Anthropic-Ratelimit-Requests-Reset: - - '2025-07-03T02:43:37Z' + - '2025-07-19T19:53:58Z' Anthropic-Ratelimit-Tokens-Limit: - '30000' Anthropic-Ratelimit-Tokens-Remaining: - '30000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-07-03T02:43:26Z' + - '2025-07-19T19:53:46Z' Request-Id: - "" Strict-Transport-Security: @@ -75,7 +75,7 @@ http_interactions: - "" body: encoding: ASCII-8BIT - string: '{"id":"msg_01TXa1GCLZd1eXkopxEdsXGc","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": + string: '{"id":"msg_01Y9cx1icSo5LmER4hv3mX6M","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":28,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":12,"service_tier":"standard"}}' - recorded_at: Thu, 03 Jul 2025 02:43:26 GMT + recorded_at: Sat, 19 Jul 2025 19:53:47 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_param.yml similarity index 82% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_param.yml index 8e96b59e7..f30d1378d 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_supports_top_k_param.yml @@ -13,12 +13,12 @@ http_interactions: Host: - bedrock-runtime..amazonaws.com X-Amz-Date: - - 20250703T023014Z + - 20250719T195347Z X-Amz-Content-Sha256: - 6e66b0df343074bb4e2b11bf211a10441f15ad68ce5af9849ddf69242979563a Authorization: - - AWS4-HMAC-SHA256 Credential=/20250703//bedrock/aws4_request, - SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=fcd9e32b652d60a37b7cc97d6fcba77ada3b58fdd75025df3e6285dcb4aff15c + - AWS4-HMAC-SHA256 Credential=/20250719//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=b0b4bc8e31635379223d6df2e1f4f6fe8973c802fcd7414a50f16651a7779941 Content-Type: - application/json Accept: @@ -31,7 +31,7 @@ http_interactions: message: OK headers: Date: - - Thu, 03 Jul 2025 02:30:15 GMT + - Sat, 19 Jul 2025 19:53:47 GMT Content-Type: - application/json Content-Length: @@ -39,16 +39,16 @@ http_interactions: Connection: - keep-alive X-Amzn-Requestid: - - 9e3152dd-eee4-4058-bc15-3b5b02aa2156 + - 0fa198e8-bb3d-47a4-9845-6ab084dcb4a7 X-Amzn-Bedrock-Invocation-Latency: - - '568' + - '577' X-Amzn-Bedrock-Output-Token-Count: - '12' X-Amzn-Bedrock-Input-Token-Count: - '28' body: encoding: UTF-8 - string: '{"id":"msg_bdrk_01KcHJWHGoWa5hRNBV8QL35e","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": + string: '{"id":"msg_bdrk_01LtafMjvqzvDCrnHXmW3rRB","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"\n \"result\": 8\n}"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":28,"output_tokens":12}}' - recorded_at: Thu, 03 Jul 2025 02:30:15 GMT + recorded_at: Sat, 19 Jul 2025 19:53:47 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_deepseek_deepseek-chat_supports_response_format_param.yml similarity index 87% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_deepseek_deepseek-chat_supports_response_format_param.yml index 55b0340cf..05b3bece9 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_deepseek_deepseek-chat_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_deepseek_deepseek-chat_supports_response_format_param.yml @@ -24,7 +24,7 @@ http_interactions: message: OK headers: Date: - - Thu, 03 Jul 2025 01:20:55 GMT + - Sat, 19 Jul 2025 19:53:26 GMT Content-Type: - application/json Transfer-Encoding: @@ -36,7 +36,7 @@ http_interactions: Access-Control-Allow-Credentials: - 'true' X-Ds-Trace-Id: - - 56c757d1f9be60608da5d92e8cfc7a44 + - 735400e014bd761346ad96d9d347e5cd Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload X-Content-Type-Options: @@ -51,7 +51,7 @@ http_interactions: - "" body: encoding: ASCII-8BIT - string: '{"id":"0d2a51f8-56cc-4c6a-b6f1-fbc73fbd098f","object":"chat.completion","created":1751505655,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + string: '{"id":"70050189-c6ad-4123-a660-9e558cf8d030","object":"chat.completion","created":1752954806,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": 8\n}"},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":39,"completion_tokens":13,"total_tokens":52,"prompt_tokens_details":{"cached_tokens":0},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":39},"system_fingerprint":"fp_8802369eaa_prod0623_fp8_kvcache"}' - recorded_at: Thu, 03 Jul 2025 01:20:58 GMT + recorded_at: Sat, 19 Jul 2025 19:53:30 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_gemini_gemini-2_0-flash_supports_responseschema_param.yml similarity index 84% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_gemini_gemini-2_0-flash_supports_responseschema_param.yml index 9298aa891..bc7746cec 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_gemini_gemini-2_0-flash_supports_responseschema_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_gemini_gemini-2_0-flash_supports_responseschema_param.yml @@ -30,7 +30,7 @@ http_interactions: - Referer - X-Origin Date: - - Thu, 03 Jul 2025 01:21:15 GMT + - Sat, 19 Jul 2025 19:53:46 GMT Server: - scaffolding on HTTPServer2 X-Xss-Protection: @@ -40,7 +40,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=572 + - gfet4t7; dur=565 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -54,19 +54,19 @@ http_interactions: "content": { "parts": [ { - "text": "{\n\"result\": 8\n}" + "text": "{\n \"result\": 8\n}" } ], "role": "model" }, "finishReason": "STOP", - "avgLogprobs": -0.17098161909315321 + "avgLogprobs": -0.0243309885263443 } ], "usageMetadata": { "promptTokenCount": 24, - "candidatesTokenCount": 9, - "totalTokenCount": 33, + "candidatesTokenCount": 10, + "totalTokenCount": 34, "promptTokensDetails": [ { "modality": "TEXT", @@ -76,12 +76,12 @@ http_interactions: "candidatesTokensDetails": [ { "modality": "TEXT", - "tokenCount": 9 + "tokenCount": 10 } ] }, "modelVersion": "gemini-2.0-flash", - "responseId": "C9tlaOnYFPCrqsMPvqfz4Qw" + "responseId": "yfd7aPOxOe6r1dkPw4Wy-QM" } - recorded_at: Thu, 03 Jul 2025 01:21:15 GMT + recorded_at: Sat, 19 Jul 2025 19:53:46 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_ollama_qwen3_supports_response_format_param.yml similarity index 82% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_ollama_qwen3_supports_response_format_param.yml index 31be35c45..f1a8219ac 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_ollama_qwen3_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_ollama_qwen3_supports_response_format_param.yml @@ -24,14 +24,14 @@ http_interactions: Content-Type: - application/json Date: - - Thu, 03 Jul 2025 01:21:14 GMT + - Sat, 19 Jul 2025 19:53:45 GMT Content-Length: - '301' body: encoding: UTF-8 - string: '{"id":"chatcmpl-758","object":"chat.completion","created":1751505674,"model":"qwen3","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": + string: '{"id":"chatcmpl-176","object":"chat.completion","created":1752954825,"model":"qwen3","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"{\n \"result\": 8\n}"},"finish_reason":"stop"}],"usage":{"prompt_tokens":29,"completion_tokens":10,"total_tokens":39}} ' - recorded_at: Thu, 03 Jul 2025 01:21:14 GMT + recorded_at: Sat, 19 Jul 2025 19:53:45 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_openai_gpt-4_1-nano_supports_response_format_param.yml similarity index 90% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_openai_gpt-4_1-nano_supports_response_format_param.yml index 77eb648c4..dbda29ece 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_openai_gpt-4_1-nano_supports_response_format_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_openai_gpt-4_1-nano_supports_response_format_param.yml @@ -24,7 +24,7 @@ http_interactions: message: OK headers: Date: - - Thu, 03 Jul 2025 01:20:59 GMT + - Sat, 19 Jul 2025 19:53:31 GMT Content-Type: - application/json Transfer-Encoding: @@ -36,11 +36,13 @@ http_interactions: Openai-Organization: - "" Openai-Processing-Ms: - - '160' + - '148' + Openai-Project: + - proj_Cgr91oaB3FCyMSthpusv7eJc Openai-Version: - '2020-10-01' X-Envoy-Upstream-Service-Time: - - '162' + - '150' X-Ratelimit-Limit-Requests: - '500' X-Ratelimit-Limit-Tokens: @@ -74,9 +76,9 @@ http_interactions: encoding: ASCII-8BIT string: | { - "id": "chatcmpl-Bp2vDJ6we2xCrUhBondp1eCMg6VAE", + "id": "chatcmpl-Bv7ud7hg2U3FxalKcWA4qjZTTRsfk", "object": "chat.completion", - "created": 1751505659, + "created": 1752954811, "model": "gpt-4.1-nano-2025-04-14", "choices": [ { @@ -107,7 +109,7 @@ http_interactions: } }, "service_tier": "default", - "system_fingerprint": "fp_38343a2f8f" + "system_fingerprint": null } - recorded_at: Thu, 03 Jul 2025 01:20:59 GMT + recorded_at: Sat, 19 Jul 2025 19:53:31 GMT recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml b/spec/fixtures/vcr_cassettes/chat_with_params_openrouter_anthropic_claude-3_5-haiku_supports_top_k_param.yml similarity index 61% rename from spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml rename to spec/fixtures/vcr_cassettes/chat_with_params_openrouter_anthropic_claude-3_5-haiku_supports_top_k_param.yml index 58c59be35..3a0de14da 100644 --- a/spec/fixtures/vcr_cassettes/chat_with_request_options_openrouter_anthropic_claude-3_5-haiku_supports_top_k_option.yml +++ b/spec/fixtures/vcr_cassettes/chat_with_params_openrouter_anthropic_claude-3_5-haiku_supports_top_k_param.yml @@ -24,7 +24,7 @@ http_interactions: message: OK headers: Date: - - Thu, 03 Jul 2025 02:35:13 GMT + - Sat, 19 Jul 2025 19:53:48 GMT Content-Type: - application/json Transfer-Encoding: @@ -35,13 +35,20 @@ http_interactions: - "*" Vary: - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff Server: - cloudflare Cf-Ray: - "" body: encoding: ASCII-8BIT - string: "\n \n\n \n\n \n{\"id\":\"gen-1751510112-xLg5OxX2KgL0qJOHgQAo\",\"provider\":\"Anthropic\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1751510112,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\\n + string: "\n \n{\"id\":\"gen-1752954827-41LuhLq98dKxehFNZkEa\",\"provider\":\"Anthropic\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1752954827,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\\n \ \\\"result\\\": 8\\n}\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":28,\"completion_tokens\":12,\"total_tokens\":40}}" - recorded_at: Thu, 03 Jul 2025 02:35:13 GMT + recorded_at: Sat, 19 Jul 2025 19:53:48 GMT recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_request_options_spec.rb b/spec/ruby_llm/chat_request_options_spec.rb index 02a05b560..07a3f87b2 100644 --- a/spec/ruby_llm/chat_request_options_spec.rb +++ b/spec/ruby_llm/chat_request_options_spec.rb @@ -5,8 +5,8 @@ RSpec.describe RubyLLM::Chat do include_context 'with configured RubyLLM' - describe 'with request_options' do - # Supported request_options vary by provider, and to lesser degree, by model. + describe 'with params' do + # Supported params vary by provider, and to lesser degree, by model. # Providers [:openai, :ollama, :deepseek] support {response_format: {type: 'json_object'}} # to guarantee a JSON object is returned. @@ -14,10 +14,10 @@ CHAT_MODELS.select { |model_info| %i[openai ollama deepseek].include?(model_info[:provider]) }.each do |model_info| model = model_info[:model] provider = model_info[:provider] - it "#{provider}/#{model} supports response_format option" do # rubocop:disable RSpec/ExampleLength + it "#{provider}/#{model} supports response_format param" do # rubocop:disable RSpec/ExampleLength chat = RubyLLM .chat(model: model, provider: provider) - .with_request_options(response_format: { type: 'json_object' }) + .with_params(response_format: { type: 'json_object' }) response = chat.ask('What is the square root of 64? Answer with a JSON object with the key `result`.') @@ -26,15 +26,15 @@ end end - # Provider [:gemini] supports a {generationConfig: {responseMimeType: ..., responseSchema: ...} } option, - # which can specify a JSON schema, requiring a deep_merge of request_options into the payload. + # Provider [:gemini] supports a {generationConfig: {responseMimeType: ..., responseSchema: ...} } param, + # which can specify a JSON schema, requiring a deep_merge of params into the payload. CHAT_MODELS.select { |model_info| model_info[:provider] == :gemini }.each do |model_info| model = model_info[:model] provider = model_info[:provider] - it "#{provider}/#{model} supports responseSchema option" do # rubocop:disable RSpec/ExampleLength + it "#{provider}/#{model} supports responseSchema param" do # rubocop:disable RSpec/ExampleLength chat = RubyLLM .chat(model: model, provider: provider) - .with_request_options( + .with_params( generationConfig: { responseMimeType: 'application/json', responseSchema: { @@ -51,14 +51,14 @@ end end - # Provider [:anthropic] supports a service_tier option. + # Provider [:anthropic] supports a service_tier param. CHAT_MODELS.select { |model_info| model_info[:provider] == :anthropic }.each do |model_info| model = model_info[:model] provider = model_info[:provider] - it "#{provider}/#{model} supports service_tier option" do # rubocop:disable RSpec/ExampleLength + it "#{provider}/#{model} supports service_tier param" do # rubocop:disable RSpec/ExampleLength chat = RubyLLM .chat(model: model, provider: provider) - .with_request_options(service_tier: 'standard_only') + .with_params(service_tier: 'standard_only') chat.add_message( role: :user, @@ -80,14 +80,14 @@ end end - # Providers [:openrouter, :bedrock] supports a {top_k: ...} option to remove low-probability next tokens. + # Providers [:openrouter, :bedrock] supports a {top_k: ...} param to remove low-probability next tokens. CHAT_MODELS.select { |model_info| %i[openrouter bedrock].include?(model_info[:provider]) }.each do |model_info| model = model_info[:model] provider = model_info[:provider] - it "#{provider}/#{model} supports top_k option" do # rubocop:disable RSpec/ExampleLength + it "#{provider}/#{model} supports top_k param" do # rubocop:disable RSpec/ExampleLength chat = RubyLLM .chat(model: model, provider: provider) - .with_request_options(top_k: 5) + .with_params(top_k: 5) chat.add_message( role: :user,