diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8a5f6d..4d99a68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [8.2.0] - 2025-08-10 + +### Added + +- Add Security.md and activate private vulnerability reporting +- Add RealTime endpoint to create WebRTC token - thank you to [@ngelx](https://github.com/ngelx) for the PR and others for input! +- Add multi-image upload - thank you to [@ryankon](https://github.com/ryankon) and others for requesting. +- Refactor streaming so that Chat, Responses, Assistant Runs and any others where events are streamed now send the event to the Proc, replacing unused _bytesize. Search the README for `_event` to see how to use this. Important change implemented by [@ingemar](https://github.com/ingemar)! +- Handle OpenAI::Files request parameters - thank you to [@okorepanov](https://github.com/okorepanov) for the PR. +- Add Gemini docs - thanks to [@francis](https://github.com/francis). +- Add web proxy debugging docs - thanks to [@cpb](https://github.com/cpb). +- Add Rails / ActiveStorage transcription docs - thanks to [@AndreyAzimov](https://github.com/AndreyAzimov). + ## [8.1.0] - 2025-03-30 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index 2937d65a..2b19c222 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ruby-openai (8.1.0) + ruby-openai (8.2.0) event_stream_parser (>= 0.3.0, < 2.0.0) faraday (>= 1) faraday-multipart (>= 1) diff --git a/README.md b/README.md index 8e63a8c5..055bb0ce 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Use the [OpenAI API](https://openai.com/blog/openai-api/) with Ruby! 🤖❤️ -Stream chats with the Responses API, transcribe and translate audio with Whisper, create images with DALL·E, and much more... +Stream GPT-5 chats with the Responses API, initiate Realtime WebRTC conversations, and much more... **Sponsors** @@ -540,11 +540,14 @@ You can stream it as well! ```ruby response = client.responses.create(parameters: { - model: "gpt-4o", - input: "Hello! I'm Szymon!" + model: "gpt-5", + input: "Hello! I'm Szymon!", + reasoning: { + "effort": "minimal" + } }) puts response.dig("output", 0, "content", 0, "text") -# => Hello Szymon! How can I assist you today? +# => Hi Szymon! Great to meet you. How can I help today? ``` #### Follow-up Messages @@ -682,8 +685,7 @@ response = message = response.dig("choices", 0, "message") if message["role"] == "assistant" && message["tool_calls"] - - # For a subsequent message with the role "tool", OpenAI requires the preceding message to have a tool_calls argument. + # For a subsequent message with the role "tool", OpenAI requires the preceding message to have a single tool_calls argument. messages << message message["tool_calls"].each do |tool_call| @@ -1681,7 +1683,7 @@ user.media.blob.open do |file| response = client.audio.transcribe( parameters: { model: "whisper-1", - file: File.open(temp_file, "rb"), + file: File.open(file, "rb"), language: "en" # Optional }) puts response["text"] diff --git a/SECURITY.md b/SECURITY.md index d78e12f2..305f27c7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,5 @@ # Security Policy + Thank you for helping us keep ruby-openai and any systems it interacts with secure. ## Reporting Security Issues diff --git a/lib/openai/version.rb b/lib/openai/version.rb index 878f3056..348b04df 100644 --- a/lib/openai/version.rb +++ b/lib/openai/version.rb @@ -1,3 +1,3 @@ module OpenAI - VERSION = "8.1.0".freeze + VERSION = "8.2.0".freeze end diff --git a/spec/fixtures/cassettes/gpt-3_5-turbo_multiple_tool_calls_full_conversation.yml b/spec/fixtures/cassettes/gpt-3_5-turbo_multiple_tool_calls_full_conversation.yml new file mode 100644 index 00000000..4b20ed74 --- /dev/null +++ b/spec/fixtures/cassettes/gpt-3_5-turbo_multiple_tool_calls_full_conversation.yml @@ -0,0 +1,257 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-3.5-turbo","messages":[{"role":"user","content":"What + is the weather like in San Francisco and Japan?"}],"stream":false,"tools":[{"type":"function","function":{"name":"get_current_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + geographic location to get the weather for"}},"required":["location"]}}}],"tool_choice":"required"}' + headers: + Content-Type: + - application/json + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Sun, 10 Aug 2025 14:27:17 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - user-jxm65ijkzc1qrfhc0ij8moic + Openai-Processing-Ms: + - '1095' + Openai-Project: + - proj_GeQgevTU0ldCrlkkXei9bV0k + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '1308' + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '50000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '49999984' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_076e19be0a37445683b86c1790c391a2 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=.FEh2Ialk3iUIgn5xob9.0rOOX_RATuK3FLmk5CdPIQ-1754836037-1.0.1.1-q6orBCU0W1IN91SW_J.ekybHP27lVWCbiew3.EdZF3N3hby30R2ka_xLaMeX.1ay5eXFxr9YF8uPGxXeMyCL1VL4D9_.kV5muZXqhwvSh1w; + path=/; expires=Sun, 10-Aug-25 14:57:17 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=KIoEfW0H_xRTApddy_kiIA2u9H6_f4zjsFA.VjdZuCs-1754836037356-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 96d02c467b1ad77e-LHR + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-C31IySwlUa8hdaSKKxS7JazofVfCy", + "object": "chat.completion", + "created": 1754836036, + "model": "gpt-3.5-turbo-0125", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_jpE40AW1quMaiDYWNV4Aob6D", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\"location\": \"San Francisco\"}" + } + }, + { + "id": "call_AZ51gEeXi37kKhFpXrcJzNQl", + "type": "function", + "function": { + "name": "get_current_weather", + "arguments": "{\"location\": \"Japan\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 70, + "completion_tokens": 46, + "total_tokens": 116, + "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": null + } + recorded_at: Sun, 10 Aug 2025 14:27:17 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: "{\"model\":\"gpt-3.5-turbo\",\"messages\":[{\"role\":\"user\",\"content\":\"What + is the weather like in San Francisco and Japan?\"},{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_jpE40AW1quMaiDYWNV4Aob6D\",\"type\":\"function\",\"function\":{\"name\":\"get_current_weather\",\"arguments\":\"{\\\"location\\\": + \\\"San Francisco\\\"}\"}},{\"id\":\"call_AZ51gEeXi37kKhFpXrcJzNQl\",\"type\":\"function\",\"function\":{\"name\":\"get_current_weather\",\"arguments\":\"{\\\"location\\\": + \\\"Japan\\\"}\"}}],\"refusal\":null,\"annotations\":[]},{\"tool_call_id\":\"call_jpE40AW1quMaiDYWNV4Aob6D\",\"role\":\"tool\",\"name\":\"get_current_weather\",\"content\":\"The + weather is nice \U0001F31E\"},{\"tool_call_id\":\"call_AZ51gEeXi37kKhFpXrcJzNQl\",\"role\":\"tool\",\"name\":\"get_current_weather\",\"content\":\"The + weather is nice \U0001F31E\"}]}" + headers: + Content-Type: + - application/json + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Sun, 10 Aug 2025 14:27:18 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - user-jxm65ijkzc1qrfhc0ij8moic + Openai-Processing-Ms: + - '437' + Openai-Project: + - proj_GeQgevTU0ldCrlkkXei9bV0k + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '490' + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '50000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '49999970' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_1619ffde9bc04acd9c6c0622c5ef77ea + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=h__9zuzi47aStP0G9GWXRgK_HjLqYgaEftrdn3pmOSk-1754836038-1.0.1.1-xJ.MZLdLbvZtZv7heRK9ExGsEzkHRq4MWszDAXx92cylOgIH0mRg63kLiLiVX5iEefz.eMVfATrHxt6yfnqdR_HQkQbIL9zNys22PpLqWOY; + path=/; expires=Sun, 10-Aug-25 14:57:18 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=JWTQlkdLGwWXR20DFmzadXtZM24JFn8oWClYIRtK25c-1754836038242-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 96d02c52a9de60ef-LHR + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-C31IzRF7qzHC7AOBlp2K1Fed9HWzf", + "object": "chat.completion", + "created": 1754836037, + "model": "gpt-3.5-turbo-0125", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The weather in both San Francisco and Japan is nice.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 104, + "completion_tokens": 11, + "total_tokens": 115, + "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": null + } + recorded_at: Sun, 10 Aug 2025 14:27:18 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/cassettes/moderations_i_m_worried_about_that_.yml b/spec/fixtures/cassettes/moderations_have_a_great_day_.yml similarity index 53% rename from spec/fixtures/cassettes/moderations_i_m_worried_about_that_.yml rename to spec/fixtures/cassettes/moderations_have_a_great_day_.yml index 77973864..721842a8 100644 --- a/spec/fixtures/cassettes/moderations_i_m_worried_about_that_.yml +++ b/spec/fixtures/cassettes/moderations_have_a_great_day_.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.openai.com/v1/moderations body: encoding: UTF-8 - string: '{"input":"I''m worried about that."}' + string: '{"input":"Have a great day!"}' headers: Content-Type: - application/json @@ -23,7 +23,7 @@ http_interactions: message: OK headers: Date: - - Tue, 14 Nov 2023 21:49:25 GMT + - Sun, 10 Aug 2025 15:41:14 GMT Content-Type: - application/json Transfer-Encoding: @@ -34,32 +34,38 @@ http_interactions: - '2020-10-01' Openai-Organization: - user-jxm65ijkzc1qrfhc0ij8moic + Openai-Project: + - proj_GeQgevTU0ldCrlkkXei9bV0k X-Request-Id: - - dbfdc68c03b807f9032ae4a787a2f0ec + - req_21f1452b682f630a9597022bf6b2874d Openai-Processing-Ms: - - '352' + - '193' + X-Envoy-Upstream-Service-Time: + - '198' Strict-Transport-Security: - - max-age=15724800; includeSubDomains + - max-age=31536000; includeSubDomains; preload Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=YW3LEBzgVbb3zRPY73g0QJbiFNRc.Ito1h13MA3eO84-1699998565-0-Aa/bvt/ZUgA8lO/hJC/HovSo+2G5cLKfBuDxOKrdpjo18KdlmUAexordUYA6wRrQ9sjrJgx6HJs3D5EpQD622p0=; - path=/; expires=Tue, 14-Nov-23 22:19:25 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=NrvT3z3B.tpG2tEhVs3pFoKKV4VozwzWLIV.zmOCwsM-1754840474-1.0.1.1-xi_dp0c5yfjX4iCmhStYewpo6nf24MjXPcJtY81cJ8E_Rl341BoifvO1uItnQcwsOfn6w8Jq9dix1mpUwok0HkNaUO4GsvWKHpfTJ5V6XtQ; + path=/; expires=Sun, 10-Aug-25 16:11:14 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=ObFB3Cje.0H2uPyh5bobPS_MqZqkf5o2hB6ODhOj1ac-1699998565928-0-604800000; + - _cfuvid=lwY5MgJ2WZpI94ihnovGeEuSHYzxIuIlQw8X7JXsT2k-1754840474962-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + X-Content-Type-Options: + - nosniff Server: - cloudflare Cf-Ray: - - 826276da0c496442-LHR + - 96d098a66860640f-LHR Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "modr-8KvZdADkUU6dO1BlY2zTIhrDpmc9M", - "model": "text-moderation-006", + "id": "modr-C32SYHp4XLB0KRtDcKtdRbiP8kSTM", + "model": "text-moderation-007", "results": [ { "flagged": false, @@ -77,20 +83,20 @@ http_interactions: "violence": false }, "category_scores": { - "sexual": 0.00003175064193783328, - "hate": 3.4778734061546857e-6, - "harassment": 3.6136987091595074e-6, - "self-harm": 1.2846102208641241e-6, - "sexual/minors": 1.117846522902255e-6, - "hate/threatening": 3.989105934465442e-9, - "violence/graphic": 3.398185697278677e-7, - "self-harm/intent": 2.000100209897937e-7, - "self-harm/instructions": 3.7548302245227205e-9, - "harassment/threatening": 8.500402515210226e-8, - "violence": 0.000021952317183604464 + "sexual": 0.000030256842364906333, + "hate": 7.682696434585523e-8, + "harassment": 2.379463239776669e-6, + "self-harm": 5.0034135057330786e-8, + "sexual/minors": 2.92258562240022e-7, + "hate/threatening": 1.4745440424235312e-9, + "violence/graphic": 2.9348245789151406e-7, + "self-harm/intent": 6.321590717561776e-8, + "self-harm/instructions": 8.927665930968942e-7, + "harassment/threatening": 3.908326959844999e-7, + "violence": 0.00001449603951186873 } } ] } - recorded_at: Tue, 14 Nov 2023 21:49:25 GMT -recorded_with: VCR 6.1.0 + recorded_at: Sun, 10 Aug 2025 15:41:15 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/openai/client/chat_spec.rb b/spec/openai/client/chat_spec.rb index 9a3f7fda..ca58071a 100644 --- a/spec/openai/client/chat_spec.rb +++ b/spec/openai/client/chat_spec.rb @@ -77,6 +77,55 @@ end end end + + context "with multiple tool calls" do + let(:cassette) { "#{model} multiple tool calls full conversation".downcase } + let(:messages) do + [ + { + "role" => "user", + "content" => "What is the weather like in San Francisco and Japan?" + } + ] + end + let(:parameters) do + { + model: model, + messages: messages, + stream: stream, + tools: tools, + tool_choice: "required" + } + end + + it "handles full conversation with multiple tool calls" do + VCR.use_cassette(cassette) do + message = response.dig("choices", 0, "message") + + if message["role"] == "assistant" && message["tool_calls"] + messages << message + + message["tool_calls"].each do |tool_call| + messages << { + tool_call_id: tool_call["id"], + role: "tool", + name: "get_current_weather", + content: "The weather is nice 🌞" + } + end + + second_response = OpenAI::Client.new.chat( + parameters: { + model: model, + messages: messages + } + ) + + expect(second_response["error"]).to be_nil + end + end + end + end end describe "streaming" do diff --git a/spec/openai/client/files_spec.rb b/spec/openai/client/files_spec.rb index d793494a..da0f7747 100644 --- a/spec/openai/client/files_spec.rb +++ b/spec/openai/client/files_spec.rb @@ -85,7 +85,7 @@ describe "#retrieve" do let(:cassette) { "files retrieve" } let(:upload_cassette) { "#{cassette} upload" } - let(:response) { OpenAI::Client.new.files.retrieve(id: upload_id) } + let(:response) { OpenAI::Client.new.files.retrieve(id: upload_id, parameters: {}) } it "succeeds" do VCR.use_cassette(cassette) do @@ -97,7 +97,7 @@ describe "#content" do let(:cassette) { "files content" } let(:upload_cassette) { "#{cassette} upload" } - let(:response) { OpenAI::Client.new.files.content(id: upload_id) } + let(:response) { OpenAI::Client.new.files.content(id: upload_id, parameters: {}) } it "succeeds" do VCR.use_cassette(cassette) do diff --git a/spec/openai/client/http_spec.rb b/spec/openai/client/http_spec.rb index 7995e2d2..471289f4 100644 --- a/spec/openai/client/http_spec.rb +++ b/spec/openai/client/http_spec.rb @@ -201,10 +201,12 @@ let(:headers) { OpenAI::Client.new.send(:headers) } - it { - expect(headers).to eq({ "Authorization" => "Bearer #{OpenAI.configuration.access_token}", - "Content-Type" => "application/json" }) - } + it "includes expected headers" do + expect(headers).to have_key("Authorization") + expect(headers["Authorization"]).to match(/^Bearer .+/) + expect(headers).to have_key("Content-Type") + expect(headers["Content-Type"]).to eq("application/json") + end describe "with Azure" do before do @@ -217,10 +219,12 @@ let(:headers) { OpenAI::Client.new.send(:headers) } - it { - expect(headers).to eq({ "Content-Type" => "application/json", - "api-key" => OpenAI.configuration.access_token }) - } + it "includes expected headers" do + expect(headers).to have_key("api-key") + expect(headers["api-key"]).not_to be_nil + expect(headers).to have_key("Content-Type") + expect(headers["Content-Type"]).to eq("application/json") + end end end diff --git a/spec/openai/client/moderations_spec.rb b/spec/openai/client/moderations_spec.rb index ae4f209a..bcb6b17e 100644 --- a/spec/openai/client/moderations_spec.rb +++ b/spec/openai/client/moderations_spec.rb @@ -1,6 +1,6 @@ RSpec.describe OpenAI::Client do describe "#moderations", :vcr do - let(:input) { "I'm worried about that." } + let(:input) { "Have a great day!" } let(:cassette) { "moderations #{input}".downcase } let(:response) do OpenAI::Client.new.moderations(