openai 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +241 -8
- data/SECURITY.md +6 -22
- data/azure.md +110 -0
- data/bedrock.md +132 -0
- data/examples/mtls_custom_http_client.rb +61 -0
- data/lib/openai/auth/workload_identity.rb +13 -2
- data/lib/openai/auth/workload_identity_auth.rb +2 -1
- data/lib/openai/client.rb +29 -6
- data/lib/openai/errors.rb +11 -1
- data/lib/openai/helpers/streaming/chat_completion_stream.rb +3 -3
- data/lib/openai/helpers/streaming/response_stream.rb +5 -6
- data/lib/openai/helpers/structured_output/base_model.rb +20 -19
- data/lib/openai/helpers/structured_output/json_schema_converter.rb +12 -5
- data/lib/openai/http_client.rb +99 -7
- data/lib/openai/internal/conversation_cursor_page.rb +5 -6
- data/lib/openai/internal/cursor_page.rb +3 -3
- data/lib/openai/internal/logging.rb +419 -0
- data/lib/openai/internal/next_cursor_page.rb +5 -6
- data/lib/openai/internal/page.rb +2 -2
- data/lib/openai/internal/provider.rb +7 -1
- data/lib/openai/internal/read_io_adapter.rb +42 -18
- data/lib/openai/internal/stream.rb +2 -2
- data/lib/openai/internal/transport/base_client.rb +137 -37
- data/lib/openai/internal/type/array_of.rb +6 -4
- data/lib/openai/internal/type/base_model.rb +39 -1
- data/lib/openai/internal/type/base_page.rb +19 -2
- data/lib/openai/internal/type/base_stream.rb +39 -7
- data/lib/openai/internal/type/hash_of.rb +3 -3
- data/lib/openai/internal/util.rb +52 -27
- data/lib/openai/models/admin/organization/audit_log_list_params.rb +3 -1
- data/lib/openai/models/admin/organization/audit_log_list_response.rb +33 -11
- data/lib/openai/models/admin/organization/groups/role_list_response.rb +5 -1
- data/lib/openai/models/admin/organization/groups/role_retrieve_response.rb +5 -1
- data/lib/openai/models/admin/organization/projects/groups/role_list_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/groups/role_retrieve_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/service_account_create_response.rb +3 -1
- data/lib/openai/models/admin/organization/projects/users/role_list_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/users/role_retrieve_response.rb +6 -1
- data/lib/openai/models/admin/organization/users/role_list_response.rb +5 -1
- data/lib/openai/models/admin/organization/users/role_retrieve_response.rb +5 -1
- data/lib/openai/models/all_models.rb +3 -0
- data/lib/openai/models/audio/transcription_create_params.rb +7 -1
- data/lib/openai/models/audio/translation_create_params.rb +7 -1
- data/lib/openai/models/beta/beta_response.rb +9 -0
- data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +28 -3
- data/lib/openai/models/beta/beta_responses_server_event.rb +1037 -61
- data/lib/openai/models/beta/response_compact_params.rb +9 -0
- data/lib/openai/models/beta/response_create_params.rb +9 -0
- data/lib/openai/models/containers/file_create_params.rb +4 -0
- data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
- data/lib/openai/models/file_create_params.rb +4 -0
- data/lib/openai/models/image_create_variation_params.rb +4 -0
- data/lib/openai/models/image_edit_params.rb +8 -0
- data/lib/openai/models/responses/response_compact_params.rb +9 -0
- data/lib/openai/models/responses/response_stream_event.rb +1 -1
- data/lib/openai/models/responses/responses_client_event.rb +13 -1
- data/lib/openai/models/responses/responses_server_event.rb +1027 -54
- data/lib/openai/models/responses_model.rb +3 -0
- data/lib/openai/models/skill_create_params.rb +4 -0
- data/lib/openai/models/skills/version_create_params.rb +4 -0
- data/lib/openai/models/uploads/part_create_params.rb +4 -0
- data/lib/openai/models/vector_store_search_response.rb +3 -1
- data/lib/openai/models/video_create_character_params.rb +4 -0
- data/lib/openai/models/video_create_params.rb +4 -0
- data/lib/openai/models/video_edit_params.rb +4 -0
- data/lib/openai/models/video_extend_params.rb +4 -0
- data/lib/openai/models.rb +9 -1
- data/lib/openai/net_http_client.rb +20 -6
- data/lib/openai/providers/azure.rb +216 -0
- data/lib/openai/providers/bedrock.rb +3 -1
- data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
- data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
- data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
- data/lib/openai/resources/admin/organization/projects.rb +4 -2
- data/lib/openai/resources/audio/transcriptions.rb +8 -0
- data/lib/openai/resources/audio/translations.rb +4 -0
- data/lib/openai/resources/beta/responses.rb +1 -1
- data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
- data/lib/openai/resources/containers/files.rb +4 -0
- data/lib/openai/resources/content_provenance_checks.rb +4 -0
- data/lib/openai/resources/conversations/items.rb +1 -1
- data/lib/openai/resources/evals/runs/output_items.rb +1 -1
- data/lib/openai/resources/files.rb +4 -0
- data/lib/openai/resources/images.rb +12 -0
- data/lib/openai/resources/responses.rb +3 -2
- data/lib/openai/resources/skills/versions.rb +4 -0
- data/lib/openai/resources/skills.rb +4 -0
- data/lib/openai/resources/uploads/parts.rb +4 -0
- data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
- data/lib/openai/resources/videos.rb +38 -0
- data/lib/openai/resources/webhooks.rb +18 -6
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +111 -107
- data/rbi/openai/auth.rbi +4 -4
- data/rbi/openai/client.rbi +13 -3
- data/rbi/openai/errors.rbi +7 -0
- data/rbi/openai/helpers/streaming/events.rbi +12 -0
- data/rbi/openai/http_client.rbi +62 -2
- data/rbi/openai/internal/logging.rbi +192 -0
- data/rbi/openai/internal/provider.rbi +3 -0
- data/rbi/openai/internal/read_io_adapter.rbi +6 -1
- data/rbi/openai/internal/transport/base_client.rbi +61 -7
- data/rbi/openai/internal/type/base_model.rbi +24 -1
- data/rbi/openai/internal/type/base_page.rbi +13 -2
- data/rbi/openai/internal/type/base_stream.rbi +15 -4
- data/rbi/openai/internal/util.rbi +26 -2
- data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
- data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
- data/rbi/openai/models/all_models.rbi +15 -0
- data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
- data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
- data/rbi/openai/models/beta/beta_response.rbi +15 -0
- data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +37 -1
- data/rbi/openai/models/beta/beta_responses_server_event.rbi +3791 -54
- data/rbi/openai/models/beta/response_compact_params.rbi +15 -0
- data/rbi/openai/models/beta/response_create_params.rbi +15 -0
- data/rbi/openai/models/containers/file_create_params.rbi +8 -0
- data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
- data/rbi/openai/models/file_create_params.rbi +8 -0
- data/rbi/openai/models/image_create_variation_params.rbi +8 -0
- data/rbi/openai/models/image_edit_params.rbi +16 -0
- data/rbi/openai/models/responses/response_compact_params.rbi +15 -0
- data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
- data/rbi/openai/models/responses/responses_client_event.rbi +19 -0
- data/rbi/openai/models/responses/responses_server_event.rbi +3282 -53
- data/rbi/openai/models/responses_model.rbi +15 -0
- data/rbi/openai/models/skill_create_params.rbi +8 -0
- data/rbi/openai/models/skills/version_create_params.rbi +8 -0
- data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
- data/rbi/openai/models/video_create_character_params.rbi +8 -0
- data/rbi/openai/models/video_create_params.rbi +8 -0
- data/rbi/openai/models/video_edit_params.rbi +8 -0
- data/rbi/openai/models/video_extend_params.rbi +8 -0
- data/rbi/openai/net_http_client.rbi +2 -2
- data/rbi/openai/providers.rbi +9 -0
- data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
- data/rbi/openai/resources/audio/translations.rbi +7 -1
- data/rbi/openai/resources/containers/files.rbi +4 -0
- data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
- data/rbi/openai/resources/files.rbi +4 -0
- data/rbi/openai/resources/images.rbi +20 -0
- data/rbi/openai/resources/skills/versions.rbi +4 -0
- data/rbi/openai/resources/skills.rbi +4 -0
- data/rbi/openai/resources/uploads/parts.rbi +4 -0
- data/rbi/openai/resources/videos.rbi +16 -0
- data/sig/openai/client.rbs +6 -2
- data/sig/openai/errors.rbs +5 -0
- data/sig/openai/http_client.rbs +37 -2
- data/sig/openai/internal/logging.rbs +88 -0
- data/sig/openai/internal/provider.rbs +3 -1
- data/sig/openai/internal/read_io_adapter.rbs +3 -1
- data/sig/openai/internal/transport/base_client.rbs +33 -5
- data/sig/openai/internal/type/base_model.rbs +13 -0
- data/sig/openai/internal/type/base_page.rbs +8 -1
- data/sig/openai/internal/type/base_stream.rbs +8 -2
- data/sig/openai/internal/util.rbs +12 -0
- data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
- data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
- data/sig/openai/models/all_models.rbs +6 -0
- data/sig/openai/models/beta/beta_response.rbs +6 -0
- data/sig/openai/models/beta/beta_responses_client_event.rbs +13 -0
- data/sig/openai/models/beta/beta_responses_server_event.rbs +1946 -54
- data/sig/openai/models/beta/response_compact_params.rbs +6 -0
- data/sig/openai/models/beta/response_create_params.rbs +6 -0
- data/sig/openai/models/responses/response_compact_params.rbs +6 -0
- data/sig/openai/models/responses/responses_client_event.rbs +7 -0
- data/sig/openai/models/responses/responses_server_event.rbs +1786 -53
- data/sig/openai/models/responses_model.rbs +6 -0
- data/sig/openai/net_http_client.rbs +2 -2
- data/sig/openai/providers.rbs +6 -0
- metadata +24 -1
|
@@ -32,7 +32,7 @@ module OpenAI
|
|
|
32
32
|
def next_page
|
|
33
33
|
unless next_page?
|
|
34
34
|
message = "No more pages available. Please check #next_page? before calling ##{__method__}"
|
|
35
|
-
raise
|
|
35
|
+
raise message
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
req = OpenAI::Internal::Util.deep_merge(@req, {query: {after: data&.last&.id}})
|
|
@@ -60,9 +60,9 @@ module OpenAI
|
|
|
60
60
|
#
|
|
61
61
|
# @param client [OpenAI::Internal::Transport::BaseClient]
|
|
62
62
|
# @param req [Hash{Symbol=>Object}]
|
|
63
|
-
# @param
|
|
63
|
+
# @param response_metadata [OpenAI::ResponseMetadata]
|
|
64
64
|
# @param page_data [Hash{Symbol=>Object}]
|
|
65
|
-
def initialize(client:, req:,
|
|
65
|
+
def initialize(client:, req:, response_metadata:, page_data:)
|
|
66
66
|
super
|
|
67
67
|
|
|
68
68
|
case page_data
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenAI
|
|
4
|
+
module Internal
|
|
5
|
+
# Request logging helpers shared by generated clients.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Logging
|
|
9
|
+
LOG_LEVELS = {off: 0, error: 1, warn: 2, info: 3, debug: 4}.freeze
|
|
10
|
+
REDACTED_HEADERS = %w[
|
|
11
|
+
api-key
|
|
12
|
+
authorization
|
|
13
|
+
cookie
|
|
14
|
+
proxy-authorization
|
|
15
|
+
set-cookie
|
|
16
|
+
x-amz-security-token
|
|
17
|
+
x-api-key
|
|
18
|
+
].freeze
|
|
19
|
+
MAX_BODY_BYTES = 16 * 1024
|
|
20
|
+
MAX_ARRAY_ITEMS = 100
|
|
21
|
+
OPAQUE_STRING_BYTES = 1_024
|
|
22
|
+
SENSITIVE_BODY_KEY = /(?:api[-_]?key|authorization|credential|password|secret|signature|token)/i
|
|
23
|
+
SENSITIVE_QUERY_KEY = /
|
|
24
|
+
(?:
|
|
25
|
+
(?:\A|[-_\[])(?:key|sig)
|
|
26
|
+
| (?-i:K)ey
|
|
27
|
+
| api[-_]?key
|
|
28
|
+
| authorization
|
|
29
|
+
| credentials?
|
|
30
|
+
| password
|
|
31
|
+
| secret
|
|
32
|
+
| signature
|
|
33
|
+
| token
|
|
34
|
+
)
|
|
35
|
+
(?:\[|\]|\z)
|
|
36
|
+
/ix
|
|
37
|
+
URL_HEADER_KEY = /(?:\A|[-_])(?:location|url|uri)\z|\A(?:link|refresh)\z/i
|
|
38
|
+
|
|
39
|
+
class Context
|
|
40
|
+
def initialize(logger:, log_level:, on_retry:, method:, url:)
|
|
41
|
+
@logger = logger
|
|
42
|
+
@log_level = log_level
|
|
43
|
+
@on_retry = on_retry
|
|
44
|
+
@id = nil
|
|
45
|
+
@method = method.to_s.upcase
|
|
46
|
+
@url = url
|
|
47
|
+
@started_at = OpenAI::Internal::Util.monotonic_secs
|
|
48
|
+
@attempt_started_at = @started_at
|
|
49
|
+
@attempts = 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def request_started(request, redirect_count:)
|
|
53
|
+
@attempts += 1
|
|
54
|
+
log(:debug) do
|
|
55
|
+
"[openai] request started log_id=#{id} attempt=#{@attempts} " \
|
|
56
|
+
"redirect=#{redirect_count} method=#{request.method.to_s.upcase} " \
|
|
57
|
+
"url=#{OpenAI::Internal::Logging.safe_url(request.url)} " \
|
|
58
|
+
"headers=#{OpenAI::Internal::Logging.format_headers(request.headers)} " \
|
|
59
|
+
"body=#{OpenAI::Internal::Logging.format_body(request.body, headers: request.headers)}"
|
|
60
|
+
end
|
|
61
|
+
@attempt_started_at = OpenAI::Internal::Util.monotonic_secs
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def response_received(response)
|
|
65
|
+
log(:debug) do
|
|
66
|
+
"[openai] response received log_id=#{id} attempt=#{@attempts} " \
|
|
67
|
+
"status=#{response.status} request_id=#{safe_field(response.headers['x-request-id'])} " \
|
|
68
|
+
"duration_ms=#{attempt_duration_ms} " \
|
|
69
|
+
"headers=#{OpenAI::Internal::Logging.format_headers(response.headers)}"
|
|
70
|
+
end
|
|
71
|
+
return response unless enabled?(:debug)
|
|
72
|
+
|
|
73
|
+
observed = ObservedBody.new(
|
|
74
|
+
body: response.body,
|
|
75
|
+
headers: response.headers,
|
|
76
|
+
context: self,
|
|
77
|
+
attempt: @attempts
|
|
78
|
+
)
|
|
79
|
+
OpenAI::HTTPClient::Response.new(
|
|
80
|
+
status: response.status,
|
|
81
|
+
headers: response.headers,
|
|
82
|
+
body: observed
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attempt_failed(error)
|
|
87
|
+
log(:debug) do
|
|
88
|
+
"[openai] request attempt failed log_id=#{id} attempt=#{@attempts} " \
|
|
89
|
+
"error=#{error.class} duration_ms=#{attempt_duration_ms}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def retry_scheduled(cause, delay:, response:, retry_count:, max_retries:)
|
|
94
|
+
reason = cause.is_a?(Integer) ? "status=#{cause}" : "error=#{cause.class}"
|
|
95
|
+
log(:warn) do
|
|
96
|
+
"[openai] request retry log_id=#{id} attempt=#{@attempts} " \
|
|
97
|
+
"#{reason} delay_seconds=#{delay}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
event = OpenAI::RetryEvent.new(
|
|
101
|
+
attempt: retry_count + 2,
|
|
102
|
+
max_attempts: max_retries + 1,
|
|
103
|
+
delay: delay,
|
|
104
|
+
response: response,
|
|
105
|
+
error: cause.is_a?(OpenAI::Errors::APIConnectionError) ? cause : nil
|
|
106
|
+
)
|
|
107
|
+
@on_retry&.call(event)
|
|
108
|
+
rescue StandardError
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def completed(response)
|
|
113
|
+
log(:info) do
|
|
114
|
+
"[openai] request complete log_id=#{id} method=#{@method} " \
|
|
115
|
+
"path=#{OpenAI::Internal::Logging.safe_path(@url)} " \
|
|
116
|
+
"status=#{response.status} request_id=#{safe_field(response.headers['x-request-id'])} " \
|
|
117
|
+
"attempts=#{@attempts} duration_ms=#{duration_ms}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def observe_stream(stream, response:)
|
|
122
|
+
return stream unless enabled?(:error)
|
|
123
|
+
|
|
124
|
+
stream.observe(context: self, response: response)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def request_failed(error)
|
|
128
|
+
status = error.is_a?(OpenAI::Errors::APIError) ? error.status : nil
|
|
129
|
+
request_id = error.is_a?(OpenAI::Errors::APIError) ? error.request_id : nil
|
|
130
|
+
log(:error) do
|
|
131
|
+
"[openai] request failed log_id=#{id} method=#{@method} " \
|
|
132
|
+
"path=#{OpenAI::Internal::Logging.safe_path(@url)} " \
|
|
133
|
+
"status=#{status || 'none'} request_id=#{safe_field(request_id)} " \
|
|
134
|
+
"error=#{error.class} attempts=#{@attempts} duration_ms=#{duration_ms}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def response_body(body, headers:, complete:, total_bytes:, attempt:)
|
|
139
|
+
log(:debug) do
|
|
140
|
+
formatted = OpenAI::Internal::Logging.format_observed_body(
|
|
141
|
+
body,
|
|
142
|
+
headers: headers,
|
|
143
|
+
complete: complete,
|
|
144
|
+
total_bytes: total_bytes
|
|
145
|
+
)
|
|
146
|
+
"[openai] response body log_id=#{id} " \
|
|
147
|
+
"attempt=#{attempt} duration_ms=#{duration_ms} body=#{formatted}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private def duration_ms
|
|
152
|
+
((OpenAI::Internal::Util.monotonic_secs - @started_at) * 1_000).round(2)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private def id = @id ||= "log_#{SecureRandom.hex(6)}"
|
|
156
|
+
|
|
157
|
+
private def attempt_duration_ms
|
|
158
|
+
((OpenAI::Internal::Util.monotonic_secs - @attempt_started_at) * 1_000).round(2)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private def enabled?(event_level)
|
|
162
|
+
!@logger.nil? && OpenAI::Internal::Logging.enabled?(@log_level, event_level)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private def safe_field(value) = OpenAI::Internal::Logging.safe_field(value)
|
|
166
|
+
|
|
167
|
+
private def log(event_level)
|
|
168
|
+
return unless enabled?(event_level)
|
|
169
|
+
|
|
170
|
+
message = yield
|
|
171
|
+
case event_level
|
|
172
|
+
in :debug
|
|
173
|
+
@logger.debug(message)
|
|
174
|
+
in :info
|
|
175
|
+
@logger.info(message)
|
|
176
|
+
in :warn
|
|
177
|
+
@logger.warn(message)
|
|
178
|
+
in :error
|
|
179
|
+
@logger.error(message)
|
|
180
|
+
end
|
|
181
|
+
rescue StandardError
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class ObservedBody
|
|
187
|
+
include Enumerable
|
|
188
|
+
|
|
189
|
+
def initialize(body:, headers:, context:, attempt:)
|
|
190
|
+
@body = body
|
|
191
|
+
@headers = headers
|
|
192
|
+
@context = context
|
|
193
|
+
@attempt = attempt
|
|
194
|
+
@captured = String.new(encoding: Encoding::BINARY)
|
|
195
|
+
@capture_body = OpenAI::Internal::Logging.capture_response_body?(headers)
|
|
196
|
+
@bytes = 0
|
|
197
|
+
@complete = false
|
|
198
|
+
@closed = false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def each
|
|
202
|
+
return enum_for unless block_given?
|
|
203
|
+
return if @closed
|
|
204
|
+
|
|
205
|
+
begin
|
|
206
|
+
@body.each do |chunk|
|
|
207
|
+
capture(chunk)
|
|
208
|
+
yield(chunk)
|
|
209
|
+
end
|
|
210
|
+
@complete = true
|
|
211
|
+
ensure
|
|
212
|
+
close
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def close
|
|
217
|
+
return if @closed
|
|
218
|
+
|
|
219
|
+
@closed = true
|
|
220
|
+
OpenAI::Internal::Util.close_fused!(@body)
|
|
221
|
+
@context.response_body(
|
|
222
|
+
@captured,
|
|
223
|
+
headers: @headers,
|
|
224
|
+
complete: @complete,
|
|
225
|
+
total_bytes: @bytes,
|
|
226
|
+
attempt: @attempt
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private def capture(chunk)
|
|
231
|
+
@bytes += chunk.bytesize
|
|
232
|
+
return unless @capture_body
|
|
233
|
+
|
|
234
|
+
remaining = OpenAI::Internal::Logging::MAX_BODY_BYTES - @captured.bytesize
|
|
235
|
+
@captured << chunk.byteslice(0, remaining) if remaining.positive?
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class << self
|
|
240
|
+
def normalize_level(value)
|
|
241
|
+
level = value.to_s.downcase.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
242
|
+
return level if LOG_LEVELS.key?(level)
|
|
243
|
+
|
|
244
|
+
raise ArgumentError, "`log_level` must be one of :off, :error, :warn, :info, or :debug"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def validate_logger!(logger)
|
|
248
|
+
return if logger.nil?
|
|
249
|
+
|
|
250
|
+
methods = [:debug, :info, :warn, :error]
|
|
251
|
+
return if methods.all? { logger.respond_to?(_1) }
|
|
252
|
+
|
|
253
|
+
raise ArgumentError, "`logger` must respond to `debug`, `info`, `warn`, and `error`"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def default_logger = ::Logger.new($stderr)
|
|
257
|
+
|
|
258
|
+
def enabled?(configured_level, event_level)
|
|
259
|
+
LOG_LEVELS.fetch(configured_level) >= LOG_LEVELS.fetch(event_level)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def safe_path(url)
|
|
263
|
+
uri = sanitized_uri(url)
|
|
264
|
+
path = uri.path.to_s.empty? ? "/" : uri.path
|
|
265
|
+
uri.query.nil? ? path : "#{path}?#{uri.query}"
|
|
266
|
+
rescue ArgumentError, URI::Error
|
|
267
|
+
"[URL OMITTED]"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def safe_url(url)
|
|
271
|
+
sanitized_uri(url).to_s
|
|
272
|
+
rescue ArgumentError, URI::Error
|
|
273
|
+
"[URL OMITTED]"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def safe_field(value)
|
|
277
|
+
return "none" if value.nil?
|
|
278
|
+
|
|
279
|
+
value.to_s.dump.delete_prefix('"').delete_suffix('"')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# @api private
|
|
283
|
+
def sensitive_header?(name)
|
|
284
|
+
normalized_name = name.to_s.downcase
|
|
285
|
+
REDACTED_HEADERS.include?(normalized_name) || SENSITIVE_QUERY_KEY.match?(normalized_name)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @api private
|
|
289
|
+
def credential_header?(name)
|
|
290
|
+
normalized_name = name.to_s.downcase
|
|
291
|
+
idempotency = normalized_name.match(/(?:\A|[-_])idempotency[-_]key\z/)
|
|
292
|
+
return sensitive_header?(normalized_name) unless idempotency
|
|
293
|
+
|
|
294
|
+
prefix = normalized_name[...idempotency.begin(0)]
|
|
295
|
+
prefix.split(/[-_]/).any? { sensitive_header?(_1) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def format_headers(headers)
|
|
299
|
+
redacted =
|
|
300
|
+
headers.sort.to_h do |name, value|
|
|
301
|
+
normalized_name = name.to_s.downcase
|
|
302
|
+
rendered =
|
|
303
|
+
if sensitive_header?(normalized_name)
|
|
304
|
+
"[REDACTED]"
|
|
305
|
+
elsif URL_HEADER_KEY.match?(normalized_name)
|
|
306
|
+
sanitized_header_url(value)
|
|
307
|
+
else
|
|
308
|
+
value
|
|
309
|
+
end
|
|
310
|
+
[name, rendered]
|
|
311
|
+
end
|
|
312
|
+
JSON.generate(redacted)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def format_body(body, headers:)
|
|
316
|
+
content_type = headers["content-type"].to_s
|
|
317
|
+
return "[NO BODY]" if body.nil?
|
|
318
|
+
return "[MULTIPART BODY OMITTED]" if content_type.match?(%r{\Amultipart/}i)
|
|
319
|
+
return "[STREAMING BODY OMITTED]" unless body.is_a?(String)
|
|
320
|
+
return "[BINARY BODY OMITTED] bytes=#{body.bytesize}" unless textual_content_type?(content_type)
|
|
321
|
+
|
|
322
|
+
format_text_body(body, content_type: content_type, total_bytes: body.bytesize)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def capture_response_body?(headers)
|
|
326
|
+
content_type = headers["content-type"].to_s
|
|
327
|
+
textual_content_type?(content_type) && !content_type.match?(%r{\Atext/event-stream}i)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def format_observed_body(body, headers:, complete:, total_bytes:)
|
|
331
|
+
content_type = headers["content-type"].to_s
|
|
332
|
+
return "[STREAM BODY OMITTED] bytes=#{total_bytes}" if content_type.match?(%r{\Atext/event-stream}i)
|
|
333
|
+
return "[BODY CLOSED EARLY] bytes=#{total_bytes}" unless complete
|
|
334
|
+
unless textual_content_type?(content_type)
|
|
335
|
+
return "[BINARY BODY OMITTED] bytes=#{total_bytes}"
|
|
336
|
+
end
|
|
337
|
+
if total_bytes > MAX_BODY_BYTES && json_content_type?(content_type)
|
|
338
|
+
return "[JSON BODY OMITTED] bytes=#{total_bytes} reason=too_large"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
format_text_body(body, content_type: content_type, total_bytes: total_bytes)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private def sanitized_uri(url)
|
|
345
|
+
uri = url.dup
|
|
346
|
+
uri.user = nil
|
|
347
|
+
uri.password = nil
|
|
348
|
+
return uri if uri.query.nil?
|
|
349
|
+
|
|
350
|
+
uri.query = sanitized_query(uri.query)
|
|
351
|
+
uri
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private def sanitized_query(query)
|
|
355
|
+
pairs = URI.decode_www_form(query).map do |name, value|
|
|
356
|
+
[name, SENSITIVE_QUERY_KEY.match?(name) ? "[REDACTED]" : value]
|
|
357
|
+
end
|
|
358
|
+
URI.encode_www_form(pairs)
|
|
359
|
+
rescue ArgumentError
|
|
360
|
+
nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private def sanitized_header_url(value)
|
|
364
|
+
safe_url(URI(value.to_s))
|
|
365
|
+
rescue URI::InvalidURIError
|
|
366
|
+
"[URL OMITTED]"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private def textual_content_type?(content_type)
|
|
370
|
+
content_type.empty? ||
|
|
371
|
+
content_type.match?(%r{\Atext/}i) ||
|
|
372
|
+
content_type.match?(/(?:json|xml|x-www-form-urlencoded)/i)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
private def json_content_type?(content_type) = content_type.match?(%r{(?:\A|[+/])json(?:;|\z)}i)
|
|
376
|
+
|
|
377
|
+
private def format_text_body(body, content_type:, total_bytes:)
|
|
378
|
+
if json_content_type?(content_type)
|
|
379
|
+
return "[JSON BODY OMITTED] bytes=#{total_bytes} reason=too_large" if total_bytes > MAX_BODY_BYTES
|
|
380
|
+
|
|
381
|
+
parsed = JSON.parse(body)
|
|
382
|
+
return summarize_json_body(parsed, total_bytes: total_bytes)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
unless content_type.match?(%r{\Aapplication/x-www-form-urlencoded(?:;|\z)}i)
|
|
386
|
+
return "[TEXT BODY OMITTED] bytes=#{total_bytes}"
|
|
387
|
+
end
|
|
388
|
+
return "[FORM BODY OMITTED] bytes=#{total_bytes} reason=too_large" if total_bytes > MAX_BODY_BYTES
|
|
389
|
+
|
|
390
|
+
fields = URI.decode_www_form(body).length
|
|
391
|
+
"[FORM BODY] bytes=#{total_bytes} fields=#{fields}"
|
|
392
|
+
rescue JSON::ParserError
|
|
393
|
+
"[INVALID JSON BODY OMITTED] bytes=#{total_bytes}"
|
|
394
|
+
rescue ArgumentError
|
|
395
|
+
"[INVALID FORM BODY OMITTED] bytes=#{total_bytes}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
private def summarize_json_body(value, total_bytes:)
|
|
399
|
+
shape =
|
|
400
|
+
case value
|
|
401
|
+
in Hash
|
|
402
|
+
"object fields=#{value.length}"
|
|
403
|
+
in Array
|
|
404
|
+
"array items=#{value.length}"
|
|
405
|
+
in String
|
|
406
|
+
"string"
|
|
407
|
+
in Integer | Float
|
|
408
|
+
"number"
|
|
409
|
+
in true | false
|
|
410
|
+
"boolean"
|
|
411
|
+
in nil
|
|
412
|
+
"null"
|
|
413
|
+
end
|
|
414
|
+
"[JSON BODY] bytes=#{total_bytes} type=#{shape}"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
@@ -35,7 +35,7 @@ module OpenAI
|
|
|
35
35
|
def next_page
|
|
36
36
|
unless next_page?
|
|
37
37
|
message = "No more pages available. Please check #next_page? before calling ##{__method__}"
|
|
38
|
-
raise
|
|
38
|
+
raise message
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
req = OpenAI::Internal::Util.deep_merge(@req, {query: {after: next_}})
|
|
@@ -63,9 +63,9 @@ module OpenAI
|
|
|
63
63
|
#
|
|
64
64
|
# @param client [OpenAI::Internal::Transport::BaseClient]
|
|
65
65
|
# @param req [Hash{Symbol=>Object}]
|
|
66
|
-
# @param
|
|
66
|
+
# @param response_metadata [OpenAI::ResponseMetadata]
|
|
67
67
|
# @param page_data [Hash{Symbol=>Object}]
|
|
68
|
-
def initialize(client:, req:,
|
|
68
|
+
def initialize(client:, req:, response_metadata:, page_data:)
|
|
69
69
|
super
|
|
70
70
|
|
|
71
71
|
case page_data
|
|
@@ -81,11 +81,10 @@ module OpenAI
|
|
|
81
81
|
#
|
|
82
82
|
# @return [String]
|
|
83
83
|
def inspect
|
|
84
|
-
# rubocop:disable Layout/LineLength
|
|
85
84
|
model = OpenAI::Internal::Type::Converter.inspect(@model, depth: 1)
|
|
86
85
|
|
|
87
|
-
"#<#{self.class}[#{model}]:0x#{object_id.to_s(16)}
|
|
88
|
-
|
|
86
|
+
"#<#{self.class}[#{model}]:0x#{object_id.to_s(16)} " \
|
|
87
|
+
"has_more=#{has_more.inspect} next_=#{next_.inspect}>"
|
|
89
88
|
end
|
|
90
89
|
end
|
|
91
90
|
end
|
data/lib/openai/internal/page.rb
CHANGED
|
@@ -54,9 +54,9 @@ module OpenAI
|
|
|
54
54
|
#
|
|
55
55
|
# @param client [OpenAI::Internal::Transport::BaseClient]
|
|
56
56
|
# @param req [Hash{Symbol=>Object}]
|
|
57
|
-
# @param
|
|
57
|
+
# @param response_metadata [OpenAI::ResponseMetadata]
|
|
58
58
|
# @param page_data [Array<Object>]
|
|
59
|
-
def initialize(client:, req:,
|
|
59
|
+
def initialize(client:, req:, response_metadata:, page_data:)
|
|
60
60
|
super
|
|
61
61
|
|
|
62
62
|
case page_data
|
|
@@ -7,7 +7,13 @@ module OpenAI
|
|
|
7
7
|
# Provider factories return opaque OpenAI::Provider instances whose public
|
|
8
8
|
# representation does not expose their definition.
|
|
9
9
|
module Provider
|
|
10
|
-
Runtime = Struct.new(
|
|
10
|
+
Runtime = Struct.new(
|
|
11
|
+
:name,
|
|
12
|
+
:base_url,
|
|
13
|
+
:prepare_request,
|
|
14
|
+
:authentication_headers,
|
|
15
|
+
keyword_init: true
|
|
16
|
+
)
|
|
11
17
|
|
|
12
18
|
class << self
|
|
13
19
|
# @api private
|
|
@@ -36,7 +36,6 @@ module OpenAI
|
|
|
36
36
|
def to_a
|
|
37
37
|
values = []
|
|
38
38
|
loop { values << self.next }
|
|
39
|
-
rescue StopIteration
|
|
40
39
|
values
|
|
41
40
|
end
|
|
42
41
|
|
|
@@ -70,18 +69,36 @@ module OpenAI
|
|
|
70
69
|
#
|
|
71
70
|
# @param max_len [Integer, nil]
|
|
72
71
|
#
|
|
73
|
-
# @return [String]
|
|
72
|
+
# @return [String, nil]
|
|
74
73
|
private def read_enum(max_len)
|
|
75
74
|
case max_len
|
|
76
75
|
in nil
|
|
77
|
-
|
|
76
|
+
# `loop` rescues StopIteration, but this method handles it below.
|
|
77
|
+
# rubocop:disable Style/InfiniteLoop
|
|
78
|
+
@buf << @stream.next.b while true
|
|
79
|
+
# rubocop:enable Style/InfiniteLoop
|
|
78
80
|
in Integer
|
|
79
|
-
@buf << @stream.next while @buf.
|
|
80
|
-
|
|
81
|
+
@buf << @stream.next.b while @buf.bytesize < max_len
|
|
82
|
+
read_buffer(max_len)
|
|
81
83
|
end
|
|
82
84
|
rescue StopIteration
|
|
85
|
+
return @buf.slice!(0..) if max_len.nil?
|
|
86
|
+
|
|
83
87
|
@stream = nil
|
|
84
|
-
@buf.
|
|
88
|
+
return nil if @buf.bytesize.zero?
|
|
89
|
+
|
|
90
|
+
read_buffer(max_len)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @api private
|
|
94
|
+
#
|
|
95
|
+
# @param max_len [Integer]
|
|
96
|
+
#
|
|
97
|
+
# @return [String]
|
|
98
|
+
private def read_buffer(max_len)
|
|
99
|
+
read = @buf.byteslice(0, max_len)
|
|
100
|
+
@buf = @buf.byteslice(max_len..) || String.new.b
|
|
101
|
+
read
|
|
85
102
|
end
|
|
86
103
|
|
|
87
104
|
# @api private
|
|
@@ -91,19 +108,26 @@ module OpenAI
|
|
|
91
108
|
#
|
|
92
109
|
# @return [String, nil]
|
|
93
110
|
def read(max_len = nil, out_string = nil)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
read = read_enum(max_len)
|
|
101
|
-
case out_string
|
|
102
|
-
in String
|
|
103
|
-
out_string.replace(read)
|
|
111
|
+
if max_len.is_a?(Integer) && max_len.negative?
|
|
112
|
+
raise ArgumentError, "negative length #{max_len} given"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
read =
|
|
116
|
+
case @stream
|
|
104
117
|
in nil
|
|
105
|
-
|
|
118
|
+
max_len.nil? || max_len.zero? ? +"" : nil
|
|
119
|
+
in IO | StringIO
|
|
120
|
+
return @stream.read(max_len, out_string).tap(&@blk)
|
|
121
|
+
in Enumerator | InterruptibleEnumerator
|
|
122
|
+
read_enum(max_len)
|
|
106
123
|
end
|
|
124
|
+
|
|
125
|
+
case out_string
|
|
126
|
+
in String
|
|
127
|
+
out_string.replace(read || +"")
|
|
128
|
+
read.nil? ? nil : out_string
|
|
129
|
+
in nil
|
|
130
|
+
read
|
|
107
131
|
end
|
|
108
132
|
.tap(&@blk)
|
|
109
133
|
end
|
|
@@ -128,7 +152,7 @@ module OpenAI
|
|
|
128
152
|
else
|
|
129
153
|
src
|
|
130
154
|
end
|
|
131
|
-
@buf = String.new
|
|
155
|
+
@buf = String.new.b
|
|
132
156
|
@blk = blk
|
|
133
157
|
end
|
|
134
158
|
end
|