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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/README.md +341 -10
- data/SECURITY.md +6 -22
- data/azure.md +110 -0
- data/bedrock.md +132 -0
- data/examples/mtls_custom_http_client.rb +61 -0
- data/lib/openai/auth/workload_identity.rb +13 -2
- data/lib/openai/auth/workload_identity_auth.rb +2 -1
- data/lib/openai/client.rb +34 -14
- data/lib/openai/errors.rb +11 -1
- data/lib/openai/helpers/streaming/chat_completion_stream.rb +3 -3
- data/lib/openai/helpers/streaming/response_stream.rb +5 -6
- data/lib/openai/helpers/structured_output/base_model.rb +20 -19
- data/lib/openai/helpers/structured_output/json_schema_converter.rb +12 -5
- data/lib/openai/http_client.rb +189 -0
- data/lib/openai/internal/conversation_cursor_page.rb +5 -6
- data/lib/openai/internal/cursor_page.rb +3 -3
- data/lib/openai/internal/logging.rb +419 -0
- data/lib/openai/internal/next_cursor_page.rb +5 -6
- data/lib/openai/internal/page.rb +2 -2
- data/lib/openai/internal/provider.rb +7 -1
- data/lib/openai/internal/read_io_adapter.rb +161 -0
- data/lib/openai/internal/stream.rb +2 -2
- data/lib/openai/internal/transport/base_client.rb +216 -57
- data/lib/openai/internal/type/array_of.rb +6 -4
- data/lib/openai/internal/type/base_model.rb +39 -1
- data/lib/openai/internal/type/base_page.rb +19 -2
- data/lib/openai/internal/type/base_stream.rb +40 -8
- data/lib/openai/internal/type/hash_of.rb +3 -3
- data/lib/openai/internal/util.rb +59 -112
- data/lib/openai/models/admin/organization/audit_log_list_params.rb +3 -1
- data/lib/openai/models/admin/organization/audit_log_list_response.rb +33 -11
- data/lib/openai/models/admin/organization/groups/role_list_response.rb +5 -1
- data/lib/openai/models/admin/organization/groups/role_retrieve_response.rb +5 -1
- data/lib/openai/models/admin/organization/projects/groups/role_list_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/groups/role_retrieve_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/service_account_create_response.rb +3 -1
- data/lib/openai/models/admin/organization/projects/users/role_list_response.rb +6 -1
- data/lib/openai/models/admin/organization/projects/users/role_retrieve_response.rb +6 -1
- data/lib/openai/models/admin/organization/users/role_list_response.rb +5 -1
- data/lib/openai/models/admin/organization/users/role_retrieve_response.rb +5 -1
- data/lib/openai/models/all_models.rb +3 -0
- data/lib/openai/models/audio/transcription_create_params.rb +7 -1
- data/lib/openai/models/audio/translation_create_params.rb +7 -1
- data/lib/openai/models/beta/beta_response.rb +9 -0
- data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +28 -3
- data/lib/openai/models/beta/beta_responses_server_event.rb +1037 -61
- data/lib/openai/models/beta/response_compact_params.rb +9 -0
- data/lib/openai/models/beta/response_create_params.rb +9 -0
- data/lib/openai/models/containers/file_create_params.rb +4 -0
- data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
- data/lib/openai/models/file_create_params.rb +4 -0
- data/lib/openai/models/image_create_variation_params.rb +4 -0
- data/lib/openai/models/image_edit_params.rb +8 -0
- data/lib/openai/models/responses/response_compact_params.rb +9 -0
- data/lib/openai/models/responses/response_stream_event.rb +1 -1
- data/lib/openai/models/responses/responses_client_event.rb +13 -1
- data/lib/openai/models/responses/responses_server_event.rb +1027 -54
- data/lib/openai/models/responses_model.rb +3 -0
- data/lib/openai/models/skill_create_params.rb +4 -0
- data/lib/openai/models/skills/version_create_params.rb +4 -0
- data/lib/openai/models/uploads/part_create_params.rb +4 -0
- data/lib/openai/models/vector_store_search_response.rb +3 -1
- data/lib/openai/models/video_create_character_params.rb +4 -0
- data/lib/openai/models/video_create_params.rb +4 -0
- data/lib/openai/models/video_edit_params.rb +4 -0
- data/lib/openai/models/video_extend_params.rb +4 -0
- data/lib/openai/models.rb +9 -1
- data/lib/openai/net_http_client.rb +295 -0
- data/lib/openai/providers/azure.rb +216 -0
- data/lib/openai/providers/bedrock.rb +3 -1
- data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
- data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
- data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
- data/lib/openai/resources/admin/organization/projects.rb +4 -2
- data/lib/openai/resources/audio/transcriptions.rb +8 -0
- data/lib/openai/resources/audio/translations.rb +4 -0
- data/lib/openai/resources/beta/responses.rb +1 -1
- data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
- data/lib/openai/resources/containers/files.rb +4 -0
- data/lib/openai/resources/content_provenance_checks.rb +4 -0
- data/lib/openai/resources/conversations/items.rb +1 -1
- data/lib/openai/resources/evals/runs/output_items.rb +1 -1
- data/lib/openai/resources/files.rb +4 -0
- data/lib/openai/resources/images.rb +12 -0
- data/lib/openai/resources/responses.rb +3 -2
- data/lib/openai/resources/skills/versions.rb +4 -0
- data/lib/openai/resources/skills.rb +4 -0
- data/lib/openai/resources/uploads/parts.rb +4 -0
- data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
- data/lib/openai/resources/videos.rb +38 -0
- data/lib/openai/resources/webhooks.rb +18 -6
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +113 -108
- data/rbi/openai/auth.rbi +4 -4
- data/rbi/openai/client.rbi +15 -3
- data/rbi/openai/errors.rbi +7 -0
- data/rbi/openai/helpers/streaming/events.rbi +12 -0
- data/rbi/openai/http_client.rbi +123 -0
- data/rbi/openai/internal/logging.rbi +192 -0
- data/rbi/openai/internal/provider.rbi +3 -0
- data/rbi/openai/internal/read_io_adapter.rbi +52 -0
- data/rbi/openai/internal/transport/base_client.rbi +80 -10
- data/rbi/openai/internal/type/base_model.rbi +24 -1
- data/rbi/openai/internal/type/base_page.rbi +13 -2
- data/rbi/openai/internal/type/base_stream.rbi +16 -5
- data/rbi/openai/internal/util.rbi +26 -42
- data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
- data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
- data/rbi/openai/models/all_models.rbi +15 -0
- data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
- data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
- data/rbi/openai/models/beta/beta_response.rbi +15 -0
- data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +37 -1
- data/rbi/openai/models/beta/beta_responses_server_event.rbi +3791 -54
- data/rbi/openai/models/beta/response_compact_params.rbi +15 -0
- data/rbi/openai/models/beta/response_create_params.rbi +15 -0
- data/rbi/openai/models/containers/file_create_params.rbi +8 -0
- data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
- data/rbi/openai/models/file_create_params.rbi +8 -0
- data/rbi/openai/models/image_create_variation_params.rbi +8 -0
- data/rbi/openai/models/image_edit_params.rbi +16 -0
- data/rbi/openai/models/responses/response_compact_params.rbi +15 -0
- data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
- data/rbi/openai/models/responses/responses_client_event.rbi +19 -0
- data/rbi/openai/models/responses/responses_server_event.rbi +3282 -53
- data/rbi/openai/models/responses_model.rbi +15 -0
- data/rbi/openai/models/skill_create_params.rbi +8 -0
- data/rbi/openai/models/skills/version_create_params.rbi +8 -0
- data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
- data/rbi/openai/models/video_create_character_params.rbi +8 -0
- data/rbi/openai/models/video_create_params.rbi +8 -0
- data/rbi/openai/models/video_edit_params.rbi +8 -0
- data/rbi/openai/models/video_extend_params.rbi +8 -0
- data/rbi/openai/net_http_client.rbi +64 -0
- data/rbi/openai/providers.rbi +9 -0
- data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
- data/rbi/openai/resources/audio/translations.rbi +7 -1
- data/rbi/openai/resources/containers/files.rbi +4 -0
- data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
- data/rbi/openai/resources/files.rbi +4 -0
- data/rbi/openai/resources/images.rbi +20 -0
- data/rbi/openai/resources/skills/versions.rbi +4 -0
- data/rbi/openai/resources/skills.rbi +4 -0
- data/rbi/openai/resources/uploads/parts.rbi +4 -0
- data/rbi/openai/resources/videos.rbi +16 -0
- data/sig/openai/client.rbs +7 -2
- data/sig/openai/errors.rbs +5 -0
- data/sig/openai/http_client.rbs +76 -0
- data/sig/openai/internal/logging.rbs +88 -0
- data/sig/openai/internal/provider.rbs +3 -1
- data/sig/openai/internal/read_io_adapter.rbs +23 -0
- data/sig/openai/internal/transport/base_client.rbs +41 -6
- data/sig/openai/internal/type/base_model.rbs +13 -0
- data/sig/openai/internal/type/base_page.rbs +8 -1
- data/sig/openai/internal/type/base_stream.rbs +9 -3
- data/sig/openai/internal/util.rbs +12 -16
- data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
- data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
- data/sig/openai/models/all_models.rbs +6 -0
- data/sig/openai/models/beta/beta_response.rbs +6 -0
- data/sig/openai/models/beta/beta_responses_client_event.rbs +13 -0
- data/sig/openai/models/beta/beta_responses_server_event.rbs +1946 -54
- data/sig/openai/models/beta/response_compact_params.rbs +6 -0
- data/sig/openai/models/beta/response_create_params.rbs +6 -0
- data/sig/openai/models/responses/response_compact_params.rbs +6 -0
- data/sig/openai/models/responses/responses_client_event.rbs +7 -0
- data/sig/openai/models/responses/responses_server_event.rbs +1786 -53
- data/sig/openai/models/responses_model.rbs +6 -0
- data/sig/openai/net_http_client.rbs +34 -0
- data/sig/openai/providers.rbs +6 -0
- metadata +33 -4
- data/lib/openai/internal/transport/pooled_net_requester.rb +0 -210
- data/rbi/openai/internal/transport/pooled_net_requester.rbi +0 -82
- 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
|
-
-> {
|
|
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
|
-
|
|
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],
|