openai 0.78.0 → 0.80.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 (239) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/README.md +241 -8
  4. data/SECURITY.md +6 -22
  5. data/azure.md +110 -0
  6. data/bedrock.md +197 -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 +5 -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_mcp_tool_call_error.rb +84 -0
  47. data/lib/openai/models/beta/beta_response.rb +27 -0
  48. data/lib/openai/models/beta/beta_response_input_item.rb +3 -3
  49. data/lib/openai/models/beta/beta_response_item.rb +3 -3
  50. data/lib/openai/models/beta/beta_response_output_item.rb +3 -3
  51. data/lib/openai/models/beta/beta_response_output_item_added_event.rb +5 -2
  52. data/lib/openai/models/beta/beta_response_output_text_annotation_added_event.rb +201 -4
  53. data/lib/openai/models/beta/beta_response_reasoning_item.rb +5 -0
  54. data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
  55. data/lib/openai/models/beta/beta_responses_client_event.rb +46 -3
  56. data/lib/openai/models/beta/beta_responses_server_event.rb +1143 -63
  57. data/lib/openai/models/beta/beta_web_search_tool.rb +11 -1
  58. data/lib/openai/models/beta/response_compact_params.rb +18 -0
  59. data/lib/openai/models/beta/response_create_params.rb +27 -0
  60. data/lib/openai/models/chat_model.rb +1 -0
  61. data/lib/openai/models/completion_usage.rb +26 -2
  62. data/lib/openai/models/containers/file_create_params.rb +4 -0
  63. data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
  64. data/lib/openai/models/conversations/conversation_item.rb +3 -3
  65. data/lib/openai/models/file_create_params.rb +4 -0
  66. data/lib/openai/models/image_create_variation_params.rb +4 -0
  67. data/lib/openai/models/image_edit_params.rb +8 -0
  68. data/lib/openai/models/model.rb +9 -1
  69. data/lib/openai/models/responses/mcp_tool_call_error.rb +82 -0
  70. data/lib/openai/models/responses/response.rb +9 -0
  71. data/lib/openai/models/responses/response_compact_params.rb +18 -0
  72. data/lib/openai/models/responses/response_create_params.rb +9 -0
  73. data/lib/openai/models/responses/response_input_item.rb +3 -3
  74. data/lib/openai/models/responses/response_item.rb +3 -3
  75. data/lib/openai/models/responses/response_output_item.rb +3 -3
  76. data/lib/openai/models/responses/response_output_item_added_event.rb +5 -2
  77. data/lib/openai/models/responses/response_output_text_annotation_added_event.rb +201 -4
  78. data/lib/openai/models/responses/response_reasoning_item.rb +5 -0
  79. data/lib/openai/models/responses/response_stream_event.rb +1 -1
  80. data/lib/openai/models/responses/responses_client_event.rb +22 -1
  81. data/lib/openai/models/responses/responses_server_event.rb +1107 -56
  82. data/lib/openai/models/responses/web_search_tool.rb +11 -1
  83. data/lib/openai/models/responses_model.rb +5 -0
  84. data/lib/openai/models/skill_create_params.rb +4 -0
  85. data/lib/openai/models/skills/version_create_params.rb +4 -0
  86. data/lib/openai/models/uploads/part_create_params.rb +4 -0
  87. data/lib/openai/models/vector_store_search_response.rb +3 -1
  88. data/lib/openai/models/video_create_character_params.rb +4 -0
  89. data/lib/openai/models/video_create_params.rb +4 -0
  90. data/lib/openai/models/video_edit_params.rb +4 -0
  91. data/lib/openai/models/video_extend_params.rb +4 -0
  92. data/lib/openai/models.rb +9 -1
  93. data/lib/openai/net_http_client.rb +20 -6
  94. data/lib/openai/providers/azure.rb +216 -0
  95. data/lib/openai/providers/bedrock.rb +130 -17
  96. data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
  97. data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
  98. data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
  99. data/lib/openai/resources/admin/organization/projects.rb +4 -2
  100. data/lib/openai/resources/audio/transcriptions.rb +8 -0
  101. data/lib/openai/resources/audio/translations.rb +4 -0
  102. data/lib/openai/resources/beta/responses.rb +1 -1
  103. data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
  104. data/lib/openai/resources/containers/files.rb +4 -0
  105. data/lib/openai/resources/content_provenance_checks.rb +4 -0
  106. data/lib/openai/resources/conversations/items.rb +1 -1
  107. data/lib/openai/resources/evals/runs/output_items.rb +1 -1
  108. data/lib/openai/resources/files.rb +5 -1
  109. data/lib/openai/resources/images.rb +12 -0
  110. data/lib/openai/resources/responses.rb +3 -2
  111. data/lib/openai/resources/skills/versions.rb +4 -0
  112. data/lib/openai/resources/skills.rb +4 -0
  113. data/lib/openai/resources/uploads/parts.rb +4 -0
  114. data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
  115. data/lib/openai/resources/videos.rb +38 -0
  116. data/lib/openai/resources/webhooks.rb +18 -6
  117. data/lib/openai/version.rb +1 -1
  118. data/lib/openai.rb +111 -105
  119. data/rbi/openai/auth.rbi +4 -4
  120. data/rbi/openai/client.rbi +13 -3
  121. data/rbi/openai/errors.rbi +7 -0
  122. data/rbi/openai/helpers/streaming/events.rbi +12 -0
  123. data/rbi/openai/http_client.rbi +62 -2
  124. data/rbi/openai/internal/logging.rbi +192 -0
  125. data/rbi/openai/internal/provider.rbi +3 -0
  126. data/rbi/openai/internal/read_io_adapter.rbi +6 -1
  127. data/rbi/openai/internal/transport/base_client.rbi +61 -7
  128. data/rbi/openai/internal/type/base_model.rbi +24 -1
  129. data/rbi/openai/internal/type/base_page.rbi +13 -2
  130. data/rbi/openai/internal/type/base_stream.rbi +15 -4
  131. data/rbi/openai/internal/util.rbi +26 -2
  132. data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
  133. data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
  134. data/rbi/openai/models/all_models.rbi +25 -0
  135. data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
  136. data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
  137. data/rbi/openai/models/beta/beta_mcp_tool_call_error.rbi +122 -0
  138. data/rbi/openai/models/beta/beta_response.rbi +47 -0
  139. data/rbi/openai/models/beta/beta_response_input_item.rbi +27 -3
  140. data/rbi/openai/models/beta/beta_response_item.rbi +27 -3
  141. data/rbi/openai/models/beta/beta_response_output_item.rbi +27 -3
  142. data/rbi/openai/models/beta/beta_response_output_item_added_event.rbi +8 -2
  143. data/rbi/openai/models/beta/beta_response_output_text_annotation_added_event.rbi +297 -5
  144. data/rbi/openai/models/beta/beta_response_reasoning_item.rbi +10 -0
  145. data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
  146. data/rbi/openai/models/beta/beta_responses_client_event.rbi +69 -1
  147. data/rbi/openai/models/beta/beta_responses_server_event.rbi +3945 -54
  148. data/rbi/openai/models/beta/beta_web_search_tool.rbi +15 -0
  149. data/rbi/openai/models/beta/response_compact_params.rbi +30 -0
  150. data/rbi/openai/models/beta/response_create_params.rbi +47 -0
  151. data/rbi/openai/models/chat_model.rbi +2 -0
  152. data/rbi/openai/models/completion_usage.rbi +39 -6
  153. data/rbi/openai/models/containers/file_create_params.rbi +8 -0
  154. data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
  155. data/rbi/openai/models/conversations/conversation_item.rbi +12 -3
  156. data/rbi/openai/models/file_create_params.rbi +8 -0
  157. data/rbi/openai/models/image_create_variation_params.rbi +8 -0
  158. data/rbi/openai/models/image_edit_params.rbi +16 -0
  159. data/rbi/openai/models/model.rbi +14 -1
  160. data/rbi/openai/models/responses/mcp_tool_call_error.rbi +120 -0
  161. data/rbi/openai/models/responses/response.rbi +17 -0
  162. data/rbi/openai/models/responses/response_compact_params.rbi +30 -0
  163. data/rbi/openai/models/responses/response_create_params.rbi +17 -0
  164. data/rbi/openai/models/responses/response_input_item.rbi +27 -3
  165. data/rbi/openai/models/responses/response_item.rbi +27 -3
  166. data/rbi/openai/models/responses/response_output_item.rbi +27 -3
  167. data/rbi/openai/models/responses/response_output_item_added_event.rbi +8 -2
  168. data/rbi/openai/models/responses/response_output_text_annotation_added_event.rbi +297 -5
  169. data/rbi/openai/models/responses/response_reasoning_item.rbi +10 -0
  170. data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
  171. data/rbi/openai/models/responses/responses_client_event.rbi +36 -0
  172. data/rbi/openai/models/responses/responses_server_event.rbi +3385 -53
  173. data/rbi/openai/models/responses/web_search_tool.rbi +15 -0
  174. data/rbi/openai/models/responses_model.rbi +25 -0
  175. data/rbi/openai/models/skill_create_params.rbi +8 -0
  176. data/rbi/openai/models/skills/version_create_params.rbi +8 -0
  177. data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
  178. data/rbi/openai/models/video_create_character_params.rbi +8 -0
  179. data/rbi/openai/models/video_create_params.rbi +8 -0
  180. data/rbi/openai/models/video_edit_params.rbi +8 -0
  181. data/rbi/openai/models/video_extend_params.rbi +8 -0
  182. data/rbi/openai/net_http_client.rbi +2 -2
  183. data/rbi/openai/providers.rbi +11 -0
  184. data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
  185. data/rbi/openai/resources/audio/translations.rbi +7 -1
  186. data/rbi/openai/resources/beta/responses.rbi +8 -0
  187. data/rbi/openai/resources/containers/files.rbi +4 -0
  188. data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
  189. data/rbi/openai/resources/files.rbi +5 -1
  190. data/rbi/openai/resources/images.rbi +20 -0
  191. data/rbi/openai/resources/responses.rbi +8 -0
  192. data/rbi/openai/resources/skills/versions.rbi +4 -0
  193. data/rbi/openai/resources/skills.rbi +4 -0
  194. data/rbi/openai/resources/uploads/parts.rbi +4 -0
  195. data/rbi/openai/resources/videos.rbi +16 -0
  196. data/sig/openai/client.rbs +6 -2
  197. data/sig/openai/errors.rbs +5 -0
  198. data/sig/openai/http_client.rbs +37 -2
  199. data/sig/openai/internal/logging.rbs +88 -0
  200. data/sig/openai/internal/provider.rbs +3 -1
  201. data/sig/openai/internal/read_io_adapter.rbs +3 -1
  202. data/sig/openai/internal/transport/base_client.rbs +33 -5
  203. data/sig/openai/internal/type/base_model.rbs +13 -0
  204. data/sig/openai/internal/type/base_page.rbs +8 -1
  205. data/sig/openai/internal/type/base_stream.rbs +8 -2
  206. data/sig/openai/internal/util.rbs +12 -0
  207. data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
  208. data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
  209. data/sig/openai/models/all_models.rbs +10 -0
  210. data/sig/openai/models/beta/beta_mcp_tool_call_error.rbs +75 -0
  211. data/sig/openai/models/beta/beta_response.rbs +14 -1
  212. data/sig/openai/models/beta/beta_response_input_item.rbs +4 -4
  213. data/sig/openai/models/beta/beta_response_item.rbs +4 -4
  214. data/sig/openai/models/beta/beta_response_output_item.rbs +4 -4
  215. data/sig/openai/models/beta/beta_response_output_text_annotation_added_event.rbs +149 -4
  216. data/sig/openai/models/beta/beta_responses_client_event.rbs +21 -1
  217. data/sig/openai/models/beta/beta_responses_server_event.rbs +2005 -53
  218. data/sig/openai/models/beta/beta_web_search_tool.rbs +7 -0
  219. data/sig/openai/models/beta/response_compact_params.rbs +12 -0
  220. data/sig/openai/models/beta/response_create_params.rbs +14 -1
  221. data/sig/openai/models/chat_model.rbs +2 -0
  222. data/sig/openai/models/completion_usage.rbs +27 -6
  223. data/sig/openai/models/conversations/conversation_item.rbs +4 -4
  224. data/sig/openai/models/model.rbs +12 -2
  225. data/sig/openai/models/responses/mcp_tool_call_error.rbs +73 -0
  226. data/sig/openai/models/responses/response.rbs +2 -1
  227. data/sig/openai/models/responses/response_compact_params.rbs +12 -0
  228. data/sig/openai/models/responses/response_create_params.rbs +2 -1
  229. data/sig/openai/models/responses/response_input_item.rbs +4 -4
  230. data/sig/openai/models/responses/response_item.rbs +4 -4
  231. data/sig/openai/models/responses/response_output_item.rbs +4 -4
  232. data/sig/openai/models/responses/response_output_text_annotation_added_event.rbs +149 -4
  233. data/sig/openai/models/responses/responses_client_event.rbs +9 -1
  234. data/sig/openai/models/responses/responses_server_event.rbs +1834 -53
  235. data/sig/openai/models/responses/web_search_tool.rbs +7 -0
  236. data/sig/openai/models/responses_model.rbs +10 -0
  237. data/sig/openai/net_http_client.rbs +2 -2
  238. data/sig/openai/providers.rbs +7 -0
  239. metadata +30 -1
data/bedrock.md ADDED
@@ -0,0 +1,197 @@
1
+ # Amazon Bedrock
2
+
3
+ Configure the standard `OpenAI::Client` with the Bedrock provider to use [Amazon Bedrock's OpenAI-compatible API](https://docs.aws.amazon.com/bedrock/latest/userguide/models-api-compatibility.html):
4
+
5
+ ```ruby
6
+ require "openai"
7
+
8
+ client = OpenAI::Client.new(
9
+ provider: OpenAI::Providers.bedrock(region: "us-west-2")
10
+ )
11
+
12
+ response = client.responses.create(
13
+ model: ENV.fetch("BEDROCK_MODEL"),
14
+ input: "Say hello!"
15
+ )
16
+
17
+ puts(response.output_text)
18
+ ```
19
+
20
+ The provider uses `https://bedrock-mantle.<region>.api.aws/v1` by default and exposes the normal SDK resources. AWS controls which endpoints and features are supported; unsupported calls surface as normal API errors.
21
+
22
+ ## Bedrock Runtime
23
+
24
+ Pass `endpoint: :runtime` to use the Bedrock Runtime OpenAI-compatible endpoint. The provider routes requests to `https://bedrock-runtime.<region>.amazonaws.com/openai/v1` and uses the AWS SigV4 signing service `bedrock` when AWS credentials are selected:
25
+
26
+ ```ruby
27
+ require "openai"
28
+
29
+ client = OpenAI::Client.new(
30
+ provider: OpenAI::Providers.bedrock(
31
+ endpoint: :runtime,
32
+ region: "us-east-1"
33
+ )
34
+ )
35
+
36
+ completion = client.chat.completions.create(
37
+ model: "us.openai.gpt-5.6-sol",
38
+ messages: [{role: :user, content: "Say hello!"}]
39
+ )
40
+
41
+ puts(completion.choices.first.message.content)
42
+ ```
43
+
44
+ Use the inference profile configured for your AWS account, for example `us.openai.gpt-5.6-sol`, `us.openai.gpt-5.6-terra`, or `us.openai.gpt-5.6-luna`. Global inference profiles, such as `global.openai.gpt-5.6-sol`, require the appropriate AWS account and regional permissions. Set `BEDROCK_MODEL` to override the model used by [`examples/bedrock_runtime.rb`](examples/bedrock_runtime.rb). The example uses Chat Completions by default; set `BEDROCK_RESPONSES=1` to also use Responses or `BEDROCK_STREAM=1` to also stream Chat Completions. Set `BEDROCK_AUTH_MODE=sigv4` to explicitly select AWS authentication when a bearer credential is also present.
45
+
46
+ The same client supports Responses and response streaming when AWS makes those APIs available for the selected model:
47
+
48
+ ```ruby
49
+ response = client.responses.create(
50
+ model: "us.openai.gpt-5.6-sol",
51
+ input: "Say hello!"
52
+ )
53
+
54
+ stream = client.chat.completions.stream(
55
+ model: "us.openai.gpt-5.6-sol",
56
+ messages: [{role: :user, content: "Say hello!"}]
57
+ )
58
+
59
+ stream.text.each { |text| print(text) }
60
+ ```
61
+
62
+ The existing Mantle endpoint remains the default. Pass `endpoint: :mantle` when you need to explicitly select it. Both endpoint symbols and the strings `"runtime"` and `"mantle"` are accepted.
63
+
64
+ | Endpoint | Default URL | SigV4 signing service |
65
+ | --- | --- | --- |
66
+ | `:mantle` (default) | `https://bedrock-mantle.<region>.api.aws/v1` | `bedrock-mantle` |
67
+ | `:runtime` | `https://bedrock-runtime.<region>.amazonaws.com/openai/v1` | `bedrock` |
68
+
69
+ Runtime URLs automatically use the correct DNS suffix for AWS China, European Sovereign Cloud, and isolated partitions. Canonical Runtime, FIPS, and dual-stack `base_url` values automatically select Runtime when `endpoint` is omitted. Canonical AWS endpoint URLs must use HTTPS, match the selected endpoint family, and agree with the configured AWS region.
70
+
71
+ ## Endpoint configuration
72
+
73
+ The region is resolved from the explicit `region` option, `AWS_REGION`, `AWS_DEFAULT_REGION`, or the selected AWS profile. Override the endpoint with `base_url` or `AWS_BEDROCK_BASE_URL`:
74
+
75
+ ```ruby
76
+ provider = OpenAI::Providers.bedrock(
77
+ endpoint: :runtime,
78
+ region: "us-west-2",
79
+ base_url: "https://bedrock.example.com/openai/v1"
80
+ )
81
+ ```
82
+
83
+ Custom endpoints using AWS credentials default to Mantle signing when `endpoint` is omitted; pass `endpoint: :runtime` to use Runtime signing. Local HTTP proxies remain supported when the endpoint is selected explicitly. Custom endpoints using bearer credentials can omit `endpoint`; in that case the default remains Mantle.
84
+
85
+ ## Authentication
86
+
87
+ The provider selects authentication in this order:
88
+
89
+ 1. One explicit mode passed to `bedrock(...)`: `api_key`, `token_provider`, static AWS credentials, `profile`, or `credentials_provider`.
90
+ 2. The Bedrock bearer credential in `AWS_BEARER_TOKEN_BEDROCK`.
91
+ 3. The standard AWS credential chain.
92
+
93
+ Explicit bearer and AWS credential modes are mutually exclusive. Configure only one explicit AWS mode at a time.
94
+
95
+ ### AWS credentials and SigV4
96
+
97
+ AWS authentication is an optional integration so applications that do not use Bedrock do not install or load AWS packages. Add the AWS SDK core gem to your application:
98
+
99
+ ```ruby
100
+ gem "aws-sdk-core", "~> 3"
101
+ ```
102
+
103
+ Then run `bundle install`. If you are not using Bundler, run `gem install aws-sdk-core`.
104
+
105
+ Omit explicit authentication to use environment credentials, the shared credentials and config files, SSO or assume-role profiles, web identity, ECS credentials, or EC2 instance credentials:
106
+
107
+ ```ruby
108
+ client = OpenAI::Client.new(
109
+ provider: OpenAI::Providers.bedrock(region: "us-west-2")
110
+ )
111
+ ```
112
+
113
+ Select a named profile with:
114
+
115
+ ```ruby
116
+ client = OpenAI::Client.new(
117
+ provider: OpenAI::Providers.bedrock(
118
+ region: "us-west-2",
119
+ profile: "engineering"
120
+ )
121
+ )
122
+ ```
123
+
124
+ You can omit `region` when that profile defines one in `~/.aws/config`.
125
+
126
+ Pass temporary credentials directly, including the session token:
127
+
128
+ ```ruby
129
+ client = OpenAI::Client.new(
130
+ provider: OpenAI::Providers.bedrock(
131
+ region: "us-west-2",
132
+ access_key_id: ENV.fetch("AWS_ACCESS_KEY_ID"),
133
+ secret_access_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
134
+ session_token: ENV["AWS_SESSION_TOKEN"]
135
+ )
136
+ )
137
+ ```
138
+
139
+ For credentials managed by your application, pass an AWS credential provider or a callable that returns `Aws::Credentials`. It is consulted before every request attempt, including retries:
140
+
141
+ ```ruby
142
+ credentials_provider = lambda do
143
+ Aws::Credentials.new(
144
+ ENV.fetch("AWS_ACCESS_KEY_ID"),
145
+ ENV.fetch("AWS_SECRET_ACCESS_KEY"),
146
+ ENV["AWS_SESSION_TOKEN"]
147
+ )
148
+ end
149
+
150
+ client = OpenAI::Client.new(
151
+ provider: OpenAI::Providers.bedrock(
152
+ region: "us-west-2",
153
+ credentials_provider: credentials_provider
154
+ )
155
+ )
156
+ ```
157
+
158
+ The provider signs each finalized attempt with AWS SigV4 service name `bedrock-mantle` for Mantle or `bedrock` for Runtime. Standard JSON API requests have replayable bodies and work normally. SigV4 rejects one-shot request streams before sending, and signed requests do not automatically follow redirects because a new target requires a new signature. Response streaming is unaffected.
159
+
160
+ ### Bearer authentication
161
+
162
+ Pass a Bedrock bearer credential directly, set `AWS_BEARER_TOKEN_BEDROCK`, or use a callable to resolve a fresh token before every attempt:
163
+
164
+ ```ruby
165
+ client = OpenAI::Client.new(
166
+ provider: OpenAI::Providers.bedrock(
167
+ region: "us-west-2",
168
+ api_key: ENV.fetch("BEDROCK_API_KEY")
169
+ )
170
+ )
171
+ ```
172
+
173
+ ```ruby
174
+ client = OpenAI::Client.new(
175
+ provider: OpenAI::Providers.bedrock(
176
+ region: "us-west-2",
177
+ token_provider: -> { refresh_bedrock_token }
178
+ )
179
+ )
180
+ ```
181
+
182
+ Bearer authentication does not load or require `aws-sdk-core`. Passing `api_key: nil` explicitly skips `AWS_BEARER_TOKEN_BEDROCK` and selects AWS authentication.
183
+
184
+ ## Live verification
185
+
186
+ Real AWS requests are disabled in the test suite unless `BEDROCK_LIVE_TEST=1` is explicitly set. To exercise Runtime against all three default US inference profiles, run:
187
+
188
+ ```sh
189
+ AWS_REGION=us-east-1 BEDROCK_LIVE_TEST=1 \
190
+ bundle exec ruby test/openai/providers/bedrock_live_test.rb
191
+ ```
192
+
193
+ Set `BEDROCK_AUTH_MODE` to `auto`, `bearer`, `token-provider`, `sigv4`, `static`, or `profile` to select credentials. Use `BEDROCK_MODEL` for one inference profile or `BEDROCK_LIVE_MODELS` for a comma-separated list. Set `BEDROCK_LIVE_RESPONSES=1` to also verify Responses and `BEDROCK_LIVE_STREAM=1` to verify Chat Completions streaming.
194
+
195
+ ## Security
196
+
197
+ Use Bedrock from a server-side Ruby runtime. Prefer roles, profiles, SSO, and temporary credentials over long-lived static keys. Do not send AWS access keys, session tokens, bearer credentials, or signed authorization headers to browsers or application logs.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This example sends an API-key request over mutual TLS by configuring the
5
+ # SDK's pooled Net::HTTP connections with Ruby's native OpenSSL support. Set
6
+ # OPENAI_CLIENT_CERTIFICATE_CHAIN to a PEM file containing the leaf certificate
7
+ # followed by any required intermediates, OPENAI_CLIENT_KEY to the matching
8
+ # private key, and OPENAI_MODEL to a model available to the project.
9
+ # OPENAI_CLIENT_KEY_PASSPHRASE is optional.
10
+
11
+ require_relative "../lib/openai"
12
+
13
+ # Set this to the mTLS endpoint for your data residency. EU Data Residency uses
14
+ # https://mtls-eu.api.openai.com/v1. For enrollment and current endpoint
15
+ # guidance, see:
16
+ # https://help.openai.com/en/articles/10876024-openai-mutual-tls-beta-program
17
+ mtls_endpoint = URI("https://mtls.api.openai.com/v1")
18
+ certificates = OpenSSL::X509::Certificate.load(
19
+ File.binread(ENV.fetch("OPENAI_CLIENT_CERTIFICATE_CHAIN"))
20
+ )
21
+ raise ArgumentError, "Expected a client certificate" if certificates.empty?
22
+
23
+ leaf_certificate, *intermediates = certificates
24
+ private_key = OpenSSL::PKey.read(
25
+ File.binread(ENV.fetch("OPENAI_CLIENT_KEY")),
26
+ ENV["OPENAI_CLIENT_KEY_PASSPHRASE"]
27
+ )
28
+ unless leaf_certificate.check_private_key(private_key)
29
+ raise ArgumentError, "The client certificate and private key do not match"
30
+ end
31
+
32
+ now = Time.now
33
+ raise ArgumentError, "The client certificate is not yet valid" if now < leaf_certificate.not_before
34
+ raise ArgumentError, "The client certificate has expired" if now > leaf_certificate.not_after
35
+
36
+ mtls_destination = [mtls_endpoint.host, mtls_endpoint.port]
37
+ http_client = OpenAI::NetHTTPClient.new do |http|
38
+ unless http.use_ssl? && mtls_destination == [http.address, http.port]
39
+ raise ArgumentError, "Refusing to present the client certificate to an unexpected origin"
40
+ end
41
+
42
+ http.cert = leaf_certificate
43
+ http.extra_chain_cert = intermediates
44
+ http.key = private_key
45
+ end
46
+
47
+ client = OpenAI::Client.new(
48
+ api_key: ENV.fetch("OPENAI_API_KEY"),
49
+ base_url: mtls_endpoint.to_s,
50
+ http_client: http_client
51
+ )
52
+
53
+ begin
54
+ response = client.responses.create(
55
+ model: ENV.fetch("OPENAI_MODEL"),
56
+ input: "Reply with: mTLS transport configured"
57
+ )
58
+ puts(response.output_text)
59
+ ensure
60
+ http_client.close
61
+ end
@@ -6,12 +6,23 @@ module OpenAI
6
6
  attr_reader :client_id, :identity_provider_id, :service_account_id, :provider, :refresh_buffer_seconds
7
7
 
8
8
  def initialize(
9
- identity_provider_id:,
10
- service_account_id:,
11
9
  provider:,
10
+ identity_provider_id: ENV["IDENTITY_PROVIDER_ID"],
11
+ service_account_id: ENV["SERVICE_ACCOUNT_ID"],
12
12
  client_id: nil,
13
13
  refresh_buffer_seconds: 1200
14
14
  )
15
+ if identity_provider_id.to_s.strip.empty?
16
+ raise ArgumentError,
17
+ "identity_provider_id must not be blank; pass identity_provider_id: " \
18
+ "or set IDENTITY_PROVIDER_ID"
19
+ end
20
+
21
+ if service_account_id.to_s.strip.empty?
22
+ raise ArgumentError,
23
+ "service_account_id must not be blank; pass service_account_id: or set SERVICE_ACCOUNT_ID"
24
+ end
25
+
15
26
  @client_id = client_id&.to_s
16
27
  @identity_provider_id = identity_provider_id.to_s
17
28
  @service_account_id = service_account_id.to_s
@@ -87,7 +87,8 @@ module OpenAI
87
87
  token_type = @config.provider.token_type
88
88
  subject_token_type = SUBJECT_TOKEN_TYPES.fetch(token_type) do
89
89
  raise ArgumentError,
90
- "Unsupported token type: #{token_type.inspect}. Supported types: #{SUBJECT_TOKEN_TYPES.keys.join(', ')}"
90
+ "Unsupported token type: #{token_type.inspect}. " \
91
+ "Supported types: #{SUBJECT_TOKEN_TYPES.keys.join(', ')}"
91
92
  end
92
93
 
93
94
  request = Net::HTTP::Post.new(@token_exchange_url)
data/lib/openai/client.rb CHANGED
@@ -229,9 +229,12 @@ module OpenAI
229
229
  # @param base_url [String, nil] Override the default base URL for the API, e.g.,
230
230
  # `"https://api.example.com/v2/"`. Defaults to `ENV["OPENAI_BASE_URL"]`
231
231
  #
232
+ # @param default_headers [Hash{String=>String, nil}, nil] Extra headers to send
233
+ # with every request. Explicit values override `ENV["OPENAI_CUSTOM_HEADERS"]`.
234
+ #
232
235
  # @param max_retries [Integer] Max number of retries to attempt after a failed retryable request.
233
236
  #
234
- # @param timeout [Float]
237
+ # @param timeout [Float, nil]
235
238
  #
236
239
  # @param initial_retry_delay [Float]
237
240
  #
@@ -239,6 +242,13 @@ module OpenAI
239
242
  #
240
243
  # @param http_client [#execute, nil] The HTTP client used to
241
244
  # execute SDK requests. Defaults to {OpenAI::NetHTTPClient}.
245
+ #
246
+ # @param logger [#debug, #info, #warn, #error, nil] Logger for SDK request diagnostics.
247
+ #
248
+ # @param log_level [Symbol, String, nil] SDK request log level. Defaults to
249
+ # `ENV["OPENAI_LOG"]`, `:info` when `logger` is provided, and `:off` otherwise.
250
+ #
251
+ # @param on_retry [Proc, nil] Callback invoked immediately before an API retry delay.
242
252
  def initialize(
243
253
  api_key: OpenAI::Internal::OMIT,
244
254
  admin_api_key: OpenAI::Internal::OMIT,
@@ -248,11 +258,15 @@ module OpenAI
248
258
  webhook_secret: OpenAI::Internal::OMIT,
249
259
  provider: nil,
250
260
  base_url: OpenAI::Internal::OMIT,
261
+ default_headers: nil,
251
262
  max_retries: self.class::DEFAULT_MAX_RETRIES,
252
263
  timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
253
264
  initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
254
265
  max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY,
255
- http_client: nil
266
+ http_client: nil,
267
+ logger: nil,
268
+ log_level: nil,
269
+ on_retry: nil
256
270
  )
257
271
  provider_runtime = nil
258
272
  unless provider.nil?
@@ -301,24 +315,30 @@ module OpenAI
301
315
 
302
316
  if provider_runtime.nil? && api_key.nil? && admin_api_key.nil? && workload_identity.nil?
303
317
  raise ArgumentError,
304
- "Missing credentials. Please pass an `api_key`, `workload_identity`, `admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` environment variable."
318
+ "Missing credentials. Please pass an `api_key`, `workload_identity`, " \
319
+ "`admin_api_key`, or set the `OPENAI_API_KEY` or `OPENAI_ADMIN_KEY` " \
320
+ "environment variable."
305
321
  end
306
322
 
307
323
  headers = {
308
324
  "openai-organization" => (@organization = organization&.to_s),
309
325
  "openai-project" => (@project = project&.to_s)
310
326
  }
327
+ parsed = {}
311
328
  custom_headers_env = ENV["OPENAI_CUSTOM_HEADERS"] unless provider_runtime
312
329
  unless custom_headers_env.nil?
313
- parsed = {}
314
330
  custom_headers_env.split("\n").each do |line|
315
331
  colon = line.index(":")
316
332
  unless colon.nil?
317
333
  parsed[line[0...colon].strip] = line[(colon + 1)..].strip
318
334
  end
319
335
  end
320
- headers = parsed.merge(headers)
321
336
  end
337
+ client_headers = OpenAI::Internal::Util.normalized_headers(default_headers.to_h)
338
+ unless provider_runtime.nil?
339
+ provider_runtime.authentication_headers.each { client_headers.delete(_1) }
340
+ end
341
+ headers = OpenAI::Internal::Util.normalized_headers(parsed, headers, client_headers)
322
342
 
323
343
  if workload_identity.nil?
324
344
  @api_key = api_key&.to_s
@@ -341,7 +361,10 @@ module OpenAI
341
361
  initial_retry_delay: initial_retry_delay,
342
362
  max_retry_delay: max_retry_delay,
343
363
  headers: headers,
344
- http_client: http_client
364
+ http_client: http_client,
365
+ logger: logger,
366
+ log_level: log_level,
367
+ on_retry: on_retry
345
368
  )
346
369
 
347
370
  @completions = OpenAI::Resources::Completions.new(client: self)
data/lib/openai/errors.rb CHANGED
@@ -58,6 +58,13 @@ module OpenAI
58
58
  # @return [String, nil]
59
59
  attr_accessor :type
60
60
 
61
+ # The ID of the API request, returned via the `x-request-id` response
62
+ # header. This is nil when no HTTP response was received or the response
63
+ # did not include the header.
64
+ #
65
+ # @return [String, nil]
66
+ def request_id = headers&.[]("x-request-id")
67
+
61
68
  # @api private
62
69
  #
63
70
  # @param url [URI::Generic]
@@ -269,7 +276,10 @@ module OpenAI
269
276
  attr_reader :error_code
270
277
 
271
278
  def initialize(status:, body:, headers:)
272
- @error_code = OpenAI::Internal::Type::Converter.coerce(OpenAI::Models::OAuthErrorCode, body&.dig(:error))
279
+ @error_code = OpenAI::Internal::Type::Converter.coerce(
280
+ OpenAI::Models::OAuthErrorCode,
281
+ body&.dig(:error)
282
+ )
273
283
 
274
284
  message =
275
285
  if body&.dig(:error_description)
@@ -8,6 +8,7 @@ module OpenAI
8
8
 
9
9
  def initialize(raw_stream:, response_format: nil, input_tools: nil)
10
10
  @raw_stream = raw_stream
11
+ @last_response = raw_stream.last_response
11
12
  @state = ChatCompletionStreamState.new(
12
13
  response_format: response_format,
13
14
  input_tools: input_tools
@@ -33,7 +34,7 @@ module OpenAI
33
34
  end
34
35
 
35
36
  def until_done
36
- each {} # rubocop:disable Lint/EmptyBlock
37
+ each { |_event| next }
37
38
  self
38
39
  end
39
40
 
@@ -507,8 +508,7 @@ module OpenAI
507
508
 
508
509
  index = delta_entry[:index] || delta_entry["index"]
509
510
  if index.nil?
510
- raise RuntimeError,
511
- "Expected list delta entry to have an `index` key; #{delta_entry}"
511
+ raise "Expected list delta entry to have an `index` key; #{delta_entry}"
512
512
  end
513
513
  unless index.is_a?(Integer)
514
514
  raise TypeError,
@@ -10,6 +10,7 @@ module OpenAI
10
10
  @text_format = text_format
11
11
  @starting_after = starting_after
12
12
  @raw_stream = raw_stream
13
+ @last_response = raw_stream.last_response
13
14
  @iterator = iterator
14
15
  @state = ResponseStreamState.new(
15
16
  text_format: text_format
@@ -17,7 +18,7 @@ module OpenAI
17
18
  end
18
19
 
19
20
  def until_done
20
- each {} # rubocop:disable Lint/EmptyBlock
21
+ each { |_event| next }
21
22
  self
22
23
  end
23
24
 
@@ -35,7 +36,7 @@ module OpenAI
35
36
  def get_final_response
36
37
  until_done
37
38
  response = @state.completed_response
38
- raise RuntimeError.new("Didn't receive a 'response.completed' event") unless response
39
+ raise "Didn't receive a 'response.completed' event" unless response
39
40
  response
40
41
  end
41
42
 
@@ -218,10 +219,8 @@ module OpenAI
218
219
  parsed = JSON.parse(text, symbolize_names: true)
219
220
  OpenAI::Internal::Type::Converter.coerce(@text_format, parsed)
220
221
  rescue JSON::ParserError => e
221
- raise RuntimeError.new(
222
- "Failed to parse structured text as JSON for #{@text_format}: #{e.message}. " \
223
- "Raw text: #{text.inspect}"
224
- )
222
+ raise "Failed to parse structured text as JSON for #{@text_format}: #{e.message}. " \
223
+ "Raw text: #{text.inspect}"
225
224
  end
226
225
  end
227
226
  end
@@ -25,26 +25,27 @@ module OpenAI
25
25
  #
26
26
  # @return [Hash{Symbol=>Object}]
27
27
  def to_json_schema_inner(state:)
28
+ field_values = fields.values
29
+ duplicate = field_values.map { _1.fetch(:api_name) }.tally.find { _2 > 1 }
30
+ raise ArgumentError.new("Duplicate API field name: #{duplicate.first}") if duplicate
31
+
28
32
  OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: self) do
29
33
  path = state.fetch(:path)
30
- properties = fields.to_h do |name, field|
31
- type, nilable, meta = field.fetch_values(:type, :nilable, :meta)
32
- new_state = {**state, path: [*path, ".#{name}"]}
34
+ properties = field_values.to_h do |field|
35
+ api_name, type, nilable, meta = field.fetch_values(:api_name, :type, :nilable, :meta)
36
+ new_state = {**state, path: [*path, ".#{api_name}"]}
33
37
 
34
- schema =
35
- case type
36
- in OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
37
- type.to_json_schema_inner(state: new_state)
38
- else
39
- OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(
40
- type,
41
- state: new_state
42
- )
43
- end
38
+ schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(
39
+ type,
40
+ state: new_state
41
+ )
44
42
  schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema) if nilable
45
- OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(schema, meta: meta)
43
+ OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(
44
+ schema,
45
+ meta: meta.except(:api_name)
46
+ )
46
47
 
47
- [name, schema]
48
+ [api_name, schema]
48
49
  end
49
50
 
50
51
  {
@@ -59,10 +60,10 @@ module OpenAI
59
60
 
60
61
  class << self
61
62
  def optional(...)
62
- # rubocop:disable Layout/LineLength
63
- message = "`optional` is not supported for structured output APIs, use `#required` with `nil?: true` instead"
64
- # rubocop:enable Layout/LineLength
65
- raise RuntimeError.new(message)
63
+ message =
64
+ "`optional` is not supported for structured output APIs, " \
65
+ "use `#required` with `nil?: true` instead"
66
+ raise message
66
67
  end
67
68
  end
68
69
  end
@@ -98,7 +98,10 @@ module OpenAI
98
98
  }
99
99
  defs.store(type, stored)
100
100
  schema = blk.call
101
- ref_path.replace("#/$defs/#{path.join('/')}")
101
+ definition_name = path.map { _1.gsub("~", "~0").gsub("/", "~1") }.join("/")
102
+ pointer_token = definition_name.gsub("~", "~0").gsub("/", "~1")
103
+ escaped_name = URI::RFC2396_PARSER.escape(pointer_token, /[^A-Za-z0-9._~-]/)
104
+ ref_path.replace("#/$defs/#{escaped_name}")
102
105
  stored.update(schema)
103
106
  ref
104
107
  end
@@ -140,7 +143,10 @@ module OpenAI
140
143
  no_refs.each { _1.replace(_1.except(:$ref).merge(sch)) }
141
144
  end
142
145
 
143
- xformed = reused_defs.transform_keys { _1.delete_prefix("#/$defs/") }
146
+ xformed = reused_defs.transform_keys do
147
+ pointer_token = URI::RFC2396_PARSER.unescape(_1.delete_prefix("#/$defs/"))
148
+ pointer_token.gsub("~1", "/").gsub("~0", "~")
149
+ end
144
150
  xformed.empty? ? schema : {"$defs": xformed}.update(schema)
145
151
  end
146
152
 
@@ -190,9 +196,10 @@ module OpenAI
190
196
  OpenAI::UnionOf
191
197
  OpenAI::BaseModel
192
198
  ]
193
- # rubocop:disable Layout/LineLength
194
- message = "#{type} does not implement the #{OpenAI::Helpers::StructuredOutput::JsonSchemaConverter} interface. Please use one of the supported types: #{models}"
195
- # rubocop:enable Layout/LineLength
199
+ message =
200
+ "#{type} does not implement the " \
201
+ "#{OpenAI::Helpers::StructuredOutput::JsonSchemaConverter} interface. " \
202
+ "Please use one of the supported types: #{models}"
196
203
  raise ArgumentError.new(message)
197
204
  end
198
205
  end