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.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +241 -8
  4. data/SECURITY.md +6 -22
  5. data/azure.md +110 -0
  6. data/bedrock.md +132 -0
  7. data/examples/mtls_custom_http_client.rb +61 -0
  8. data/lib/openai/auth/workload_identity.rb +13 -2
  9. data/lib/openai/auth/workload_identity_auth.rb +2 -1
  10. data/lib/openai/client.rb +29 -6
  11. data/lib/openai/errors.rb +11 -1
  12. data/lib/openai/helpers/streaming/chat_completion_stream.rb +3 -3
  13. data/lib/openai/helpers/streaming/response_stream.rb +5 -6
  14. data/lib/openai/helpers/structured_output/base_model.rb +20 -19
  15. data/lib/openai/helpers/structured_output/json_schema_converter.rb +12 -5
  16. data/lib/openai/http_client.rb +99 -7
  17. data/lib/openai/internal/conversation_cursor_page.rb +5 -6
  18. data/lib/openai/internal/cursor_page.rb +3 -3
  19. data/lib/openai/internal/logging.rb +419 -0
  20. data/lib/openai/internal/next_cursor_page.rb +5 -6
  21. data/lib/openai/internal/page.rb +2 -2
  22. data/lib/openai/internal/provider.rb +7 -1
  23. data/lib/openai/internal/read_io_adapter.rb +42 -18
  24. data/lib/openai/internal/stream.rb +2 -2
  25. data/lib/openai/internal/transport/base_client.rb +137 -37
  26. data/lib/openai/internal/type/array_of.rb +6 -4
  27. data/lib/openai/internal/type/base_model.rb +39 -1
  28. data/lib/openai/internal/type/base_page.rb +19 -2
  29. data/lib/openai/internal/type/base_stream.rb +39 -7
  30. data/lib/openai/internal/type/hash_of.rb +3 -3
  31. data/lib/openai/internal/util.rb +52 -27
  32. data/lib/openai/models/admin/organization/audit_log_list_params.rb +3 -1
  33. data/lib/openai/models/admin/organization/audit_log_list_response.rb +33 -11
  34. data/lib/openai/models/admin/organization/groups/role_list_response.rb +5 -1
  35. data/lib/openai/models/admin/organization/groups/role_retrieve_response.rb +5 -1
  36. data/lib/openai/models/admin/organization/projects/groups/role_list_response.rb +6 -1
  37. data/lib/openai/models/admin/organization/projects/groups/role_retrieve_response.rb +6 -1
  38. data/lib/openai/models/admin/organization/projects/service_account_create_response.rb +3 -1
  39. data/lib/openai/models/admin/organization/projects/users/role_list_response.rb +6 -1
  40. data/lib/openai/models/admin/organization/projects/users/role_retrieve_response.rb +6 -1
  41. data/lib/openai/models/admin/organization/users/role_list_response.rb +5 -1
  42. data/lib/openai/models/admin/organization/users/role_retrieve_response.rb +5 -1
  43. data/lib/openai/models/all_models.rb +3 -0
  44. data/lib/openai/models/audio/transcription_create_params.rb +7 -1
  45. data/lib/openai/models/audio/translation_create_params.rb +7 -1
  46. data/lib/openai/models/beta/beta_response.rb +9 -0
  47. data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
  48. data/lib/openai/models/beta/beta_responses_client_event.rb +28 -3
  49. data/lib/openai/models/beta/beta_responses_server_event.rb +1037 -61
  50. data/lib/openai/models/beta/response_compact_params.rb +9 -0
  51. data/lib/openai/models/beta/response_create_params.rb +9 -0
  52. data/lib/openai/models/containers/file_create_params.rb +4 -0
  53. data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
  54. data/lib/openai/models/file_create_params.rb +4 -0
  55. data/lib/openai/models/image_create_variation_params.rb +4 -0
  56. data/lib/openai/models/image_edit_params.rb +8 -0
  57. data/lib/openai/models/responses/response_compact_params.rb +9 -0
  58. data/lib/openai/models/responses/response_stream_event.rb +1 -1
  59. data/lib/openai/models/responses/responses_client_event.rb +13 -1
  60. data/lib/openai/models/responses/responses_server_event.rb +1027 -54
  61. data/lib/openai/models/responses_model.rb +3 -0
  62. data/lib/openai/models/skill_create_params.rb +4 -0
  63. data/lib/openai/models/skills/version_create_params.rb +4 -0
  64. data/lib/openai/models/uploads/part_create_params.rb +4 -0
  65. data/lib/openai/models/vector_store_search_response.rb +3 -1
  66. data/lib/openai/models/video_create_character_params.rb +4 -0
  67. data/lib/openai/models/video_create_params.rb +4 -0
  68. data/lib/openai/models/video_edit_params.rb +4 -0
  69. data/lib/openai/models/video_extend_params.rb +4 -0
  70. data/lib/openai/models.rb +9 -1
  71. data/lib/openai/net_http_client.rb +20 -6
  72. data/lib/openai/providers/azure.rb +216 -0
  73. data/lib/openai/providers/bedrock.rb +3 -1
  74. data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
  75. data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
  76. data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
  77. data/lib/openai/resources/admin/organization/projects.rb +4 -2
  78. data/lib/openai/resources/audio/transcriptions.rb +8 -0
  79. data/lib/openai/resources/audio/translations.rb +4 -0
  80. data/lib/openai/resources/beta/responses.rb +1 -1
  81. data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
  82. data/lib/openai/resources/containers/files.rb +4 -0
  83. data/lib/openai/resources/content_provenance_checks.rb +4 -0
  84. data/lib/openai/resources/conversations/items.rb +1 -1
  85. data/lib/openai/resources/evals/runs/output_items.rb +1 -1
  86. data/lib/openai/resources/files.rb +4 -0
  87. data/lib/openai/resources/images.rb +12 -0
  88. data/lib/openai/resources/responses.rb +3 -2
  89. data/lib/openai/resources/skills/versions.rb +4 -0
  90. data/lib/openai/resources/skills.rb +4 -0
  91. data/lib/openai/resources/uploads/parts.rb +4 -0
  92. data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
  93. data/lib/openai/resources/videos.rb +38 -0
  94. data/lib/openai/resources/webhooks.rb +18 -6
  95. data/lib/openai/version.rb +1 -1
  96. data/lib/openai.rb +111 -107
  97. data/rbi/openai/auth.rbi +4 -4
  98. data/rbi/openai/client.rbi +13 -3
  99. data/rbi/openai/errors.rbi +7 -0
  100. data/rbi/openai/helpers/streaming/events.rbi +12 -0
  101. data/rbi/openai/http_client.rbi +62 -2
  102. data/rbi/openai/internal/logging.rbi +192 -0
  103. data/rbi/openai/internal/provider.rbi +3 -0
  104. data/rbi/openai/internal/read_io_adapter.rbi +6 -1
  105. data/rbi/openai/internal/transport/base_client.rbi +61 -7
  106. data/rbi/openai/internal/type/base_model.rbi +24 -1
  107. data/rbi/openai/internal/type/base_page.rbi +13 -2
  108. data/rbi/openai/internal/type/base_stream.rbi +15 -4
  109. data/rbi/openai/internal/util.rbi +26 -2
  110. data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
  111. data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
  112. data/rbi/openai/models/all_models.rbi +15 -0
  113. data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
  114. data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
  115. data/rbi/openai/models/beta/beta_response.rbi +15 -0
  116. data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
  117. data/rbi/openai/models/beta/beta_responses_client_event.rbi +37 -1
  118. data/rbi/openai/models/beta/beta_responses_server_event.rbi +3791 -54
  119. data/rbi/openai/models/beta/response_compact_params.rbi +15 -0
  120. data/rbi/openai/models/beta/response_create_params.rbi +15 -0
  121. data/rbi/openai/models/containers/file_create_params.rbi +8 -0
  122. data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
  123. data/rbi/openai/models/file_create_params.rbi +8 -0
  124. data/rbi/openai/models/image_create_variation_params.rbi +8 -0
  125. data/rbi/openai/models/image_edit_params.rbi +16 -0
  126. data/rbi/openai/models/responses/response_compact_params.rbi +15 -0
  127. data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
  128. data/rbi/openai/models/responses/responses_client_event.rbi +19 -0
  129. data/rbi/openai/models/responses/responses_server_event.rbi +3282 -53
  130. data/rbi/openai/models/responses_model.rbi +15 -0
  131. data/rbi/openai/models/skill_create_params.rbi +8 -0
  132. data/rbi/openai/models/skills/version_create_params.rbi +8 -0
  133. data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
  134. data/rbi/openai/models/video_create_character_params.rbi +8 -0
  135. data/rbi/openai/models/video_create_params.rbi +8 -0
  136. data/rbi/openai/models/video_edit_params.rbi +8 -0
  137. data/rbi/openai/models/video_extend_params.rbi +8 -0
  138. data/rbi/openai/net_http_client.rbi +2 -2
  139. data/rbi/openai/providers.rbi +9 -0
  140. data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
  141. data/rbi/openai/resources/audio/translations.rbi +7 -1
  142. data/rbi/openai/resources/containers/files.rbi +4 -0
  143. data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
  144. data/rbi/openai/resources/files.rbi +4 -0
  145. data/rbi/openai/resources/images.rbi +20 -0
  146. data/rbi/openai/resources/skills/versions.rbi +4 -0
  147. data/rbi/openai/resources/skills.rbi +4 -0
  148. data/rbi/openai/resources/uploads/parts.rbi +4 -0
  149. data/rbi/openai/resources/videos.rbi +16 -0
  150. data/sig/openai/client.rbs +6 -2
  151. data/sig/openai/errors.rbs +5 -0
  152. data/sig/openai/http_client.rbs +37 -2
  153. data/sig/openai/internal/logging.rbs +88 -0
  154. data/sig/openai/internal/provider.rbs +3 -1
  155. data/sig/openai/internal/read_io_adapter.rbs +3 -1
  156. data/sig/openai/internal/transport/base_client.rbs +33 -5
  157. data/sig/openai/internal/type/base_model.rbs +13 -0
  158. data/sig/openai/internal/type/base_page.rbs +8 -1
  159. data/sig/openai/internal/type/base_stream.rbs +8 -2
  160. data/sig/openai/internal/util.rbs +12 -0
  161. data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
  162. data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
  163. data/sig/openai/models/all_models.rbs +6 -0
  164. data/sig/openai/models/beta/beta_response.rbs +6 -0
  165. data/sig/openai/models/beta/beta_responses_client_event.rbs +13 -0
  166. data/sig/openai/models/beta/beta_responses_server_event.rbs +1946 -54
  167. data/sig/openai/models/beta/response_compact_params.rbs +6 -0
  168. data/sig/openai/models/beta/response_create_params.rbs +6 -0
  169. data/sig/openai/models/responses/response_compact_params.rbs +6 -0
  170. data/sig/openai/models/responses/responses_client_event.rbs +7 -0
  171. data/sig/openai/models/responses/responses_server_event.rbs +1786 -53
  172. data/sig/openai/models/responses_model.rbs +6 -0
  173. data/sig/openai/net_http_client.rbs +2 -2
  174. data/sig/openai/providers.rbs +6 -0
  175. metadata +24 -1
@@ -28,6 +28,9 @@ module OpenAI
28
28
  GPT_5_PRO = :"gpt-5-pro"
29
29
  GPT_5_PRO_2025_10_06 = :"gpt-5-pro-2025-10-06"
30
30
  GPT_5_1_CODEX_MAX = :"gpt-5.1-codex-max"
31
+ GPT_DAYBREAK_BLUE_LATEST = :"gpt-daybreak-blue-latest"
32
+ GPT_DAYBREAK_RED_LATEST = :"gpt-daybreak-red-latest"
33
+ GPT_5_6_CYBER = :"gpt-5.6-cyber"
31
34
 
32
35
  # @!method self.values
33
36
  # @return [Array<Symbol>]
@@ -10,6 +10,10 @@ module OpenAI
10
10
  # @!attribute files
11
11
  # Skill files to upload (directory upload) or a single zip file.
12
12
  #
13
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
14
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
15
+ # content type.
16
+ #
13
17
  # @return [Array<Pathname, StringIO, IO, String, OpenAI::FilePart>, Pathname, StringIO, IO, String, OpenAI::FilePart, nil]
14
18
  optional :files, union: -> { OpenAI::SkillCreateParams::Files }
15
19
 
@@ -22,6 +22,10 @@ module OpenAI
22
22
  # @!attribute files
23
23
  # Skill files to upload (directory upload) or a single zip file.
24
24
  #
25
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
26
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
27
+ # content type.
28
+ #
25
29
  # @return [Array<Pathname, StringIO, IO, String, OpenAI::FilePart>, Pathname, StringIO, IO, String, OpenAI::FilePart, nil]
26
30
  optional :files, union: -> { OpenAI::Skills::VersionCreateParams::Files }
27
31
 
@@ -16,6 +16,10 @@ module OpenAI
16
16
  # @!attribute data
17
17
  # The chunk of bytes for this Part.
18
18
  #
19
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
20
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
21
+ # content type.
22
+ #
19
23
  # @return [Pathname, StringIO, IO, String, OpenAI::FilePart]
20
24
  required :data, OpenAI::Internal::Type::FileInput
21
25
 
@@ -13,7 +13,9 @@ module OpenAI
13
13
  #
14
14
  # @return [Hash{Symbol=>String, Float, Boolean}, nil]
15
15
  required :attributes,
16
- -> { OpenAI::Internal::Type::HashOf[union: OpenAI::Models::VectorStoreSearchResponse::Attribute] },
16
+ -> {
17
+ OpenAI::Internal::Type::HashOf[union: OpenAI::Models::VectorStoreSearchResponse::Attribute]
18
+ },
17
19
  nil?: true
18
20
 
19
21
  # @!attribute content
@@ -16,6 +16,10 @@ module OpenAI
16
16
  # @!attribute video
17
17
  # Video file used to create a character.
18
18
  #
19
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
20
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
21
+ # content type.
22
+ #
19
23
  # @return [Pathname, StringIO, IO, String, OpenAI::FilePart]
20
24
  required :video, OpenAI::Internal::Type::FileInput
21
25
 
@@ -16,6 +16,10 @@ module OpenAI
16
16
  # @!attribute input_reference
17
17
  # Optional reference asset upload or reference object that guides generation.
18
18
  #
19
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
20
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
21
+ # content type.
22
+ #
19
23
  # @return [Pathname, StringIO, IO, String, OpenAI::FilePart, OpenAI::Models::ImageInputReferenceParam, nil]
20
24
  optional :input_reference, union: -> { OpenAI::VideoCreateParams::InputReference }
21
25
 
@@ -16,6 +16,10 @@ module OpenAI
16
16
  # @!attribute video
17
17
  # Reference to the completed video to edit.
18
18
  #
19
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
20
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
21
+ # content type.
22
+ #
19
23
  # @return [Pathname, StringIO, IO, String, OpenAI::FilePart, OpenAI::Models::VideoEditParams::Video::VideoReferenceInputParam]
20
24
  required :video, union: -> { OpenAI::VideoEditParams::Video }
21
25
 
@@ -23,6 +23,10 @@ module OpenAI
23
23
  # @!attribute video
24
24
  # Reference to the completed video to extend.
25
25
  #
26
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
27
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
28
+ # content type.
29
+ #
26
30
  # @return [Pathname, StringIO, IO, String, OpenAI::FilePart, OpenAI::Models::VideoExtendParams::Video::VideoReferenceInputParam]
27
31
  required :video, union: -> { OpenAI::VideoExtendParams::Video }
28
32
 
data/lib/openai/models.rb CHANGED
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenAI
4
- [OpenAI::Internal::Type::BaseModel, *OpenAI::Internal::Type::BaseModel.subclasses].each do |cls|
4
+ # ActiveSupport 6 implements Class#subclasses using ==, which invokes BaseModel's
5
+ # structural equality before all model aliases are loaded.
6
+ base_model = OpenAI::Internal::Type::BaseModel
7
+ subclasses =
8
+ ObjectSpace.each_object(base_model.singleton_class).select do |candidate|
9
+ !candidate.singleton_class? && candidate.superclass.equal?(base_model)
10
+ end
11
+
12
+ [base_model, *subclasses].each do |cls|
5
13
  cls.define_sorbet_constant!(:OrHash) { T.type_alias { T.any(cls, OpenAI::Internal::AnyHash) } }
6
14
  end
7
15
 
@@ -5,6 +5,10 @@ require_relative "internal/read_io_adapter"
5
5
  module OpenAI
6
6
  # The SDK's pooled Net::HTTP implementation.
7
7
  #
8
+ # Network operations from a non-blocking fiber cooperate with its active Ruby
9
+ # Fiber scheduler, allowing concurrent requests and streams without occupying
10
+ # one thread per request.
11
+ #
8
12
  # Pass a block to configure each SDK-created connection before it is pooled
9
13
  # and started.
10
14
  class NetHTTPClient < HTTPClient
@@ -66,15 +70,15 @@ module OpenAI
66
70
  # @api private
67
71
  #
68
72
  # @param conn [Net::HTTP]
69
- # @param deadline [Float]
73
+ # @param deadline [Float, nil]
70
74
  private def calibrate_socket_timeout(conn, deadline)
71
- timeout = remaining_timeout(deadline)
75
+ timeout = deadline&.then { remaining_timeout(_1) }
72
76
  conn.open_timeout = conn.read_timeout = conn.write_timeout = conn.continue_timeout = timeout
73
77
  end
74
78
 
75
79
  # @api private
76
80
  #
77
- # @param deadline [Float]
81
+ # @param deadline [Float, nil]
78
82
  # @return [Float]
79
83
  # @raise [Timeout::Error]
80
84
  private def remaining_timeout(deadline)
@@ -130,7 +134,6 @@ module OpenAI
130
134
  # @yieldparam [Net::HTTP]
131
135
  private def with_pool(url, deadline:, &blk)
132
136
  origin = OpenAI::Internal::Util.uri_origin(url)
133
- timeout = remaining_timeout(deadline)
134
137
  pool =
135
138
  @mutex.synchronize do
136
139
  @pools[origin] ||= ConnectionPool.new(size: @size) do
@@ -138,7 +141,18 @@ module OpenAI
138
141
  end
139
142
  end
140
143
 
141
- pool.with(timeout: timeout, &blk)
144
+ return pool.with(timeout: remaining_timeout(deadline), &blk) if deadline
145
+
146
+ checked_out = false
147
+ begin
148
+ pool.with do |connection|
149
+ checked_out = true
150
+ blk.call(connection)
151
+ end
152
+ rescue ConnectionPool::TimeoutError
153
+ retry unless checked_out
154
+ raise
155
+ end
142
156
  end
143
157
 
144
158
  # @api private
@@ -200,7 +214,7 @@ module OpenAI
200
214
  # @return [OpenAI::HTTPClient::Response]
201
215
  def execute(request)
202
216
  url = request.url
203
- deadline = OpenAI::Internal::Util.monotonic_secs + request.timeout
217
+ deadline = request.timeout&.then { OpenAI::Internal::Util.monotonic_secs + _1 }
204
218
 
205
219
  req = nil
206
220
  finished = false
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Providers
5
+ # @api private
6
+ module Azure
7
+ API_KEY_AUTH_MARKER = :openai_azure_api_key
8
+ BEARER_AUTH_MARKER = :openai_azure_bearer
9
+ AUTH_HEADERS = %w[api-key authorization].freeze
10
+ MISSING_ENDPOINT_MESSAGE =
11
+ "Azure OpenAI requires an endpoint. Pass `endpoint` to `azure(...)`, or set " \
12
+ "`AZURE_OPENAI_ENDPOINT`."
13
+ MISSING_CREDENTIALS_MESSAGE =
14
+ "Could not find credentials for Azure OpenAI. Pass `api_key` or `token_provider` " \
15
+ "to `azure(...)`, or set `AZURE_OPENAI_API_KEY`."
16
+
17
+ class Definition
18
+ attr_reader :name
19
+
20
+ def initialize(base_url:, credential_provider:, authentication:)
21
+ @name = "azure"
22
+ @base_url = base_url.freeze
23
+ @credential_provider = credential_provider
24
+ @authentication = authentication
25
+ end
26
+
27
+ def configure
28
+ auth = Auth.new(
29
+ base_url: @base_url,
30
+ credential_provider: @credential_provider,
31
+ authentication: @authentication
32
+ )
33
+ OpenAI::Internal::Provider::Runtime.new(
34
+ name: name,
35
+ base_url: @base_url,
36
+ prepare_request: auth.method(:prepare_request),
37
+ authentication_headers: AUTH_HEADERS
38
+ )
39
+ end
40
+ end
41
+
42
+ class Auth
43
+ def initialize(base_url:, credential_provider:, authentication:)
44
+ @base_url = URI(base_url)
45
+ @credential_provider = credential_provider
46
+ @authentication = authentication
47
+ end
48
+
49
+ def prepare_request(request)
50
+ Azure.validate_origin!(request.fetch(:url), @base_url)
51
+ marker = @authentication == :api_key ? API_KEY_AUTH_MARKER : BEARER_AUTH_MARKER
52
+ headers = Azure.provider_headers(request, marker: marker)
53
+ credential = resolve_credential
54
+ auth_header =
55
+ if @authentication == :api_key
56
+ {"api-key" => credential}
57
+ else
58
+ {"authorization" => "Bearer #{credential}"}
59
+ end
60
+
61
+ request.merge(headers: headers.merge(auth_header), provider_auth: marker)
62
+ end
63
+
64
+ private def resolve_credential
65
+ credential = @credential_provider.call
66
+ unless credential.is_a?(String) && !credential.strip.empty?
67
+ name = @authentication == :api_key ? "API key" : "token provider"
68
+ raise OpenAI::Errors::Error,
69
+ "The Azure OpenAI #{name} must return a non-empty string."
70
+ end
71
+ credential
72
+ rescue OpenAI::Errors::Error
73
+ raise
74
+ rescue StandardError => e
75
+ message =
76
+ if @authentication == :api_key
77
+ "Failed to resolve an API key for Azure OpenAI."
78
+ else
79
+ "Failed to resolve a bearer token for Azure OpenAI."
80
+ end
81
+ raise OpenAI::Errors::Error.new(message), cause: e
82
+ end
83
+ end
84
+
85
+ class << self
86
+ def normalize_endpoint(endpoint)
87
+ uri = URI(endpoint)
88
+ unless uri.is_a?(URI::HTTP) && uri.host
89
+ raise ArgumentError, "The Azure OpenAI `endpoint` must be an absolute HTTP or HTTPS URL."
90
+ end
91
+ unless uri.query.nil?
92
+ raise ArgumentError, "The Azure OpenAI `endpoint` must not include a query string."
93
+ end
94
+ unless uri.fragment.nil?
95
+ raise ArgumentError, "The Azure OpenAI `endpoint` must not include a fragment."
96
+ end
97
+ unless uri.userinfo.nil?
98
+ raise ArgumentError, "The Azure OpenAI `endpoint` must not include user information."
99
+ end
100
+
101
+ path = uri.path.sub(%r{/+\z}, "")
102
+ uri.path =
103
+ case path
104
+ when %r{/openai/v1\z}
105
+ path
106
+ when %r{/openai\z}
107
+ "#{path}/v1"
108
+ else
109
+ "#{path}/openai/v1"
110
+ end
111
+ uri.to_s
112
+ rescue URI::InvalidURIError => e
113
+ message = "The Azure OpenAI `endpoint` must be an absolute HTTP or HTTPS URL."
114
+ raise ArgumentError.new(message), cause: e
115
+ end
116
+
117
+ def normalize_optional_string(value)
118
+ return nil unless value.is_a?(String)
119
+ normalized = value.strip
120
+ normalized unless normalized.empty?
121
+ end
122
+
123
+ def provider_headers(request, marker:)
124
+ headers = request.fetch(:headers).dup
125
+ if request[:provider_auth] == marker
126
+ AUTH_HEADERS.each { headers.delete(_1) }
127
+ elsif AUTH_HEADERS.any? { headers.key?(_1) }
128
+ raise OpenAI::Errors::Error,
129
+ "Azure OpenAI provider authentication cannot be combined with a custom " \
130
+ "`Authorization` or `api-key` header."
131
+ end
132
+ headers
133
+ end
134
+
135
+ def validate_origin!(url, base_url)
136
+ return if OpenAI::Internal::Util.uri_origin(url) == OpenAI::Internal::Util.uri_origin(base_url)
137
+ raise OpenAI::Errors::Error,
138
+ "Refusing to authenticate a request for an origin other than the configured " \
139
+ "Azure OpenAI endpoint."
140
+ end
141
+ end
142
+ end
143
+
144
+ class << self
145
+ # Configure the standard OpenAI client for the Azure OpenAI v1 API.
146
+ #
147
+ # @param endpoint [String, nil] Azure OpenAI resource endpoint. Defaults to
148
+ # `AZURE_OPENAI_ENDPOINT`. `/openai/v1` is appended when absent.
149
+ #
150
+ # @param api_key [String, nil] Azure OpenAI API key. Defaults to
151
+ # `AZURE_OPENAI_API_KEY` when no token provider is configured. Passing
152
+ # `nil` explicitly skips the environment fallback.
153
+ #
154
+ # @param token_provider [#call, nil] Callable returning a Microsoft Entra
155
+ # bearer token before each request attempt.
156
+ #
157
+ # @return [OpenAI::Provider]
158
+ def azure(
159
+ endpoint: OpenAI::Internal::OMIT,
160
+ api_key: OpenAI::Internal::OMIT,
161
+ token_provider: nil
162
+ )
163
+ configured_endpoint =
164
+ if endpoint.equal?(OpenAI::Internal::OMIT)
165
+ Azure.normalize_optional_string(ENV["AZURE_OPENAI_ENDPOINT"])
166
+ else
167
+ normalized = Azure.normalize_optional_string(endpoint)
168
+ if !endpoint.nil? && normalized.nil?
169
+ raise ArgumentError, "The Azure OpenAI `endpoint` must not be empty."
170
+ end
171
+ normalized
172
+ end
173
+ raise ArgumentError, Azure::MISSING_ENDPOINT_MESSAGE if configured_endpoint.nil?
174
+ base_url = Azure.normalize_endpoint(configured_endpoint)
175
+
176
+ explicit_api_key = !api_key.equal?(OpenAI::Internal::OMIT) && !api_key.nil?
177
+ normalized_api_key = Azure.normalize_optional_string(api_key)
178
+ if explicit_api_key && normalized_api_key.nil?
179
+ raise ArgumentError, "The Azure OpenAI API key must not be empty."
180
+ end
181
+ unless token_provider.nil? || token_provider.respond_to?(:call)
182
+ raise ArgumentError, "The Azure OpenAI `token_provider` must respond to `call`."
183
+ end
184
+ if explicit_api_key && !token_provider.nil?
185
+ raise ArgumentError,
186
+ "The Azure OpenAI `api_key` and `token_provider` options are mutually exclusive."
187
+ end
188
+
189
+ environment_api_key =
190
+ if api_key.equal?(OpenAI::Internal::OMIT) && token_provider.nil?
191
+ Azure.normalize_optional_string(ENV["AZURE_OPENAI_API_KEY"])
192
+ end
193
+ resolved_api_key = normalized_api_key || environment_api_key
194
+ if resolved_api_key.nil? && token_provider.nil?
195
+ raise ArgumentError, Azure::MISSING_CREDENTIALS_MESSAGE
196
+ end
197
+
198
+ if token_provider
199
+ credential_provider = token_provider
200
+ authentication = :bearer
201
+ else
202
+ resolved_api_key.freeze
203
+ credential_provider = -> { resolved_api_key }
204
+ authentication = :api_key
205
+ end
206
+
207
+ definition = Azure::Definition.new(
208
+ base_url: base_url,
209
+ credential_provider: credential_provider,
210
+ authentication: authentication
211
+ )
212
+ OpenAI::Internal::Provider.create(definition)
213
+ end
214
+ end
215
+ end
216
+ end
@@ -6,6 +6,7 @@ module OpenAI
6
6
  module Bedrock
7
7
  SERVICE = "bedrock-mantle"
8
8
  CANONICAL_HOST = /\Abedrock-mantle\.([a-z0-9-]+)\.api\.aws\z/i
9
+ AUTH_HEADERS = %w[authorization].freeze
9
10
  SIGNING_HEADERS = %w[authorization x-amz-content-sha256 x-amz-date x-amz-security-token].freeze
10
11
  BEARER_AUTH_MARKER = :openai_bedrock_bearer
11
12
  SIGV4_AUTH_MARKER = :openai_bedrock_sigv4
@@ -91,7 +92,8 @@ module OpenAI
91
92
  OpenAI::Internal::Provider::Runtime.new(
92
93
  name: name,
93
94
  base_url: base_url,
94
- prepare_request: auth.method(:prepare_request)
95
+ prepare_request: auth.method(:prepare_request),
96
+ authentication_headers: AUTH_HEADERS
95
97
  )
96
98
  end
97
99
  end
@@ -97,11 +97,11 @@ module OpenAI
97
97
  # @see OpenAI::Models::Admin::Organization::Projects::Groups::RoleListParams
98
98
  def list(group_id, params)
99
99
  parsed, options = OpenAI::Admin::Organization::Projects::Groups::RoleListParams.dump_request(params)
100
- query = OpenAI::Internal::Util.encode_query_params(parsed)
101
100
  project_id =
102
101
  parsed.delete(:project_id) do
103
102
  raise ArgumentError.new("missing required path argument #{_1}")
104
103
  end
104
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
105
105
  @client.request(
106
106
  method: :get,
107
107
  path: ["projects/%1$s/groups/%2$s/roles", project_id, group_id],
@@ -53,11 +53,11 @@ module OpenAI
53
53
  # @see OpenAI::Models::Admin::Organization::Projects::GroupRetrieveParams
54
54
  def retrieve(group_id, params)
55
55
  parsed, options = OpenAI::Admin::Organization::Projects::GroupRetrieveParams.dump_request(params)
56
- query = OpenAI::Internal::Util.encode_query_params(parsed)
57
56
  project_id =
58
57
  parsed.delete(:project_id) do
59
58
  raise ArgumentError.new("missing required path argument #{_1}")
60
59
  end
60
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
61
61
  @client.request(
62
62
  method: :get,
63
63
  path: ["organization/projects/%1$s/groups/%2$s", project_id, group_id],
@@ -97,11 +97,11 @@ module OpenAI
97
97
  # @see OpenAI::Models::Admin::Organization::Projects::Users::RoleListParams
98
98
  def list(user_id, params)
99
99
  parsed, options = OpenAI::Admin::Organization::Projects::Users::RoleListParams.dump_request(params)
100
- query = OpenAI::Internal::Util.encode_query_params(parsed)
101
100
  project_id =
102
101
  parsed.delete(:project_id) do
103
102
  raise ArgumentError.new("missing required path argument #{_1}")
104
103
  end
104
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
105
105
  @client.request(
106
106
  method: :get,
107
107
  path: ["projects/%1$s/users/%2$s/roles", project_id, user_id],
@@ -182,7 +182,8 @@ module OpenAI
182
182
  def initialize(client:)
183
183
  @client = client
184
184
  @users = OpenAI::Resources::Admin::Organization::Projects::Users.new(client: client)
185
- @service_accounts = OpenAI::Resources::Admin::Organization::Projects::ServiceAccounts.new(client: client)
185
+ @service_accounts =
186
+ OpenAI::Resources::Admin::Organization::Projects::ServiceAccounts.new(client: client)
186
187
  @api_keys = OpenAI::Resources::Admin::Organization::Projects::APIKeys.new(client: client)
187
188
  @rate_limits = OpenAI::Resources::Admin::Organization::Projects::RateLimits.new(client: client)
188
189
  @model_permissions =
@@ -191,7 +192,8 @@ module OpenAI
191
192
  OpenAI::Resources::Admin::Organization::Projects::HostedToolPermissions.new(client: client)
192
193
  @groups = OpenAI::Resources::Admin::Organization::Projects::Groups.new(client: client)
193
194
  @roles = OpenAI::Resources::Admin::Organization::Projects::Roles.new(client: client)
194
- @data_retention = OpenAI::Resources::Admin::Organization::Projects::DataRetention.new(client: client)
195
+ @data_retention =
196
+ OpenAI::Resources::Admin::Organization::Projects::DataRetention.new(client: client)
195
197
  @spend_limit = OpenAI::Resources::Admin::Organization::Projects::SpendLimit.new(client: client)
196
198
  @spend_alerts = OpenAI::Resources::Admin::Organization::Projects::SpendAlerts.new(client: client)
197
199
  @certificates = OpenAI::Resources::Admin::Organization::Projects::Certificates.new(client: client)
@@ -16,6 +16,10 @@ module OpenAI
16
16
  # Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
17
17
  # format, or a stream of transcript events.
18
18
  #
19
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
20
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
21
+ # content type.
22
+ #
19
23
  # @overload create(file:, model:, chunking_strategy: nil, include: nil, keywords: nil, known_speaker_names: nil, known_speaker_references: nil, language: nil, languages: nil, prompt: nil, response_format: nil, temperature: nil, timestamp_granularities: nil, request_options: {})
20
24
  #
21
25
  # @param file [Pathname, StringIO, IO, String, OpenAI::FilePart] The audio file object (not file name) to transcribe, in one of these formats: fl
@@ -77,6 +81,10 @@ module OpenAI
77
81
  # Returns a transcription object in `json`, `diarized_json`, or `verbose_json`
78
82
  # format, or a stream of transcript events.
79
83
  #
84
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
85
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
86
+ # content type.
87
+ #
80
88
  # @overload create_streaming(file:, model:, chunking_strategy: nil, include: nil, keywords: nil, known_speaker_names: nil, known_speaker_references: nil, language: nil, languages: nil, prompt: nil, response_format: nil, temperature: nil, timestamp_granularities: nil, request_options: {})
81
89
  #
82
90
  # @param file [Pathname, StringIO, IO, String, OpenAI::FilePart] The audio file object (not file name) to transcribe, in one of these formats: fl
@@ -10,6 +10,10 @@ module OpenAI
10
10
  #
11
11
  # Translates audio into English.
12
12
  #
13
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
14
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
15
+ # content type.
16
+ #
13
17
  # @overload create(file:, model:, prompt: nil, response_format: nil, temperature: nil, request_options: {})
14
18
  #
15
19
  # @param file [Pathname, StringIO, IO, String, OpenAI::FilePart] The audio file object (not file name) translate, in one of these formats: flac,
@@ -301,12 +301,12 @@ module OpenAI
301
301
  def retrieve_streaming(response_id, params = {})
302
302
  query_params = [:include, :include_obfuscation, :starting_after, :stream]
303
303
  parsed, options = OpenAI::Beta::ResponseRetrieveParams.dump_request(params)
304
- query = OpenAI::Internal::Util.encode_query_params(parsed.slice(*query_params))
305
304
  unless parsed.fetch(:stream, true)
306
305
  message = "Please use `#retrieve` for the non-streaming use case."
307
306
  raise ArgumentError.new(message)
308
307
  end
309
308
  parsed.store(:stream, true)
309
+ query = OpenAI::Internal::Util.encode_query_params(parsed.slice(*query_params))
310
310
  @client.request(
311
311
  method: :get,
312
312
  path: ["responses/%1$s?beta=true", response_id],
@@ -33,7 +33,6 @@ module OpenAI
33
33
  # @see OpenAI::Models::Beta::Threads::Runs::StepRetrieveParams
34
34
  def retrieve(step_id, params)
35
35
  parsed, options = OpenAI::Beta::Threads::Runs::StepRetrieveParams.dump_request(params)
36
- query = OpenAI::Internal::Util.encode_query_params(parsed)
37
36
  thread_id =
38
37
  parsed.delete(:thread_id) do
39
38
  raise ArgumentError.new("missing required path argument #{_1}")
@@ -42,6 +41,7 @@ module OpenAI
42
41
  parsed.delete(:run_id) do
43
42
  raise ArgumentError.new("missing required path argument #{_1}")
44
43
  end
44
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
45
45
  @client.request(
46
46
  method: :get,
47
47
  path: ["threads/%1$s/runs/%2$s/steps/%3$s", thread_id, run_id, step_id],
@@ -82,11 +82,11 @@ module OpenAI
82
82
  # @see OpenAI::Models::Beta::Threads::Runs::StepListParams
83
83
  def list(run_id, params)
84
84
  parsed, options = OpenAI::Beta::Threads::Runs::StepListParams.dump_request(params)
85
- query = OpenAI::Internal::Util.encode_query_params(parsed)
86
85
  thread_id =
87
86
  parsed.delete(:thread_id) do
88
87
  raise ArgumentError.new("missing required path argument #{_1}")
89
88
  end
89
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
90
90
  @client.request(
91
91
  method: :get,
92
92
  path: ["threads/%1$s/runs/%2$s/steps", thread_id, run_id],
@@ -15,6 +15,10 @@ module OpenAI
15
15
  # You can send either a multipart/form-data request with the raw file content, or
16
16
  # a JSON request with a file ID.
17
17
  #
18
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
19
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
20
+ # content type.
21
+ #
18
22
  # @overload create(container_id, file: nil, file_id: nil, request_options: {})
19
23
  #
20
24
  # @param container_id [String]
@@ -13,6 +13,10 @@ module OpenAI
13
13
  # signals were available. Content could also still be AI-generated by another
14
14
  # company's model, which the tool currently does not detect.
15
15
  #
16
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
17
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
18
+ # content type.
19
+ #
16
20
  # @overload create(file:, request_options: {})
17
21
  #
18
22
  # @param file [Pathname, StringIO, IO, String, OpenAI::FilePart] The image or audio file to check for supported OpenAI provenance signals.
@@ -58,11 +58,11 @@ module OpenAI
58
58
  # @see OpenAI::Models::Conversations::ItemRetrieveParams
59
59
  def retrieve(item_id, params)
60
60
  parsed, options = OpenAI::Conversations::ItemRetrieveParams.dump_request(params)
61
- query = OpenAI::Internal::Util.encode_query_params(parsed)
62
61
  conversation_id =
63
62
  parsed.delete(:conversation_id) do
64
63
  raise ArgumentError.new("missing required path argument #{_1}")
65
64
  end
65
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
66
66
  @client.request(
67
67
  method: :get,
68
68
  path: ["conversations/%1$s/items/%2$s", conversation_id, item_id],
@@ -66,11 +66,11 @@ module OpenAI
66
66
  # @see OpenAI::Models::Evals::Runs::OutputItemListParams
67
67
  def list(run_id, params)
68
68
  parsed, options = OpenAI::Evals::Runs::OutputItemListParams.dump_request(params)
69
- query = OpenAI::Internal::Util.encode_query_params(parsed)
70
69
  eval_id =
71
70
  parsed.delete(:eval_id) do
72
71
  raise ArgumentError.new("missing required path argument #{_1}")
73
72
  end
73
+ query = OpenAI::Internal::Util.encode_query_params(parsed)
74
74
  @client.request(
75
75
  method: :get,
76
76
  path: ["evals/%1$s/runs/%2$s/output_items", eval_id, run_id],
@@ -36,6 +36,10 @@ module OpenAI
36
36
  # Please [contact us](https://help.openai.com/) if you need to increase these
37
37
  # storage limits.
38
38
  #
39
+ # `String`, `StringIO`, and pathless `IO` inputs are sent with generic upload
40
+ # metadata. Use `OpenAI::FilePart` when you need to override the filename or
41
+ # content type.
42
+ #
39
43
  # @overload create(file:, purpose:, expires_after: nil, request_options: {})
40
44
  #
41
45
  # @param file [Pathname, StringIO, IO, String, OpenAI::FilePart] The File object (not file name) to be uploaded.