From fd84c914a103904c2fdbb3f233ddf86c5c7006bb Mon Sep 17 00:00:00 2001 From: Anton Kuzmenko <1917237+default-anton@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:57:48 -0800 Subject: [PATCH] feat: add completion_tokens_details to the response --- lib/onellm/response.rb | 25 +++- .../returns_completion_tokens_details.yml | 114 ++++++++++++++++++ spec/providers/openai_provider_spec.rb | 13 ++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/vcr_cassettes/Onellm_OpenAIProvider/_complete/validation_errors/returns_completion_tokens_details.yml diff --git a/lib/onellm/response.rb b/lib/onellm/response.rb index 4408cc9..de752b1 100644 --- a/lib/onellm/response.rb +++ b/lib/onellm/response.rb @@ -162,6 +162,25 @@ def to_h end end + # Represents completion tokens details + class CompletionTokensDetails + attr_reader :reasoning_tokens, :accepted_prediction_tokens, :rejected_prediction_tokens + + def initialize(attributes = {}) + @reasoning_tokens = attributes[:reasoning_tokens] + @accepted_prediction_tokens = attributes[:accepted_prediction_tokens] + @rejected_prediction_tokens = attributes[:rejected_prediction_tokens] + end + + def to_h + { + reasoning_tokens: reasoning_tokens, + accepted_prediction_tokens: accepted_prediction_tokens, + rejected_prediction_tokens: rejected_prediction_tokens + } + end + end + # Represents the token usage information class Usage attr_reader :completion_tokens, :prompt_tokens, :total_tokens, @@ -172,7 +191,9 @@ def initialize(attributes = {}) @completion_tokens = attributes[:completion_tokens] @prompt_tokens = attributes[:prompt_tokens] @total_tokens = attributes[:total_tokens] - @completion_tokens_details = attributes[:completion_tokens_details] + @completion_tokens_details = if attributes[:completion_tokens_details] + CompletionTokensDetails.new(attributes[:completion_tokens_details]) + end @prompt_tokens_details = attributes[:prompt_tokens_details] || {} @cache_creation_input_tokens = attributes[:cache_creation_input_tokens] @cache_read_input_tokens = attributes[:cache_read_input_tokens] @@ -183,7 +204,7 @@ def to_h completion_tokens: completion_tokens, prompt_tokens: prompt_tokens, total_tokens: total_tokens, - completion_tokens_details: completion_tokens_details, + completion_tokens_details: completion_tokens_details&.to_h, prompt_tokens_details: prompt_tokens_details, cache_creation_input_tokens: cache_creation_input_tokens, cache_read_input_tokens: cache_read_input_tokens diff --git a/spec/fixtures/vcr_cassettes/Onellm_OpenAIProvider/_complete/validation_errors/returns_completion_tokens_details.yml b/spec/fixtures/vcr_cassettes/Onellm_OpenAIProvider/_complete/validation_errors/returns_completion_tokens_details.yml new file mode 100644 index 0000000..05a3a0d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Onellm_OpenAIProvider/_complete/validation_errors/returns_completion_tokens_details.yml @@ -0,0 +1,114 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello, + how are you?"}],"stream":false,"frequency_penalty":0,"presence_penalty":0,"top_p":1,"temperature":1,"logprobs":false}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Ruby + Host: + - api.openai.com + Content-Type: + - application/json + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Date: + - Thu, 02 Jan 2025 06:57:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - aha-labs-inc + Openai-Processing-Ms: + - '899' + Openai-Version: + - '2020-10-01' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999978' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_2b7aa062ceb35a9ea074eaf1500ca0d4 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=EGhGMGk_0x5hI.Blo3jNe7e70yc9LrOy8p4YdSphaMY-1735801027-1.0.1.1-MHNXCFUpR4.d29eM7VVVrFWGlhXce7UkHP53mB8Mt18VJ8mI0tZpUN4pQfgZ2FEjp9alC6oHmUiue47g0dvOuw; + path=/; expires=Thu, 02-Jan-25 07:27:07 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=_B4CQqRLC_v7gP9NKDGqBBhW95lTEUVRqEg_JGOlKEU-1735801027326-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 8fb8da5e2be18447-YVR + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-Al9Qg4oIRnt81lJfevuEIZHt2F0fr", + "object": "chat.completion", + "created": 1735801026, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! I'm just a program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?", + "refusal": null + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 13, + "completion_tokens": 30, + "total_tokens": 43, + "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 + } + }, + "system_fingerprint": "fp_0aa8d3e20b" + } + recorded_at: Thu, 02 Jan 2025 06:57:07 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/providers/openai_provider_spec.rb b/spec/providers/openai_provider_spec.rb index 873f130..32659e2 100644 --- a/spec/providers/openai_provider_spec.rb +++ b/spec/providers/openai_provider_spec.rb @@ -391,6 +391,19 @@ expect(response.choices.first.logprobs.content.first.top_logprobs).to be_an(Array) end + it 'returns completion tokens details', :vcr do + response = provider.complete( + model: valid_model, + messages: valid_messages + ) + + expect(response).to be_a(Onellm::Response) + expect(response.usage.completion_tokens_details).to be_a(Onellm::CompletionTokensDetails) + expect(response.usage.completion_tokens_details.reasoning_tokens).to be_a(Integer) + expect(response.usage.completion_tokens_details.accepted_prediction_tokens).to be_a(Integer) + expect(response.usage.completion_tokens_details.rejected_prediction_tokens).to be_a(Integer) + end + it 'raises error for invalid top_p' do expect do provider.complete(