ruby-openai 3.6.0 → 6.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,6 +1,19 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
- task default: :spec
7
+ task :default do
8
+ Rake::Task["test"].invoke
9
+ Rake::Task["lint"].invoke
10
+ end
11
+
12
+ task :test do
13
+ Rake::Task["spec"].invoke
14
+ end
15
+
16
+ task :lint do
17
+ RuboCop::RakeTask.new(:rubocop)
18
+ Rake::Task["rubocop"].invoke
19
+ end
@@ -0,0 +1,27 @@
1
+ module OpenAI
2
+ class Assistants
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list
8
+ @client.get(path: "/assistants")
9
+ end
10
+
11
+ def retrieve(id:)
12
+ @client.get(path: "/assistants/#{id}")
13
+ end
14
+
15
+ def create(parameters: {})
16
+ @client.json_post(path: "/assistants", parameters: parameters)
17
+ end
18
+
19
+ def modify(id:, parameters: {})
20
+ @client.json_post(path: "/assistants/#{id}", parameters: parameters)
21
+ end
22
+
23
+ def delete(id:)
24
+ @client.delete(path: "/assistants/#{id}")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module OpenAI
2
+ class Audio
3
+ def initialize(client:)
4
+ @client = client
5
+ end
6
+
7
+ def transcribe(parameters: {})
8
+ @client.multipart_post(path: "/audio/transcriptions", parameters: parameters)
9
+ end
10
+
11
+ def translate(parameters: {})
12
+ @client.multipart_post(path: "/audio/translations", parameters: parameters)
13
+ end
14
+
15
+ def speech(parameters: {})
16
+ @client.json_post(path: "/audio/speech", parameters: parameters)
17
+ end
18
+ end
19
+ end
data/lib/openai/client.rb CHANGED
@@ -1,105 +1,95 @@
1
1
  module OpenAI
2
2
  class Client
3
- URI_BASE = "https://api.openai.com/".freeze
4
-
5
- def initialize(access_token: nil, organization_id: nil, request_timeout: nil)
6
- OpenAI.configuration.access_token = access_token if access_token
7
- OpenAI.configuration.organization_id = organization_id if organization_id
8
- OpenAI.configuration.request_timeout = request_timeout if request_timeout
3
+ include OpenAI::HTTP
4
+
5
+ CONFIG_KEYS = %i[
6
+ api_type
7
+ api_version
8
+ access_token
9
+ organization_id
10
+ uri_base
11
+ request_timeout
12
+ extra_headers
13
+ ].freeze
14
+ attr_reader *CONFIG_KEYS, :faraday_middleware
15
+
16
+ def initialize(config = {}, &faraday_middleware)
17
+ CONFIG_KEYS.each do |key|
18
+ # Set instance variables like api_type & access_token. Fall back to global config
19
+ # if not present.
20
+ instance_variable_set("@#{key}", config[key] || OpenAI.configuration.send(key))
21
+ end
22
+ @faraday_middleware = faraday_middleware
9
23
  end
10
24
 
11
25
  def chat(parameters: {})
12
- OpenAI::Client.json_post(path: "/chat/completions", parameters: parameters)
13
- end
14
-
15
- def completions(parameters: {})
16
- OpenAI::Client.json_post(path: "/completions", parameters: parameters)
26
+ json_post(path: "/chat/completions", parameters: parameters)
17
27
  end
18
28
 
19
29
  def edits(parameters: {})
20
- OpenAI::Client.json_post(path: "/edits", parameters: parameters)
30
+ json_post(path: "/edits", parameters: parameters)
21
31
  end
22
32
 
23
33
  def embeddings(parameters: {})
24
- OpenAI::Client.json_post(path: "/embeddings", parameters: parameters)
34
+ json_post(path: "/embeddings", parameters: parameters)
25
35
  end
26
36
 
27
- def files
28
- @files ||= OpenAI::Files.new
37
+ def completions(parameters: {})
38
+ json_post(path: "/completions", parameters: parameters)
29
39
  end
30
40
 
31
- def finetunes
32
- @finetunes ||= OpenAI::Finetunes.new
41
+ def audio
42
+ @audio ||= OpenAI::Audio.new(client: self)
33
43
  end
34
44
 
35
- def images
36
- @images ||= OpenAI::Images.new
45
+ def files
46
+ @files ||= OpenAI::Files.new(client: self)
37
47
  end
38
48
 
39
- def models
40
- @models ||= OpenAI::Models.new
49
+ def finetunes
50
+ @finetunes ||= OpenAI::Finetunes.new(client: self)
41
51
  end
42
52
 
43
- def moderations(parameters: {})
44
- OpenAI::Client.json_post(path: "/moderations", parameters: parameters)
53
+ def images
54
+ @images ||= OpenAI::Images.new(client: self)
45
55
  end
46
56
 
47
- def transcribe(parameters: {})
48
- OpenAI::Client.multipart_post(path: "/audio/transcriptions", parameters: parameters)
57
+ def models
58
+ @models ||= OpenAI::Models.new(client: self)
49
59
  end
50
60
 
51
- def translate(parameters: {})
52
- OpenAI::Client.multipart_post(path: "/audio/translations", parameters: parameters)
61
+ def assistants
62
+ @assistants ||= OpenAI::Assistants.new(client: self)
53
63
  end
54
64
 
55
- def self.get(path:)
56
- HTTParty.get(
57
- uri(path: path),
58
- headers: headers,
59
- timeout: request_timeout
60
- )
65
+ def threads
66
+ @threads ||= OpenAI::Threads.new(client: self)
61
67
  end
62
68
 
63
- def self.json_post(path:, parameters:)
64
- HTTParty.post(
65
- uri(path: path),
66
- headers: headers,
67
- body: parameters&.to_json,
68
- timeout: request_timeout
69
- )
69
+ def messages
70
+ @messages ||= OpenAI::Messages.new(client: self)
70
71
  end
71
72
 
72
- def self.multipart_post(path:, parameters: nil)
73
- HTTParty.post(
74
- uri(path: path),
75
- headers: headers.merge({ "Content-Type" => "multipart/form-data" }),
76
- body: parameters,
77
- timeout: request_timeout
78
- )
73
+ def runs
74
+ @runs ||= OpenAI::Runs.new(client: self)
79
75
  end
80
76
 
81
- def self.delete(path:)
82
- HTTParty.delete(
83
- uri(path: path),
84
- headers: headers,
85
- timeout: request_timeout
86
- )
77
+ def run_steps
78
+ @run_steps ||= OpenAI::RunSteps.new(client: self)
87
79
  end
88
80
 
89
- private_class_method def self.uri(path:)
90
- URI_BASE + OpenAI.configuration.api_version + path
81
+ def moderations(parameters: {})
82
+ json_post(path: "/moderations", parameters: parameters)
91
83
  end
92
84
 
93
- private_class_method def self.headers
94
- {
95
- "Content-Type" => "application/json",
96
- "Authorization" => "Bearer #{OpenAI.configuration.access_token}",
97
- "OpenAI-Organization" => OpenAI.configuration.organization_id
98
- }
85
+ def azure?
86
+ @api_type&.to_sym == :azure
99
87
  end
100
88
 
101
- private_class_method def self.request_timeout
102
- OpenAI.configuration.request_timeout
89
+ def beta(apis)
90
+ dup.tap do |client|
91
+ client.add_headers("OpenAI-Beta": apis.map { |k, v| "#{k}=#{v}" }.join(";"))
92
+ end
103
93
  end
104
94
  end
105
95
  end
@@ -5,5 +5,6 @@ module Ruby
5
5
  Error = ::OpenAI::Error
6
6
  ConfigurationError = ::OpenAI::ConfigurationError
7
7
  Configuration = ::OpenAI::Configuration
8
+ MiddlewareErrors = ::OpenAI::MiddlewareErrors
8
9
  end
9
10
  end
data/lib/openai/files.rb CHANGED
@@ -1,33 +1,32 @@
1
1
  module OpenAI
2
2
  class Files
3
- def initialize(access_token: nil, organization_id: nil)
4
- OpenAI.configuration.access_token = access_token if access_token
5
- OpenAI.configuration.organization_id = organization_id if organization_id
3
+ def initialize(client:)
4
+ @client = client
6
5
  end
7
6
 
8
7
  def list
9
- OpenAI::Client.get(path: "/files")
8
+ @client.get(path: "/files")
10
9
  end
11
10
 
12
11
  def upload(parameters: {})
13
- validate(file: parameters[:file])
12
+ validate(file: parameters[:file]) if parameters[:file].include?(".jsonl")
14
13
 
15
- OpenAI::Client.multipart_post(
14
+ @client.multipart_post(
16
15
  path: "/files",
17
16
  parameters: parameters.merge(file: File.open(parameters[:file]))
18
17
  )
19
18
  end
20
19
 
21
20
  def retrieve(id:)
22
- OpenAI::Client.get(path: "/files/#{id}")
21
+ @client.get(path: "/files/#{id}")
23
22
  end
24
23
 
25
24
  def content(id:)
26
- OpenAI::Client.get(path: "/files/#{id}/content")
25
+ @client.get(path: "/files/#{id}/content")
27
26
  end
28
27
 
29
28
  def delete(id:)
30
- OpenAI::Client.delete(path: "/files/#{id}")
29
+ @client.delete(path: "/files/#{id}")
31
30
  end
32
31
 
33
32
  private
@@ -1,36 +1,27 @@
1
1
  module OpenAI
2
2
  class Finetunes
3
- def initialize(access_token: nil, organization_id: nil)
4
- OpenAI.configuration.access_token = access_token if access_token
5
- OpenAI.configuration.organization_id = organization_id if organization_id
3
+ def initialize(client:)
4
+ @client = client
6
5
  end
7
6
 
8
7
  def list
9
- OpenAI::Client.get(path: "/fine-tunes")
8
+ @client.get(path: "/fine_tuning/jobs")
10
9
  end
11
10
 
12
11
  def create(parameters: {})
13
- OpenAI::Client.json_post(path: "/fine-tunes", parameters: parameters)
12
+ @client.json_post(path: "/fine_tuning/jobs", parameters: parameters)
14
13
  end
15
14
 
16
15
  def retrieve(id:)
17
- OpenAI::Client.get(path: "/fine-tunes/#{id}")
16
+ @client.get(path: "/fine_tuning/jobs/#{id}")
18
17
  end
19
18
 
20
19
  def cancel(id:)
21
- OpenAI::Client.multipart_post(path: "/fine-tunes/#{id}/cancel")
20
+ @client.json_post(path: "/fine_tuning/jobs/#{id}/cancel", parameters: {})
22
21
  end
23
22
 
24
- def events(id:)
25
- OpenAI::Client.get(path: "/fine-tunes/#{id}/events")
26
- end
27
-
28
- def delete(fine_tuned_model:)
29
- if fine_tuned_model.start_with?("ft-")
30
- raise ArgumentError, "Please give a fine_tuned_model name, not a fine-tune ID"
31
- end
32
-
33
- OpenAI::Client.delete(path: "/models/#{fine_tuned_model}")
23
+ def list_events(id:)
24
+ @client.get(path: "/fine_tuning/jobs/#{id}/events")
34
25
  end
35
26
  end
36
27
  end
@@ -0,0 +1,127 @@
1
+ require "event_stream_parser"
2
+
3
+ require_relative "http_headers"
4
+
5
+ module OpenAI
6
+ module HTTP
7
+ include HTTPHeaders
8
+
9
+ def get(path:)
10
+ parse_jsonl(conn.get(uri(path: path)) do |req|
11
+ req.headers = headers
12
+ end&.body)
13
+ end
14
+
15
+ def post(path:)
16
+ parse_jsonl(conn.post(uri(path: path)) do |req|
17
+ req.headers = headers
18
+ end&.body)
19
+ end
20
+
21
+ def json_post(path:, parameters:)
22
+ conn.post(uri(path: path)) do |req|
23
+ configure_json_post_request(req, parameters)
24
+ end&.body
25
+ end
26
+
27
+ def multipart_post(path:, parameters: nil)
28
+ conn(multipart: true).post(uri(path: path)) do |req|
29
+ req.headers = headers.merge({ "Content-Type" => "multipart/form-data" })
30
+ req.body = multipart_parameters(parameters)
31
+ end&.body
32
+ end
33
+
34
+ def delete(path:)
35
+ conn.delete(uri(path: path)) do |req|
36
+ req.headers = headers
37
+ end&.body
38
+ end
39
+
40
+ private
41
+
42
+ def parse_jsonl(response)
43
+ return unless response
44
+ return response unless response.is_a?(String)
45
+
46
+ # Convert a multiline string of JSON objects to a JSON array.
47
+ response = response.gsub("}\n{", "},{").prepend("[").concat("]")
48
+
49
+ JSON.parse(response)
50
+ end
51
+
52
+ # Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
53
+ # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
54
+ # be a data object or an error object as described in the OpenAI API documentation.
55
+ #
56
+ # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
57
+ # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
58
+ def to_json_stream(user_proc:)
59
+ parser = EventStreamParser::Parser.new
60
+
61
+ proc do |chunk, _bytes, env|
62
+ if env && env.status != 200
63
+ raise_error = Faraday::Response::RaiseError.new
64
+ raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
65
+ end
66
+
67
+ parser.feed(chunk) do |_type, data|
68
+ user_proc.call(JSON.parse(data)) unless data == "[DONE]"
69
+ end
70
+ end
71
+ end
72
+
73
+ def conn(multipart: false)
74
+ connection = Faraday.new do |f|
75
+ f.options[:timeout] = @request_timeout
76
+ f.request(:multipart) if multipart
77
+ f.use MiddlewareErrors
78
+ f.response :raise_error
79
+ f.response :json
80
+ end
81
+
82
+ @faraday_middleware&.call(connection)
83
+
84
+ connection
85
+ end
86
+
87
+ def uri(path:)
88
+ if azure?
89
+ base = File.join(@uri_base, path)
90
+ "#{base}?api-version=#{@api_version}"
91
+ else
92
+ File.join(@uri_base, @api_version, path)
93
+ end
94
+ end
95
+
96
+ def multipart_parameters(parameters)
97
+ parameters&.transform_values do |value|
98
+ next value unless value.respond_to?(:close) # File or IO object.
99
+
100
+ # Doesn't seem like OpenAI needs mime_type yet, so not worth
101
+ # the library to figure this out. Hence the empty string
102
+ # as the second argument.
103
+ Faraday::UploadIO.new(value, "", value.path)
104
+ end
105
+ end
106
+
107
+ def configure_json_post_request(req, parameters)
108
+ req_parameters = parameters.dup
109
+
110
+ if parameters[:stream].respond_to?(:call)
111
+ req.options.on_data = to_json_stream(user_proc: parameters[:stream])
112
+ req_parameters[:stream] = true # Necessary to tell OpenAI to stream.
113
+ elsif parameters[:stream]
114
+ raise ArgumentError, "The stream parameter must be a Proc or have a #call method"
115
+ end
116
+
117
+ req.headers = headers
118
+ req.body = req_parameters.to_json
119
+ end
120
+
121
+ def try_parse_json(maybe_json)
122
+ JSON.parse(maybe_json)
123
+ rescue JSON::ParserError
124
+ maybe_json
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,36 @@
1
+ module OpenAI
2
+ module HTTPHeaders
3
+ def add_headers(headers)
4
+ @extra_headers = extra_headers.merge(headers.transform_keys(&:to_s))
5
+ end
6
+
7
+ private
8
+
9
+ def headers
10
+ if azure?
11
+ azure_headers
12
+ else
13
+ openai_headers
14
+ end.merge(extra_headers)
15
+ end
16
+
17
+ def openai_headers
18
+ {
19
+ "Content-Type" => "application/json",
20
+ "Authorization" => "Bearer #{@access_token}",
21
+ "OpenAI-Organization" => @organization_id
22
+ }
23
+ end
24
+
25
+ def azure_headers
26
+ {
27
+ "Content-Type" => "application/json",
28
+ "api-key" => @access_token
29
+ }
30
+ end
31
+
32
+ def extra_headers
33
+ @extra_headers ||= {}
34
+ end
35
+ end
36
+ end
data/lib/openai/images.rb CHANGED
@@ -1,20 +1,19 @@
1
1
  module OpenAI
2
2
  class Images
3
- def initialize(access_token: nil, organization_id: nil)
4
- OpenAI.configuration.access_token = access_token if access_token
5
- OpenAI.configuration.organization_id = organization_id if organization_id
3
+ def initialize(client: nil)
4
+ @client = client
6
5
  end
7
6
 
8
7
  def generate(parameters: {})
9
- OpenAI::Client.json_post(path: "/images/generations", parameters: parameters)
8
+ @client.json_post(path: "/images/generations", parameters: parameters)
10
9
  end
11
10
 
12
11
  def edit(parameters: {})
13
- OpenAI::Client.multipart_post(path: "/images/edits", parameters: open_files(parameters))
12
+ @client.multipart_post(path: "/images/edits", parameters: open_files(parameters))
14
13
  end
15
14
 
16
15
  def variations(parameters: {})
17
- OpenAI::Client.multipart_post(path: "/images/variations", parameters: open_files(parameters))
16
+ @client.multipart_post(path: "/images/variations", parameters: open_files(parameters))
18
17
  end
19
18
 
20
19
  private
@@ -0,0 +1,23 @@
1
+ module OpenAI
2
+ class Messages
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list(thread_id:)
8
+ @client.get(path: "/threads/#{thread_id}/messages")
9
+ end
10
+
11
+ def retrieve(thread_id:, id:)
12
+ @client.get(path: "/threads/#{thread_id}/messages/#{id}")
13
+ end
14
+
15
+ def create(thread_id:, parameters: {})
16
+ @client.json_post(path: "/threads/#{thread_id}/messages", parameters: parameters)
17
+ end
18
+
19
+ def modify(id:, thread_id:, parameters: {})
20
+ @client.json_post(path: "/threads/#{thread_id}/messages/#{id}", parameters: parameters)
21
+ end
22
+ end
23
+ end
data/lib/openai/models.rb CHANGED
@@ -1,16 +1,15 @@
1
1
  module OpenAI
2
2
  class Models
3
- def initialize(access_token: nil, organization_id: nil)
4
- OpenAI.configuration.access_token = access_token if access_token
5
- OpenAI.configuration.organization_id = organization_id if organization_id
3
+ def initialize(client:)
4
+ @client = client
6
5
  end
7
6
 
8
7
  def list
9
- OpenAI::Client.get(path: "/models")
8
+ @client.get(path: "/models")
10
9
  end
11
10
 
12
11
  def retrieve(id:)
13
- OpenAI::Client.get(path: "/models/#{id}")
12
+ @client.get(path: "/models/#{id}")
14
13
  end
15
14
  end
16
15
  end
@@ -0,0 +1,15 @@
1
+ module OpenAI
2
+ class RunSteps
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list(thread_id:, run_id:)
8
+ @client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps")
9
+ end
10
+
11
+ def retrieve(thread_id:, run_id:, id:)
12
+ @client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps/#{id}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module OpenAI
2
+ class Runs
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list(thread_id:)
8
+ @client.get(path: "/threads/#{thread_id}/runs")
9
+ end
10
+
11
+ def retrieve(thread_id:, id:)
12
+ @client.get(path: "/threads/#{thread_id}/runs/#{id}")
13
+ end
14
+
15
+ def create(thread_id:, parameters: {})
16
+ @client.json_post(path: "/threads/#{thread_id}/runs", parameters: parameters)
17
+ end
18
+
19
+ def modify(id:, thread_id:, parameters: {})
20
+ @client.json_post(path: "/threads/#{thread_id}/runs/#{id}", parameters: parameters)
21
+ end
22
+
23
+ def cancel(id:, thread_id:)
24
+ @client.post(path: "/threads/#{thread_id}/runs/#{id}/cancel")
25
+ end
26
+
27
+ def submit_tool_outputs(thread_id:, run_id:, parameters: {})
28
+ @client.json_post(path: "/threads/#{thread_id}/runs/#{run_id}/submit_tool_outputs",
29
+ parameters: parameters)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module OpenAI
2
+ class Threads
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def retrieve(id:)
8
+ @client.get(path: "/threads/#{id}")
9
+ end
10
+
11
+ def create(parameters: {})
12
+ @client.json_post(path: "/threads", parameters: parameters)
13
+ end
14
+
15
+ def modify(id:, parameters: {})
16
+ @client.json_post(path: "/threads/#{id}", parameters: parameters)
17
+ end
18
+
19
+ def delete(id:)
20
+ @client.delete(path: "/threads/#{id}")
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,3 @@
1
1
  module OpenAI
2
- VERSION = "3.6.0".freeze
2
+ VERSION = "6.5.0".freeze
3
3
  end