ruby-openai 4.0.0 → 6.3.1

Sign up to get free protection for your applications and to get access to all the features.
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: []