roseflow-openai 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 022b3ba6d3486eb3a7ed6abce48815272b2e4d5a52502dc814ebcc26c8c44712
4
- data.tar.gz: eac633fb4aab864322916a3c5aa4e71cf15fe937d4c34aec84cbf8b444b44778
3
+ metadata.gz: 40a8de2181c6b43b561fe331f6ad6af571a0ade5554f2dc468be70aafcc94de5
4
+ data.tar.gz: 6689cb738ea1170dd30229cc80405879fb327b2d00fb4871205021852ddc5d8a
5
5
  SHA512:
6
- metadata.gz: 4fb415158051794661293c7ec227c9384ac8edd3882e209a70cb559f95710d99248f01bba3c91633d6398594132cb1fcb9c5780477074a1aec1f86a95ba43555
7
- data.tar.gz: c8d660546cbc37cb46e577f32e51ddfc71c5454f0dfc62fa8fbbbdfa90ba3ae0d40018fd71de856755dab4ed4350ddb30592d82185e0f29d6710b2105441f29d
6
+ metadata.gz: 65711accd9d75651f3d56cbae8b02275d41c8d1beca380f2747711148e60086c9163a57159d6358672afde2bc938c92c53138d7ea3d447556947dfb56ec44c9e
7
+ data.tar.gz: 8242cfca9ed092991e9eda08cf914195eba2609669219bd3b49d0b55c4ef12e27e1e4341b547ba0fcc062e825964e847438371b07a6be84b2c1614887ad9eaea
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2023-07-24
4
+
5
+ - Roseflow::OpenAI::Model now implements Roseflow::AI::Model API spec.
6
+ - API operations are now composed and validated before calling the API.
7
+
3
8
  ## [0.1.0] - 2023-04-26
4
9
 
5
10
  - Initial release
data/Gemfile CHANGED
@@ -8,3 +8,5 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rspec", "~> 3.5"
10
10
  gem "standard", "~> 1.3"
11
+
12
+ eval_gemfile "Gemfile.local" if File.exist?("Gemfile.local")
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roseflow/chat/message"
4
+
5
+ module Types
6
+ module OpenAI
7
+ FunctionCallObject = Types::Hash
8
+ StringOrObject = Types::String | FunctionCallObject
9
+ StringOrArray = Types::String | Types::Array
10
+ end
11
+ end
12
+
13
+ module Roseflow
14
+ module OpenAI
15
+ class ChatMessage < Roseflow::Chat::Message
16
+ attribute? :function_call, Types::OpenAI::FunctionCallObject
17
+ end
18
+ end
19
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "faraday"
4
4
  require "faraday/retry"
5
+ require "roseflow/types"
5
6
  require "roseflow/openai/config"
6
7
  require "roseflow/openai/model"
7
8
  require "roseflow/openai/response"
@@ -32,6 +33,23 @@ module Roseflow
32
33
  end
33
34
  end
34
35
 
36
+ # Posts an operation to the API.
37
+ #
38
+ # @param operation [OpenAI::Operation] the operation to post
39
+ # @yield [String] the streamed API response
40
+ # @return [OpenAI::Response] the API response object if no block is given
41
+ def post(operation, &block)
42
+ response = connection.post(operation.path) do |request|
43
+ request.body = operation.body
44
+ if operation.stream
45
+ request.options.on_data = Proc.new do |chunk|
46
+ yield chunk if block_given?
47
+ end
48
+ end
49
+ end
50
+ response unless block_given?
51
+ end
52
+
35
53
  # Creates a chat completion.
36
54
  #
37
55
  # @param model [Roseflow::OpenAI::Model] the model to use
@@ -42,7 +60,7 @@ module Roseflow
42
60
  response = connection.post("/v1/chat/completions") do |request|
43
61
  request.body = options.merge({
44
62
  model: model.name,
45
- messages: messages
63
+ messages: messages,
46
64
  })
47
65
  end
48
66
  ChatResponse.new(response)
@@ -58,10 +76,11 @@ module Roseflow
58
76
  def streaming_chat_completion(model:, messages:, **options, &block)
59
77
  streamed = []
60
78
  connection.post("/v1/chat/completions") do |request|
79
+ options.delete(:streaming)
61
80
  request.body = options.merge({
62
81
  model: model.name,
63
82
  messages: messages,
64
- stream: true
83
+ stream: true,
65
84
  })
66
85
  request.options.on_data = Proc.new do |chunk|
67
86
  yield streaming_chunk(chunk) if block_given?
@@ -81,7 +100,7 @@ module Roseflow
81
100
  response = connection.post("/v1/completions") do |request|
82
101
  request.body = options.merge({
83
102
  model: model.name,
84
- prompt: prompt
103
+ prompt: prompt,
85
104
  })
86
105
  end
87
106
  CompletionResponse.new(response)
@@ -100,7 +119,7 @@ module Roseflow
100
119
  request.body = options.merge({
101
120
  model: model.name,
102
121
  prompt: prompt,
103
- stream: true
122
+ stream: true,
104
123
  })
105
124
  request.options.on_data = Proc.new do |chunk|
106
125
  yield streaming_chunk(chunk) if block_given?
@@ -120,7 +139,7 @@ module Roseflow
120
139
  response = connection.post("/v1/edits") do |request|
121
140
  request.body = options.merge({
122
141
  model: model.name,
123
- instruction: instruction
142
+ instruction: instruction,
124
143
  })
125
144
  end
126
145
  EditResponse.new(response)
@@ -144,7 +163,7 @@ module Roseflow
144
163
  connection.post("/v1/embeddings") do |request|
145
164
  request.body = {
146
165
  model: model.name,
147
- input: input
166
+ input: input,
148
167
  }
149
168
  end
150
169
  )
@@ -160,8 +179,8 @@ module Roseflow
160
179
  url: Config::OPENAI_API_URL,
161
180
  headers: {
162
181
  # "Content-Type" => "application/json",
163
- "OpenAI-Organization" => config.organization_id
164
- }
182
+ "OpenAI-Organization" => config.organization_id,
183
+ },
165
184
  ) do |faraday|
166
185
  faraday.request :authorization, "Bearer", -> { config.api_key }
167
186
  faraday.request :json
@@ -182,4 +201,4 @@ module Roseflow
182
201
  end
183
202
  end # Client
184
203
  end # OpenAI
185
- end # Roseflow
204
+ end # Roseflow
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry-struct"
4
- require "roseflow/tokenizer"
4
+ require "roseflow/tiktoken/tokenizer"
5
5
  require "active_support/core_ext/module/delegation"
6
6
 
7
7
  module Types
@@ -25,32 +25,39 @@ module Roseflow
25
25
 
26
26
  # Tokenizer instance for the model.
27
27
  def tokenizer
28
- @tokenizer_ ||= Tokenizer.new(model: name)
28
+ @tokenizer_ ||= Roseflow::Tiktoken::Tokenizer.new(model: name)
29
29
  end
30
30
 
31
- # Handles the model call.
32
- # FIXME: Operations should be rewritten to match the client API.
31
+ # Convenience method for chat completions.
32
+ #
33
+ # @param messages [Array<String>] Messages to use
34
+ # @param options [Hash] Options to use
35
+ # @yield [chunk] Chunk of data if stream is enabled
36
+ # @return [OpenAI::ChatResponse] the chat response object if no block is given
37
+ def chat(messages, options = {}, &block)
38
+ token_count = tokenizer.count_tokens(transform_chat_messages(options.fetch(:messages, [])))
39
+ raise TokenLimitExceededError, "Token limit for model #{name} exceeded: #{token_count} is more than #{max_tokens}" if token_count > max_tokens
40
+ response = call(:chat, options.merge({ messages: messages, model: name }), &block)
41
+ ChatResponse.new(response) unless block_given?
42
+ end
43
+
44
+ # Calls the model.
33
45
  #
34
46
  # @param operation [Symbol] Operation to perform
35
- # @param input [String] Input to use
36
- def call(operation, input, **options)
37
- token_count = tokenizer.count_tokens(transform_chat_messages(input))
38
- if token_count < max_tokens
39
- case operation
40
- when :chat
41
- @provider_.create_chat_completion(model: name, messages: transform_chat_messages(input), **options)
42
- when :completion
43
- @provider_.create_completion(input)
44
- when :image
45
- @provider_.create_image_completion(input)
46
- when :embed
47
- @provider_.create_embedding(input)
48
- else
49
- raise ArgumentError, "Invalid operation: #{operation}"
50
- end
51
- else
52
- raise TokenLimitExceededError, "Token limit for model #{name} exceeded: #{token_count} is more than #{max_tokens}"
53
- end
47
+ # @param options [Hash] Options to use
48
+ # @yield [chunk] Chunk of data if stream is enabled
49
+ # @return [Faraday::Response] raw API response if no block is given
50
+ def call(operation, options, &block)
51
+ operation = OperationHandler.new(operation, options).call
52
+ client.post(operation, &block)
53
+ end
54
+
55
+ # Returns a list of operations for the model.
56
+ #
57
+ # TODO: OpenAI does not actually provide this information per model.
58
+ # Figure out a way to do this in a proper way if feasible.
59
+ def operations
60
+ OperationHandler::OPERATION_CLASSES.keys
54
61
  end
55
62
 
56
63
  # Indicates if the model is chattable.
@@ -99,6 +106,8 @@ module Roseflow
99
106
 
100
107
  private
101
108
 
109
+ attr_reader :provider_
110
+
102
111
  def assign_attributes
103
112
  @name = @model_.fetch("id")
104
113
  @created_at = Time.at(@model_.fetch("created"))
@@ -108,6 +117,10 @@ module Roseflow
108
117
  def transform_chat_messages(input)
109
118
  input.map(&:to_h)
110
119
  end
120
+
121
+ def client
122
+ provider_.client
123
+ end
111
124
  end # Model
112
125
 
113
126
  # Represents a model permission.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "operations/chat"
4
+ require_relative "operations/completion"
5
+ require_relative "operations/image"
6
+ require_relative "operations/image_edit"
7
+ require_relative "operations/image_variation"
8
+ require_relative "operations/embedding"
9
+
10
+ module Roseflow
11
+ module OpenAI
12
+ class OperationHandler
13
+ OPERATION_CLASSES = {
14
+ chat: Operations::Chat,
15
+ completion: Operations::Completion,
16
+ embedding: Operations::Embedding,
17
+ image: Operations::Image,
18
+ image_edit: Operations::ImageEdit,
19
+ image_variation: Operations::ImageVariation,
20
+ }
21
+
22
+ def initialize(operation, options = {})
23
+ @operation = operation
24
+ @options = options
25
+ end
26
+
27
+ def call
28
+ operation_class.new(@options)
29
+ end
30
+
31
+ private
32
+
33
+ def operation_class
34
+ OPERATION_CLASSES.fetch(@operation) do
35
+ raise ArgumentError, "Invalid operation: #{@operation}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roseflow
4
+ module OpenAI
5
+ module Operations
6
+ class Base < Dry::Struct
7
+ transform_keys(&:to_sym)
8
+
9
+ attribute :model, Types::String
10
+
11
+ def body
12
+ to_h.except(:path)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ require "roseflow/openai/chat_message"
6
+
7
+ module Roseflow
8
+ module OpenAI
9
+ module Operations
10
+ # Chat operation.
11
+ #
12
+ # Given a list of messages comprising a conversation, the model will
13
+ # return a response.
14
+ #
15
+ # See https://platform.openai.com/docs/api-reference/chat for more
16
+ # information.
17
+ #
18
+ # Many of the attributes are actually optional for the API, but we
19
+ # provide defaults to them. This may change in the future.
20
+ class Chat < Base
21
+ attribute :messages, Types::Array.of(ChatMessage)
22
+ attribute? :functions, Types::Array.of(Types::Hash)
23
+ attribute? :function_call, Types::OpenAI::StringOrObject
24
+ attribute :temperature, Types::Float.default(1.0)
25
+ attribute :top_p, Types::Float.default(1.0)
26
+ attribute :n, Types::Integer.default(1)
27
+ attribute :stream, Types::Bool.default(false)
28
+ attribute? :stop, Types::OpenAI::StringOrArray
29
+ attribute? :max_tokens, Types::Integer
30
+ attribute :presence_penalty, Types::Number.default(0)
31
+ attribute :frequency_penalty, Types::Number.default(0)
32
+ attribute? :user, Types::String
33
+
34
+ attribute :path, Types::String.default("/v1/chat/completions")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ module Operations
8
+ # Completion operation.
9
+ #
10
+ # Given a prompt, the model will return one or more predicted
11
+ # completions, and can also return the probabilities of
12
+ # alternative tokens at each position.
13
+ #
14
+ # See https://platform.openai.com/docs/api-reference/completions
15
+ # for more information.
16
+ #
17
+ # Many of the attributes are actually optional for the API, but we
18
+ # provide defaults to them. This may change in the future.
19
+ class Completion < Base
20
+ attribute :prompt, Types::OpenAI::StringOrArray
21
+ attribute? :suffix, Types::String
22
+ attribute :max_tokens, Types::Integer.default(16)
23
+ attribute :temperature, Types::Float.default(1.0)
24
+ attribute :top_p, Types::Float.default(1.0)
25
+ attribute :n, Types::Integer.default(1)
26
+ attribute :stream, Types::Bool.default(false)
27
+ attribute? :logprobs, Types::Integer
28
+ attribute :echo, Types::Bool.default(false)
29
+ attribute? :stop, Types::OpenAI::StringOrArray
30
+ attribute :presence_penalty, Types::Number.default(0)
31
+ attribute :frequency_penalty, Types::Number.default(0)
32
+ attribute :best_of, Types::Integer.default(1)
33
+ attribute? :user, Types::String
34
+
35
+ attribute :path, Types::String.default("/v1/completions")
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ module Operations
8
+ # Embedding operation.
9
+ #
10
+ # Get a vector representation of a given input that can be easily
11
+ # consumed by machine learning models and algorithms.
12
+ #
13
+ # See https://platform.openai.com/docs/api-reference/embeddings
14
+ # for more information.
15
+ #
16
+ # Many of the attributes are actually optional for the API, but we
17
+ # provide defaults to them. This may change in the future.
18
+ class Embedding < Base
19
+ attribute :input, Types::OpenAI::StringOrArray
20
+ attribute? :user, Types::String
21
+
22
+ attribute :path, Types::String.default("/v1/embeddings")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ module Operations
8
+ # Image operation.
9
+ #
10
+ # Given a prompt and/or an input image, the model will generate
11
+ # a new image. This operation creates an image given a prompt.
12
+ #
13
+ # See https://platform.openai.com/docs/api-reference/images for
14
+ # more information.
15
+ #
16
+ # Many of the attributes are actually optional for the API, but we
17
+ # provide defaults to them. This may change in the future.
18
+ class Image < Dry::Struct
19
+ transform_keys(&:to_sym)
20
+
21
+ attribute :prompt, Types::String
22
+ attribute :n, Types::Integer.default(1)
23
+ attribute :size, Types::String.default("1024x1024")
24
+ attribute :response_format, Types::String.default("url")
25
+ attribute? :user, Types::String
26
+
27
+ attribute :path, Types::String.default("/v1/images/generations")
28
+
29
+ def body
30
+ to_h.except(:path)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ module Operations
8
+ # Image edit operation.
9
+ #
10
+ # Given a prompt and/or an input image, the model will generate
11
+ # a new image. This operation creates an edited or extended image
12
+ # given an original image and a prompt.
13
+ #
14
+ # See https://platform.openai.com/docs/api-reference/images for
15
+ # more information.
16
+ #
17
+ # Many of the attributes are actually optional for the API, but we
18
+ # provide defaults to them. This may change in the future.
19
+ class ImageEdit < Dry::Struct
20
+ transform_keys(&:to_sym)
21
+
22
+ attribute :image, Types::String
23
+ attribute? :mask, Types::String
24
+ attribute :prompt, Types::String
25
+ attribute :n, Types::Integer.default(1)
26
+ attribute :size, Types::String.default("1024x1024")
27
+ attribute :response_format, Types::String.default("url")
28
+ attribute? :user, Types::String
29
+
30
+ attribute :path, Types::String.default("/v1/images/edits")
31
+
32
+ def body
33
+ to_h.except(:path)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Roseflow
6
+ module OpenAI
7
+ module Operations
8
+ # Image variation operation.
9
+ #
10
+ # Given a prompt and/or an input image, the model will generate
11
+ # a new image. This operation creates a variation of a given image.
12
+ #
13
+ # See https://platform.openai.com/docs/api-reference/images for
14
+ # more information.
15
+ #
16
+ # Many of the attributes are actually optional for the API, but we
17
+ # provide defaults to them. This may change in the future.
18
+ class ImageVariation < Dry::Struct
19
+ transform_keys(&:to_sym)
20
+
21
+ attribute :image, Types::String
22
+ attribute :n, Types::Integer.default(1)
23
+ attribute :size, Types::String.default("1024x1024")
24
+ attribute :response_format, Types::String.default("url")
25
+ attribute? :user, Types::String
26
+
27
+ attribute :path, Types::String.default("/v1/images/variations")
28
+
29
+ def body
30
+ to_h.except(:path)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -38,11 +38,11 @@ module Roseflow
38
38
  # @option options [Integer] :streaming Whether to stream back partial progress
39
39
  # @option options [String] :user A unique identifier representing your end-user
40
40
  # @return [Roseflow::OpenAI::ChatResponse] The response object from the API.
41
- def chat(model:, messages:, **options)
41
+ def chat(model:, messages:, **options, &block)
42
42
  streaming = options.fetch(:streaming, false)
43
43
 
44
44
  if streaming
45
- client.streaming_chat_completion(model: model, messages: messages.map(&:to_h), **options)
45
+ client.streaming_chat_completion(model: model, messages: messages.map(&:to_h), **options, &block)
46
46
  else
47
47
  client.create_chat_completion(model: model, messages: messages.map(&:to_h), **options)
48
48
  end
@@ -4,11 +4,6 @@ require "dry-struct"
4
4
  require "roseflow/types"
5
5
  require "roseflow/openai/embedding"
6
6
 
7
- module Types
8
- include Dry.Types()
9
- Number = Types::Integer | Types::Float
10
- end
11
-
12
7
  module Roseflow
13
8
  module OpenAI
14
9
  FailedToCreateEmbeddingError = Class.new(StandardError)
@@ -142,7 +137,7 @@ module Roseflow
142
137
  transform_keys(&:to_sym)
143
138
 
144
139
  attribute :object, Types::String.default("embedding")
145
- attribute :embedding, Types::Array(Types::Number)
140
+ attribute :embedding, Types::Array(::Types::Number)
146
141
  attribute :index, Types::Integer
147
142
  end # OpenAIEmbedding
148
143
 
@@ -8,9 +8,9 @@ module Roseflow
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 0
11
- MINOR = 1
11
+ MINOR = 2
12
12
  PATCH = 0
13
- PRE = nil
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
16
16
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "roseflow/types"
4
+
3
5
  require_relative "openai/version"
6
+ require_relative "openai/client"
7
+ require_relative "openai/config"
8
+ require_relative "openai/operation_handler"
9
+ require_relative "openai/provider"
4
10
 
5
11
  module Roseflow
6
12
  module OpenAI
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "dry-struct"
35
35
  spec.add_dependency "faraday"
36
36
  spec.add_dependency "faraday-retry"
37
+ spec.add_dependency "roseflow-tiktoken"
37
38
 
38
39
  spec.add_development_dependency "awesome_print"
39
40
  spec.add_development_dependency "pry"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roseflow-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lauri Jutila
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-10 00:00:00.000000000 Z
11
+ date: 2023-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: roseflow-tiktoken
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: awesome_print
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -167,11 +181,20 @@ files:
167
181
  - Rakefile
168
182
  - config/openai.yml
169
183
  - lib/roseflow/openai.rb
184
+ - lib/roseflow/openai/chat_message.rb
170
185
  - lib/roseflow/openai/client.rb
171
186
  - lib/roseflow/openai/config.rb
172
187
  - lib/roseflow/openai/embedding.rb
173
188
  - lib/roseflow/openai/model.rb
174
189
  - lib/roseflow/openai/model_repository.rb
190
+ - lib/roseflow/openai/operation_handler.rb
191
+ - lib/roseflow/openai/operations/base.rb
192
+ - lib/roseflow/openai/operations/chat.rb
193
+ - lib/roseflow/openai/operations/completion.rb
194
+ - lib/roseflow/openai/operations/embedding.rb
195
+ - lib/roseflow/openai/operations/image.rb
196
+ - lib/roseflow/openai/operations/image_edit.rb
197
+ - lib/roseflow/openai/operations/image_variation.rb
175
198
  - lib/roseflow/openai/provider.rb
176
199
  - lib/roseflow/openai/response.rb
177
200
  - lib/roseflow/openai/structs.rb