openai 0.77.1 → 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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/README.md +341 -10
  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 +34 -14
  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 +189 -0
  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 +161 -0
  24. data/lib/openai/internal/stream.rb +2 -2
  25. data/lib/openai/internal/transport/base_client.rb +216 -57
  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 +40 -8
  30. data/lib/openai/internal/type/hash_of.rb +3 -3
  31. data/lib/openai/internal/util.rb +59 -112
  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 +295 -0
  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 +113 -108
  97. data/rbi/openai/auth.rbi +4 -4
  98. data/rbi/openai/client.rbi +15 -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 +123 -0
  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 +52 -0
  105. data/rbi/openai/internal/transport/base_client.rbi +80 -10
  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 +16 -5
  109. data/rbi/openai/internal/util.rbi +26 -42
  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 +64 -0
  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 +7 -2
  151. data/sig/openai/errors.rbs +5 -0
  152. data/sig/openai/http_client.rbs +76 -0
  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 +23 -0
  156. data/sig/openai/internal/transport/base_client.rbs +41 -6
  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 +9 -3
  160. data/sig/openai/internal/util.rbs +12 -16
  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 +34 -0
  174. data/sig/openai/providers.rbs +6 -0
  175. metadata +33 -4
  176. data/lib/openai/internal/transport/pooled_net_requester.rb +0 -210
  177. data/rbi/openai/internal/transport/pooled_net_requester.rbi +0 -82
  178. data/sig/openai/internal/transport/pooled_net_requester.rbs +0 -48
@@ -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
 
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "internal/read_io_adapter"
4
+
5
+ module OpenAI
6
+ # The SDK's pooled Net::HTTP implementation.
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
+ #
12
+ # Pass a block to configure each SDK-created connection before it is pooled
13
+ # and started.
14
+ class NetHTTPClient < HTTPClient
15
+ # from the golang stdlib
16
+ # https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49
17
+ KEEP_ALIVE_TIMEOUT = 30
18
+
19
+ DEFAULT_MAX_CONNECTIONS = [Etc.nprocessors, 99].max
20
+
21
+ NETWORK_ERRORS = [
22
+ EOFError,
23
+ IOError,
24
+ SocketError,
25
+ SystemCallError,
26
+ OpenSSL::SSL::SSLError,
27
+ Net::HTTPBadResponse,
28
+ Net::HTTPHeaderSyntaxError,
29
+ Net::ProtocolError,
30
+ (defined?(Zlib::Error) ? Zlib::Error : nil),
31
+ ConnectionPool::PoolShuttingDownError
32
+ ].compact.freeze
33
+ private_constant :NETWORK_ERRORS
34
+
35
+ class ConnectionConfigurationError < StandardError
36
+ attr_reader :original
37
+
38
+ def initialize(original)
39
+ @original = original
40
+ super()
41
+ end
42
+ end
43
+ private_constant :ConnectionConfigurationError
44
+
45
+ # @api private
46
+ #
47
+ # @param url [URI::Generic]
48
+ #
49
+ # @return [Net::HTTP]
50
+ private def connect(url:)
51
+ port =
52
+ case [url.port, url.scheme]
53
+ in [Integer, _]
54
+ url.port
55
+ in [nil, "http" | "ws"]
56
+ Net::HTTP.http_default_port
57
+ in [nil, "https" | "wss"]
58
+ Net::HTTP.https_default_port
59
+ end
60
+
61
+ Net::HTTP.new(url.host, port).tap do
62
+ _1.use_ssl = %w[https wss].include?(url.scheme)
63
+ _1.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
64
+ _1.max_retries = 0
65
+
66
+ (_1.cert_store = @cert_store) if _1.use_ssl?
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ #
72
+ # @param conn [Net::HTTP]
73
+ # @param deadline [Float, nil]
74
+ private def calibrate_socket_timeout(conn, deadline)
75
+ timeout = deadline&.then { remaining_timeout(_1) }
76
+ conn.open_timeout = conn.read_timeout = conn.write_timeout = conn.continue_timeout = timeout
77
+ end
78
+
79
+ # @api private
80
+ #
81
+ # @param deadline [Float, nil]
82
+ # @return [Float]
83
+ # @raise [Timeout::Error]
84
+ private def remaining_timeout(deadline)
85
+ timeout = deadline - OpenAI::Internal::Util.monotonic_secs
86
+ raise Timeout::Error, "request timed out" unless timeout.positive?
87
+
88
+ timeout
89
+ end
90
+
91
+ # @api private
92
+ #
93
+ # @param request [OpenAI::HTTPClient::Request]
94
+ # @param blk [Proc]
95
+ #
96
+ # @yieldparam [String]
97
+ # @return [Array(Net::HTTPGenericRequest, Proc)]
98
+ private def build_request(request, &blk)
99
+ method = request.method
100
+ body = request.body
101
+ req = Net::HTTPGenericRequest.new(
102
+ method.to_s.upcase,
103
+ !body.nil?,
104
+ method != :head,
105
+ URI(request.url.to_s)
106
+ )
107
+
108
+ request.headers.each { req[_1] = _2 }
109
+
110
+ case body
111
+ in nil
112
+ req["content-length"] ||= 0 unless req["transfer-encoding"]
113
+ in String
114
+ req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"]
115
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
116
+ in StringIO
117
+ req["content-length"] ||= body.size.to_s unless req["transfer-encoding"]
118
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
119
+ in Pathname | IO | Enumerator
120
+ req["transfer-encoding"] ||= "chunked" unless req["content-length"]
121
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
122
+ end
123
+
124
+ [req, req.body_stream&.method(:close)]
125
+ end
126
+
127
+ # @api private
128
+ #
129
+ # @param url [URI::Generic]
130
+ # @param deadline [Float]
131
+ # @param blk [Proc]
132
+ #
133
+ # @raise [Timeout::Error]
134
+ # @yieldparam [Net::HTTP]
135
+ private def with_pool(url, deadline:, &blk)
136
+ origin = OpenAI::Internal::Util.uri_origin(url)
137
+ pool =
138
+ @mutex.synchronize do
139
+ @pools[origin] ||= ConnectionPool.new(size: @size) do
140
+ configured_connection(url)
141
+ end
142
+ end
143
+
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
156
+ end
157
+
158
+ # @api private
159
+ #
160
+ # @param url [URI::Generic]
161
+ #
162
+ # @return [Net::HTTP]
163
+ private def configured_connection(url)
164
+ connection = nil
165
+ connection = connect(url: url)
166
+ begin
167
+ @connection_configurator&.call(connection)
168
+ rescue StandardError => e
169
+ raise ConnectionConfigurationError.new(e)
170
+ end
171
+
172
+ if connection.started?
173
+ raise ArgumentError, "connection configuration must leave the connection unstarted"
174
+ end
175
+
176
+ expected_ssl = %w[https wss].include?(url.scheme)
177
+ unless connection.use_ssl? == expected_ssl
178
+ raise ArgumentError, "connection configuration must preserve TLS for the requested URL"
179
+ end
180
+
181
+ connection.max_retries = 0
182
+ connection
183
+ rescue StandardError
184
+ begin
185
+ connection.finish if connection&.started?
186
+ rescue StandardError
187
+ nil
188
+ end
189
+ raise
190
+ end
191
+
192
+ # Closes current pooled connections. The client remains reusable and will
193
+ # create fresh pools on subsequent requests.
194
+ #
195
+ # In-flight requests are allowed to finish before their connection closes.
196
+ #
197
+ # @return [void]
198
+ def close
199
+ pools =
200
+ @mutex.synchronize do
201
+ current_pools = @pools
202
+ @pools = {}
203
+ current_pools
204
+ end
205
+ pools.each_value do |pool|
206
+ pool.shutdown { |connection| connection.finish if connection.started? }
207
+ end
208
+ nil
209
+ end
210
+
211
+ # Executes a request using a pooled Net::HTTP connection.
212
+ #
213
+ # @param request [OpenAI::HTTPClient::Request]
214
+ # @return [OpenAI::HTTPClient::Response]
215
+ def execute(request)
216
+ url = request.url
217
+ deadline = request.timeout&.then { OpenAI::Internal::Util.monotonic_secs + _1 }
218
+
219
+ req = nil
220
+ finished = false
221
+
222
+ # rubocop:disable Metrics/BlockLength
223
+ enum = Enumerator.new do |y|
224
+ next if finished
225
+
226
+ with_pool(url, deadline: deadline) do |conn|
227
+ eof = false
228
+ closing = nil
229
+ ::Thread.handle_interrupt(Object => :never) do
230
+ ::Thread.handle_interrupt(Object => :immediate) do
231
+ req, closing = build_request(request) do
232
+ calibrate_socket_timeout(conn, deadline)
233
+ end
234
+
235
+ calibrate_socket_timeout(conn, deadline)
236
+ conn.start unless conn.started?
237
+
238
+ calibrate_socket_timeout(conn, deadline)
239
+ ::Kernel.catch(:jump) do
240
+ conn.request(req) do |rsp|
241
+ y << [req, rsp]
242
+ ::Kernel.throw(:jump) if finished
243
+
244
+ rsp.read_body do |bytes|
245
+ y << bytes.force_encoding(Encoding::BINARY)
246
+ ::Kernel.throw(:jump) if finished
247
+
248
+ calibrate_socket_timeout(conn, deadline)
249
+ end
250
+ eof = true
251
+ end
252
+ end
253
+ end
254
+ ensure
255
+ begin
256
+ conn.finish if !eof && conn&.started?
257
+ ensure
258
+ closing&.call
259
+ end
260
+ end
261
+ end
262
+ rescue ConnectionConfigurationError => e
263
+ raise e.original, cause: e.original.cause
264
+ rescue Timeout::Error
265
+ raise OpenAI::Errors::APITimeoutError.new(url: url, request: req)
266
+ rescue *NETWORK_ERRORS
267
+ raise OpenAI::Errors::APIConnectionError.new(url: url, request: req)
268
+ end
269
+ # rubocop:enable Metrics/BlockLength
270
+
271
+ _, response = enum.next
272
+ body = OpenAI::Internal::Util.fused_enum(enum, external: true) do
273
+ finished = true
274
+ loop { enum.next }
275
+ end
276
+ OpenAI::HTTPClient::Response.new(
277
+ status: Integer(response.code),
278
+ headers: response.each_header.to_h,
279
+ body: body
280
+ )
281
+ end
282
+
283
+ # @param size [Integer]
284
+ # @param connection_configurator [#call, nil] A block that configures every
285
+ # SDK-created Net::HTTP connection before it is pooled and started.
286
+ def initialize(size: self.class::DEFAULT_MAX_CONNECTIONS, &connection_configurator)
287
+ super()
288
+ @mutex = Mutex.new
289
+ @size = size
290
+ @cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
291
+ @connection_configurator = connection_configurator
292
+ @pools = {}
293
+ end
294
+ end
295
+ end
@@ -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],