Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ PATH
base64
cgi
connection_pool (>= 2.2.3)
logger

GEM
remote: https://rubygems.org/
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,65 @@ Error codes are as follows:
| Timeout | `APITimeoutError` |
| Network error | `APIConnectionError` |

### Request logging

Request logging is disabled by default. Enable it with a standard Ruby logger
and an explicit log level:

```ruby
client = OpenAI::Client.new(
api_key: ENV.fetch("OPENAI_API_KEY"),
logger: Rails.logger,
log_level: :info
)
```

The logger can be any object that responds to `debug`, `info`, `warn`, and
`error`; the SDK does not depend on Rails. Supplying a logger does not enable
logging by itself. You can also set `OPENAI_LOG=info` or `OPENAI_LOG=debug`.
An explicit `log_level` takes precedence over the environment variable. When
logging is enabled without a custom logger, the SDK uses a standard-library
`Logger` that writes to stderr.

For example, to use the stderr logger for one process:

```sh
OPENAI_LOG=info bundle exec ruby app.rb
```

A completion message includes the logical request and retry context:

```text
[openai] request complete log_id=log_a1b2c3d4e5f6 method=POST path=/v1/responses status=200 request_id=req_123 attempts=1 duration_ms=42.7
```

| Level | Behavior |
| --- | --- |
| `:off` | No SDK request logs (default) |
| `:error` | Terminal request failures after retries are exhausted |
| `:warn` | Error events plus retry reason and delay |
| `:info` | Safe request completion summaries |
| `:debug` | Per-attempt headers and bounded body diagnostics |

Info, warning, and error logs include operational fields such as the HTTP
method, sanitized path, status, request ID, duration, and attempt count. They
never include headers or bodies. Debug logs redact credential-bearing headers
and query parameters, including authorization, API-key, cookie, token,
credential, and signature values.

Debug logging can still disclose sensitive prompts, model responses, and tool
arguments. Do not enable it in production unless your log destination and data
retention policy are appropriate. The built-in logger omits uploaded file
contents, multipart bodies, binary bodies, large opaque/base64-like values, and
server-sent event contents. Text bodies are truncated to a fixed bound;
oversized JSON and incomplete bodies are marked as omitted. Response bodies are
observed only as the application consumes them and are never read eagerly for
logging.

SDK log messages are intended for human diagnostics. Their text format is not
a stable structured-event API and may change between releases. Exceptions from
a supplied logger are isolated and never replace an API result or API error.

### Request IDs

OpenAI recommends logging request IDs in production so requests can be traced
Expand Down
7 changes: 6 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ multitask(:"format:rbs") do
sed = xargs + [sed_bin, "-E", *inplace, "-e"]
# annotate unprocessable aliases with a unique comment
pre = sed + ["s/(class|module) ([^ ]+) = (.+$)/# \\1 #{uuid}\\n\\2: \\3/", "--"]
fmt = xargs + %w[stree write --plugin=rbs --]
fmt = xargs + %w[
stree write --plugin=rbs
--ignore-files=sig/openai/internal/transport/base_client.rbs
--ignore-files=./sig/openai/internal/transport/base_client.rbs
--
]
# remove the unique comment and unprocessable aliases to type aliases
subst = <<~SED
s/# (class|module) #{uuid}/\\1/
Expand Down
2 changes: 2 additions & 0 deletions lib/openai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "erb"
require "etc"
require "json"
require "logger"
require "net/http"
require "openssl"
require "pathname"
Expand Down Expand Up @@ -56,6 +57,7 @@
require_relative "openai/http_client"
require_relative "openai/raw_response"
require_relative "openai/net_http_client"
require_relative "openai/internal/logging"
require_relative "openai/provider"
require_relative "openai/internal/provider"
require_relative "openai/providers/azure"
Expand Down
26 changes: 20 additions & 6 deletions lib/openai/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class Client < OpenAI::Internal::Transport::BaseClient
end

# @api private
private def send_request(request, redirect_count:, retry_count:, send_retry_header:)
def send_request(request, redirect_count:, retry_count:, send_retry_header:, log_context:)
return super unless @workload_identity_auth

workload_identity_auth_header = "Bearer #{WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER}"
Expand All @@ -186,7 +186,8 @@ class Client < OpenAI::Internal::Transport::BaseClient
updated_request,
redirect_count: redirect_count,
retry_count: retry_count,
send_retry_header: send_retry_header
send_retry_header: send_retry_header,
log_context: log_context
)
rescue OpenAI::Errors::AuthenticationError
raise unless retry_count.zero? && request_replayable?(request)
Expand All @@ -200,7 +201,8 @@ class Client < OpenAI::Internal::Transport::BaseClient
refreshed_request,
redirect_count: redirect_count,
retry_count: retry_count + 1,
send_retry_header: send_retry_header
send_retry_header: send_retry_header,
log_context: log_context
)
end
end
Expand Down Expand Up @@ -231,14 +233,21 @@ class Client < OpenAI::Internal::Transport::BaseClient
#
# @param max_retries [Integer] Max number of retries to attempt after a failed retryable request.
#
# @param timeout [Float]
# @param timeout [Float, nil]
#
# @param initial_retry_delay [Float]
#
# @param max_retry_delay [Float]
#
# @param http_client [#execute, nil] The HTTP client used to
# execute SDK requests. Defaults to {OpenAI::NetHTTPClient}.
#
# @param logger [#debug, #info, #warn, #error, nil] Logger used for SDK
# request diagnostics. Logging is disabled unless `log_level` or
# `OPENAI_LOG` enables it.
#
# @param log_level [Symbol, String] One of `:off`, `:error`, `:warn`,
# `:info`, or `:debug`. Defaults to `ENV["OPENAI_LOG"]`, then `:off`.
def initialize(
api_key: OpenAI::Internal::OMIT,
admin_api_key: OpenAI::Internal::OMIT,
Expand All @@ -252,7 +261,9 @@ def initialize(
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY,
http_client: nil
http_client: nil,
logger: nil,
log_level: OpenAI::Internal::OMIT
)
provider_runtime = nil
unless provider.nil?
Expand Down Expand Up @@ -285,6 +296,7 @@ def initialize(
project = ENV["OPENAI_PROJECT_ID"] if project.equal?(OpenAI::Internal::OMIT) && provider_runtime.nil?
webhook_secret = ENV["OPENAI_WEBHOOK_SECRET"] if webhook_secret.equal?(OpenAI::Internal::OMIT)
base_url = ENV["OPENAI_BASE_URL"] if base_url.equal?(OpenAI::Internal::OMIT) && provider_runtime.nil?
log_level = ENV.fetch("OPENAI_LOG", :off) if log_level.equal?(OpenAI::Internal::OMIT)

api_key = nil if api_key.equal?(OpenAI::Internal::OMIT)
admin_api_key = nil if admin_api_key.equal?(OpenAI::Internal::OMIT)
Expand Down Expand Up @@ -341,7 +353,9 @@ def initialize(
initial_retry_delay: initial_retry_delay,
max_retry_delay: max_retry_delay,
headers: headers,
http_client: http_client
http_client: http_client,
logger: logger,
log_level: log_level
)

@completions = OpenAI::Resources::Completions.new(client: self)
Expand Down
Loading