ruby-openai 4.0.0 → 6.3.1

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.
data/lib/openai/http.rb CHANGED
@@ -1,93 +1,127 @@
1
+ require "event_stream_parser"
2
+
3
+ require_relative "http_headers"
4
+
1
5
  module OpenAI
2
6
  module HTTP
7
+ include HTTPHeaders
8
+
3
9
  def get(path:)
4
- to_json(conn.get(uri(path: path)) do |req|
10
+ parse_jsonl(conn.get(uri(path: path)) do |req|
5
11
  req.headers = headers
6
12
  end&.body)
7
13
  end
8
14
 
9
- def json_post(path:, parameters:)
10
- to_json(conn.post(uri(path: path)) do |req|
11
- if parameters[:stream].is_a?(Proc)
12
- req.options.on_data = to_json_stream(user_proc: parameters[:stream])
13
- parameters[:stream] = true # Necessary to tell OpenAI to stream.
14
- end
15
-
15
+ def post(path:)
16
+ parse_jsonl(conn.post(uri(path: path)) do |req|
16
17
  req.headers = headers
17
- req.body = parameters.to_json
18
18
  end&.body)
19
19
  end
20
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
+
21
27
  def multipart_post(path:, parameters: nil)
22
- to_json(conn(multipart: true).post(uri(path: path)) do |req|
28
+ conn(multipart: true).post(uri(path: path)) do |req|
23
29
  req.headers = headers.merge({ "Content-Type" => "multipart/form-data" })
24
30
  req.body = multipart_parameters(parameters)
25
- end&.body)
31
+ end&.body
26
32
  end
27
33
 
28
34
  def delete(path:)
29
- to_json(conn.delete(uri(path: path)) do |req|
35
+ conn.delete(uri(path: path)) do |req|
30
36
  req.headers = headers
31
- end&.body)
37
+ end&.body
32
38
  end
33
39
 
34
40
  private
35
41
 
36
- def to_json(string)
37
- return unless string
42
+ def parse_jsonl(response)
43
+ return unless response
44
+ return response unless response.is_a?(String)
38
45
 
39
- JSON.parse(string)
40
- rescue JSON::ParserError
41
46
  # Convert a multiline string of JSON objects to a JSON array.
42
- JSON.parse(string.gsub("}\n{", "},{").prepend("[").concat("]"))
47
+ response = response.gsub("}\n{", "},{").prepend("[").concat("]")
48
+
49
+ JSON.parse(response)
43
50
  end
44
51
 
45
52
  # Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
46
53
  # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
47
54
  # be a data object or an error object as described in the OpenAI API documentation.
48
55
  #
49
- # If the JSON object for a given data or error message is invalid, it is ignored.
50
- #
51
56
  # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
52
57
  # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
53
58
  def to_json_stream(user_proc:)
54
- proc do |chunk, _|
55
- chunk.scan(/(?:data|error): (\{.*\})/i).flatten.each do |data|
56
- user_proc.call(JSON.parse(data))
57
- rescue JSON::ParserError
58
- # Ignore invalid JSON.
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]"
59
69
  end
60
70
  end
61
71
  end
62
72
 
63
73
  def conn(multipart: false)
64
- Faraday.new do |f|
65
- f.options[:timeout] = OpenAI.configuration.request_timeout
74
+ connection = Faraday.new do |f|
75
+ f.options[:timeout] = @request_timeout
66
76
  f.request(:multipart) if multipart
77
+ f.use MiddlewareErrors
78
+ f.response :raise_error
79
+ f.response :json
67
80
  end
68
- end
69
81
 
70
- def uri(path:)
71
- OpenAI.configuration.uri_base + OpenAI.configuration.api_version + path
82
+ @faraday_middleware&.call(connection)
83
+
84
+ connection
72
85
  end
73
86
 
74
- def headers
75
- {
76
- "Content-Type" => "application/json",
77
- "Authorization" => "Bearer #{OpenAI.configuration.access_token}",
78
- "OpenAI-Organization" => OpenAI.configuration.organization_id
79
- }
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
80
94
  end
81
95
 
82
96
  def multipart_parameters(parameters)
83
97
  parameters&.transform_values do |value|
84
- next value unless value.is_a?(File)
98
+ next value unless value.respond_to?(:close) # File or IO object.
85
99
 
86
- # Doesn't seem like OpenAI need mime_type yet, so not worth
100
+ # Doesn't seem like OpenAI needs mime_type yet, so not worth
87
101
  # the library to figure this out. Hence the empty string
88
102
  # as the second argument.
89
103
  Faraday::UploadIO.new(value, "", value.path)
90
104
  end
91
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
92
126
  end
93
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,27 @@
1
+ module OpenAI
2
+ class Threads
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list
8
+ @client.get(path: "/threads")
9
+ end
10
+
11
+ def retrieve(id:)
12
+ @client.get(path: "/threads/#{id}")
13
+ end
14
+
15
+ def create(parameters: {})
16
+ @client.json_post(path: "/threads", parameters: parameters)
17
+ end
18
+
19
+ def modify(id:, parameters: {})
20
+ @client.json_post(path: "/threads/#{id}", parameters: parameters)
21
+ end
22
+
23
+ def delete(id:)
24
+ @client.delete(path: "/threads/#{id}")
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module OpenAI
2
- VERSION = "4.0.0".freeze
2
+ VERSION = "6.3.1".freeze
3
3
  end
data/lib/openai.rb CHANGED
@@ -7,15 +7,38 @@ require_relative "openai/files"
7
7
  require_relative "openai/finetunes"
8
8
  require_relative "openai/images"
9
9
  require_relative "openai/models"
10
+ require_relative "openai/assistants"
11
+ require_relative "openai/threads"
12
+ require_relative "openai/messages"
13
+ require_relative "openai/runs"
14
+ require_relative "openai/run_steps"
15
+ require_relative "openai/audio"
10
16
  require_relative "openai/version"
11
17
 
12
18
  module OpenAI
13
19
  class Error < StandardError; end
14
20
  class ConfigurationError < Error; end
15
21
 
22
+ class MiddlewareErrors < Faraday::Middleware
23
+ def call(env)
24
+ @app.call(env)
25
+ rescue Faraday::Error => e
26
+ raise e unless e.response.is_a?(Hash)
27
+
28
+ logger = Logger.new($stdout)
29
+ logger.formatter = proc do |_severity, _datetime, _progname, msg|
30
+ "\033[31mOpenAI HTTP Error (spotted in ruby-openai #{VERSION}): #{msg}\n\033[0m"
31
+ end
32
+ logger.error(e.response[:body])
33
+
34
+ raise e
35
+ end
36
+ end
37
+
16
38
  class Configuration
17
39
  attr_writer :access_token
18
- attr_accessor :api_version, :organization_id, :uri_base, :request_timeout
40
+ attr_accessor :api_type, :api_version, :organization_id, :uri_base, :request_timeout,
41
+ :extra_headers
19
42
 
20
43
  DEFAULT_API_VERSION = "v1".freeze
21
44
  DEFAULT_URI_BASE = "https://api.openai.com/".freeze
@@ -23,10 +46,12 @@ module OpenAI
23
46
 
24
47
  def initialize
25
48
  @access_token = nil
49
+ @api_type = nil
26
50
  @api_version = DEFAULT_API_VERSION
27
51
  @organization_id = nil
28
52
  @uri_base = DEFAULT_URI_BASE
29
53
  @request_timeout = DEFAULT_REQUEST_TIMEOUT
54
+ @extra_headers = {}
30
55
  end
31
56
 
32
57
  def access_token
@@ -48,4 +73,16 @@ module OpenAI
48
73
  def self.configure
49
74
  yield(configuration)
50
75
  end
76
+
77
+ # Estimate the number of tokens in a string, using the rules of thumb from OpenAI:
78
+ # https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
79
+ def self.rough_token_count(content = "")
80
+ raise ArgumentError, "rough_token_count requires a string" unless content.is_a? String
81
+ return 0 if content.empty?
82
+
83
+ count_by_chars = content.size / 4.0
84
+ count_by_words = content.split.size * 4.0 / 3
85
+ estimate = ((count_by_chars + count_by_words) / 2.0).round
86
+ [1, estimate].max
87
+ end
51
88
  end
data/ruby-openai.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Alex"]
7
7
  spec.email = ["alexrudall@users.noreply.github.com"]
8
8
 
9
- spec.summary = "OpenAI API + Ruby! 🤖❤️"
9
+ spec.summary = "OpenAI API + Ruby! 🤖🩵"
10
10
  spec.homepage = "https://github.com/alexrudall/ruby-openai"
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ["lib"]
27
27
 
28
+ spec.add_dependency "event_stream_parser", ">= 0.3.0", "< 2.0.0"
28
29
  spec.add_dependency "faraday", ">= 1"
29
30
  spec.add_dependency "faraday-multipart", ">= 1"
30
31
  end
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 6.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-26 00:00:00.000000000 Z
11
+ date: 2023-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: event_stream_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.3.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: faraday
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -38,7 +58,7 @@ dependencies:
38
58
  - - ">="
39
59
  - !ruby/object:Gem::Version
40
60
  version: '1'
41
- description:
61
+ description:
42
62
  email:
43
63
  - alexrudall@users.noreply.github.com
44
64
  executables: []
@@ -46,6 +66,10 @@ extensions: []
46
66
  extra_rdoc_files: []
47
67
  files:
48
68
  - ".circleci/config.yml"
69
+ - ".devcontainer/Dockerfile"
70
+ - ".devcontainer/devcontainer.json"
71
+ - ".devcontainer/docker-compose.yml"
72
+ - ".github/FUNDING.yml"
49
73
  - ".github/ISSUE_TEMPLATE/bug_report.md"
50
74
  - ".github/ISSUE_TEMPLATE/feature_request.md"
51
75
  - ".github/dependabot.yml"
@@ -63,13 +87,20 @@ files:
63
87
  - bin/console
64
88
  - bin/setup
65
89
  - lib/openai.rb
90
+ - lib/openai/assistants.rb
91
+ - lib/openai/audio.rb
66
92
  - lib/openai/client.rb
67
93
  - lib/openai/compatibility.rb
68
94
  - lib/openai/files.rb
69
95
  - lib/openai/finetunes.rb
70
96
  - lib/openai/http.rb
97
+ - lib/openai/http_headers.rb
71
98
  - lib/openai/images.rb
99
+ - lib/openai/messages.rb
72
100
  - lib/openai/models.rb
101
+ - lib/openai/run_steps.rb
102
+ - lib/openai/runs.rb
103
+ - lib/openai/threads.rb
73
104
  - lib/openai/version.rb
74
105
  - lib/ruby/openai.rb
75
106
  - pull_request_template.md
@@ -82,7 +113,7 @@ metadata:
82
113
  source_code_uri: https://github.com/alexrudall/ruby-openai
83
114
  changelog_uri: https://github.com/alexrudall/ruby-openai/blob/main/CHANGELOG.md
84
115
  rubygems_mfa_required: 'true'
85
- post_install_message:
116
+ post_install_message:
86
117
  rdoc_options: []
87
118
  require_paths:
88
119
  - lib
@@ -97,8 +128,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
128
  - !ruby/object:Gem::Version
98
129
  version: '0'
99
130
  requirements: []
100
- rubygems_version: 3.4.12
101
- signing_key:
131
+ rubygems_version: 3.4.10
132
+ signing_key:
102
133
  specification_version: 4
103
- summary: "OpenAI API + Ruby! \U0001F916❤️"
134
+ summary: "OpenAI API + Ruby! \U0001F916\U0001FA75"
104
135
  test_files: []