roseflow-openai 0.1.0 → 0.2.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 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