ruby-openai 4.3.2 → 5.1.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: 9007c29faed86a4792fd80cd1081ddf18be51f71700ca7569dc05088bdf711fa
4
- data.tar.gz: 6d190ab521aeada561ddaf72a5922d7d603d799a3e2e841e49ad5d9942c97bba
3
+ metadata.gz: 6ea6e5d9149ffa94f53c0952491e827a5a082830cb5a4ecbdafe4e4d2523f54e
4
+ data.tar.gz: 1e8072b9fce1c48612b0120e1df4d6f45e422daec13a41e4b985f60d6cc07f6a
5
5
  SHA512:
6
- metadata.gz: cf02b7a6170d0497365b6dc1ee93e647f62f5eaf3080f3e27df42c58200c34b395aa8b7d0bfa1cec521bd00f4ede3a49983f40e074b8bb1d6dcba0a40a3373a5
7
- data.tar.gz: 63d37b64f16825ff36314a66bb75b3188916fa03d2ee25e78f7dc7605a6734976c3678ac01fd398d4e96b0166d22de47a0f940c018b081c120c30d063dbc4963
6
+ metadata.gz: 00b71588418d3c33fb2511147e9a500755cf864c1d4cd7c420599b7b7af7d10bd4bb0b1490ce5399efbf38ce2527461ad40c21f20685b6aba40db275d7c9c633
7
+ data.tar.gz: e2574855121d6ed5126aa809b32feab815b1bd8f668c9eff1d3f6c9e9a25ed83cbb45c19e1900695b814d79b708a9cedfaf911ea5112ba2ff6eadcc76332f980
data/.rubocop.yml CHANGED
@@ -12,6 +12,11 @@ Layout/LineLength:
12
12
  Exclude:
13
13
  - "**/*.gemspec"
14
14
 
15
+ Lint/AmbiguousOperator:
16
+ # https://github.com/rubocop/rubocop/issues/4294
17
+ Exclude:
18
+ - "lib/openai/client.rb"
19
+
15
20
  Metrics/AbcSize:
16
21
  Max: 20
17
22
 
data/CHANGELOG.md CHANGED
@@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [4.3.1] - 2023-08-13
8
+ ## [5.1.0] - 2023-08-20
9
+
10
+ ### Added
11
+
12
+ - Added rough_token_count to estimate tokens in a string according to OpenAI's "rules of thumb". Thank you to [@jamiemccarthy](https://github.com/jamiemccarthy) for the idea and implementation!
13
+
14
+ ## [5.0.0] - 2023-08-14
15
+
16
+ ### Added
17
+
18
+ - Support multi-tenant use of the gem! Each client now holds its own config, so you can create unlimited clients in the same project, for example to Azure and OpenAI, or for different headers, access keys, etc.
19
+ - [BREAKING-ish] This change should only break your usage of ruby-openai if you are directly calling class methods like `OpenAI::Client.get` for some reason, as they are now instance methods. Normal usage of the gem should be unaffected, just you can make new clients and they'll keep their own config if you want, overriding the global config.
20
+ - Huge thanks to [@petergoldstein](https://github.com/petergoldstein) for his original work on this, [@cthulhu](https://github.com/cthulhu) for testing and many others for reviews and suggestions.
21
+
22
+ ### Changed
23
+
24
+ - [BREAKING] Move audio related method to Audio model from Client model. You will need to update your code to handle this change, changing `client.translate` to `client.audio.translate` and `client.transcribe` to `client.audio.transcribe`.
25
+
26
+ ## [4.3.2] - 2023-08-14
9
27
 
10
28
  ### Fixed
11
29
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-openai (4.3.2)
4
+ ruby-openai (5.1.0)
5
5
  faraday (>= 1)
6
6
  faraday-multipart (>= 1)
7
7
 
data/README.md CHANGED
@@ -24,13 +24,17 @@ gem "ruby-openai"
24
24
 
25
25
  And then execute:
26
26
 
27
+ ```bash
27
28
  $ bundle install
29
+ ```
28
30
 
29
31
  ### Gem install
30
32
 
31
33
  Or install with:
32
34
 
35
+ ```bash
33
36
  $ gem install ruby-openai
37
+ ```
34
38
 
35
39
  and require with:
36
40
 
@@ -68,6 +72,12 @@ Then you can create a client like this:
68
72
  client = OpenAI::Client.new
69
73
  ```
70
74
 
75
+ You can still override the config defaults when making new clients; any options not included will fall back to any global config set with OpenAI.configure. e.g. in this example the organization_id, request_timeout, etc. will fallback to any set globally using OpenAI.configure, with only the access_token overridden:
76
+
77
+ ```ruby
78
+ client = OpenAI::Client.new(access_token: "access_token_goes_here")
79
+ ```
80
+
71
81
  #### Custom timeout or base URI
72
82
 
73
83
  The default timeout for any request using this library is 120 seconds. You can change that by passing a number of seconds to the `request_timeout` when initializing the client. You can also change the base URI used for all requests, eg. to use observability tools like [Helicone](https://docs.helicone.ai/quickstart/integrate-in-one-line-of-code), and add arbitrary other headers e.g. for [openai-caching-proxy-worker](https://github.com/6/openai-caching-proxy-worker):
@@ -80,7 +90,8 @@ client = OpenAI::Client.new(
80
90
  extra_headers: {
81
91
  "X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
82
92
  "X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
83
- "Helicone-Auth": "Bearer HELICONE_API_KEY" # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
93
+ "Helicone-Auth": "Bearer HELICONE_API_KEY", # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
94
+ "helicone-stream-force-format" => "true", # Use this with Helicone otherwise streaming drops chunks # https://github.com/alexrudall/ruby-openai/issues/251
84
95
  }
85
96
  )
86
97
  ```
@@ -116,6 +127,18 @@ To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognit
116
127
 
117
128
  where `AZURE_OPENAI_URI` is e.g. `https://custom-domain.openai.azure.com/openai/deployments/gpt-35-turbo`
118
129
 
130
+ ### Counting Tokens
131
+
132
+ OpenAI parses prompt text into [tokens](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them), which are words or portions of words. (These tokens are unrelated to your API access_token.) Counting tokens can help you estimate your [costs](https://openai.com/pricing). It can also help you ensure your prompt text size is within the max-token limits of your model's context window, and choose an appropriate [`max_tokens`](https://platform.openai.com/docs/api-reference/chat/create#chat/create-max_tokens) completion parameter so your response will fit as well.
133
+
134
+ To estimate the token-count of your text:
135
+
136
+ ```ruby
137
+ OpenAI.rough_token_count("Your text")
138
+ ```
139
+
140
+ If you need a more accurate count, try [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby).
141
+
119
142
  ### Models
120
143
 
121
144
  There are different models that can be used to generate text. For a full list and to retrieve information about a single model:
@@ -174,7 +197,7 @@ client.chat(
174
197
  # => "Anna is a young woman in her mid-twenties, with wavy chestnut hair that falls to her shoulders..."
175
198
  ```
176
199
 
177
- Note: the API docs state that token usage is included in the streamed chat chunk objects, but this doesn't currently appear to be the case. If you need to work out how many tokens are being used while streaming, try [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby).
200
+ Note: the API docs state that token usage is included in the streamed chat chunk objects, but this doesn't currently appear to be the case. To count tokens while streaming, try `OpenAI.rough_token_count` or [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby).
178
201
 
179
202
  ### Functions
180
203
 
@@ -411,7 +434,7 @@ Whisper is a speech to text model that can be used to generate text based on aud
411
434
  The translations API takes as input the audio file in any of the supported languages and transcribes the audio into English.
412
435
 
413
436
  ```ruby
414
- response = client.translate(
437
+ response = client.audio.translate(
415
438
  parameters: {
416
439
  model: "whisper-1",
417
440
  file: File.open("path_to_file", "rb"),
@@ -425,7 +448,7 @@ puts response["text"]
425
448
  The transcriptions API takes as input the audio file you want to transcribe and returns the text in the desired output file format.
426
449
 
427
450
  ```ruby
428
- response = client.transcribe(
451
+ response = client.audio.transcribe(
429
452
  parameters: {
430
453
  model: "whisper-1",
431
454
  file: File.open("path_to_file", "rb"),
@@ -0,0 +1,15 @@
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
+ end
15
+ end
data/lib/openai/client.rb CHANGED
@@ -1,58 +1,68 @@
1
1
  module OpenAI
2
2
  class Client
3
- extend OpenAI::HTTP
3
+ include OpenAI::HTTP
4
4
 
5
- def initialize(access_token: nil, organization_id: nil, uri_base: nil, request_timeout: nil,
6
- extra_headers: nil)
7
- OpenAI.configuration.access_token = access_token if access_token
8
- OpenAI.configuration.organization_id = organization_id if organization_id
9
- OpenAI.configuration.uri_base = uri_base if uri_base
10
- OpenAI.configuration.request_timeout = request_timeout if request_timeout
11
- OpenAI.configuration.extra_headers = extra_headers if extra_headers
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
15
+
16
+ def initialize(config = {})
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
12
22
  end
13
23
 
14
24
  def chat(parameters: {})
15
- OpenAI::Client.json_post(path: "/chat/completions", parameters: parameters)
25
+ json_post(path: "/chat/completions", parameters: parameters)
16
26
  end
17
27
 
18
28
  def completions(parameters: {})
19
- OpenAI::Client.json_post(path: "/completions", parameters: parameters)
29
+ json_post(path: "/completions", parameters: parameters)
20
30
  end
21
31
 
22
32
  def edits(parameters: {})
23
- OpenAI::Client.json_post(path: "/edits", parameters: parameters)
33
+ json_post(path: "/edits", parameters: parameters)
24
34
  end
25
35
 
26
36
  def embeddings(parameters: {})
27
- OpenAI::Client.json_post(path: "/embeddings", parameters: parameters)
37
+ json_post(path: "/embeddings", parameters: parameters)
38
+ end
39
+
40
+ def audio
41
+ @audio ||= OpenAI::Audio.new(client: self)
28
42
  end
29
43
 
30
44
  def files
31
- @files ||= OpenAI::Files.new
45
+ @files ||= OpenAI::Files.new(client: self)
32
46
  end
33
47
 
34
48
  def finetunes
35
- @finetunes ||= OpenAI::Finetunes.new
49
+ @finetunes ||= OpenAI::Finetunes.new(client: self)
36
50
  end
37
51
 
38
52
  def images
39
- @images ||= OpenAI::Images.new
53
+ @images ||= OpenAI::Images.new(client: self)
40
54
  end
41
55
 
42
56
  def models
43
- @models ||= OpenAI::Models.new
57
+ @models ||= OpenAI::Models.new(client: self)
44
58
  end
45
59
 
46
60
  def moderations(parameters: {})
47
- OpenAI::Client.json_post(path: "/moderations", parameters: parameters)
48
- end
49
-
50
- def transcribe(parameters: {})
51
- OpenAI::Client.multipart_post(path: "/audio/transcriptions", parameters: parameters)
61
+ json_post(path: "/moderations", parameters: parameters)
52
62
  end
53
63
 
54
- def translate(parameters: {})
55
- OpenAI::Client.multipart_post(path: "/audio/translations", parameters: parameters)
64
+ def azure?
65
+ @api_type&.to_sym == :azure
56
66
  end
57
67
  end
58
68
  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
12
  validate(file: parameters[:file])
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,28 +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-tunes")
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-tunes", 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-tunes/#{id}")
18
17
  end
19
18
 
20
19
  def cancel(id:)
21
- OpenAI::Client.multipart_post(path: "/fine-tunes/#{id}/cancel")
20
+ @client.multipart_post(path: "/fine-tunes/#{id}/cancel")
22
21
  end
23
22
 
24
23
  def events(id:)
25
- OpenAI::Client.get(path: "/fine-tunes/#{id}/events")
24
+ @client.get(path: "/fine-tunes/#{id}/events")
26
25
  end
27
26
 
28
27
  def delete(fine_tuned_model:)
@@ -30,7 +29,7 @@ module OpenAI
30
29
  raise ArgumentError, "Please give a fine_tuned_model name, not a fine-tune ID"
31
30
  end
32
31
 
33
- OpenAI::Client.delete(path: "/models/#{fine_tuned_model}")
32
+ @client.delete(path: "/models/#{fine_tuned_model}")
34
33
  end
35
34
  end
36
35
  end
data/lib/openai/http.rb CHANGED
@@ -64,40 +64,40 @@ module OpenAI
64
64
 
65
65
  def conn(multipart: false)
66
66
  Faraday.new do |f|
67
- f.options[:timeout] = OpenAI.configuration.request_timeout
67
+ f.options[:timeout] = @request_timeout
68
68
  f.request(:multipart) if multipart
69
69
  end
70
70
  end
71
71
 
72
72
  def uri(path:)
73
- if OpenAI.configuration.api_type == :azure
74
- base = File.join(OpenAI.configuration.uri_base, path)
75
- "#{base}?api-version=#{OpenAI.configuration.api_version}"
73
+ if azure?
74
+ base = File.join(@uri_base, path)
75
+ "#{base}?api-version=#{@api_version}"
76
76
  else
77
- File.join(OpenAI.configuration.uri_base, OpenAI.configuration.api_version, path)
77
+ File.join(@uri_base, @api_version, path)
78
78
  end
79
79
  end
80
80
 
81
81
  def headers
82
- if OpenAI.configuration.api_type == :azure
82
+ if azure?
83
83
  azure_headers
84
84
  else
85
85
  openai_headers
86
- end.merge(OpenAI.configuration.extra_headers || {})
86
+ end.merge(@extra_headers || {})
87
87
  end
88
88
 
89
89
  def openai_headers
90
90
  {
91
91
  "Content-Type" => "application/json",
92
- "Authorization" => "Bearer #{OpenAI.configuration.access_token}",
93
- "OpenAI-Organization" => OpenAI.configuration.organization_id
92
+ "Authorization" => "Bearer #{@access_token}",
93
+ "OpenAI-Organization" => @organization_id
94
94
  }
95
95
  end
96
96
 
97
97
  def azure_headers
98
98
  {
99
99
  "Content-Type" => "application/json",
100
- "api-key" => OpenAI.configuration.access_token
100
+ "api-key" => @access_token
101
101
  }
102
102
  end
103
103
 
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
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
@@ -1,3 +1,3 @@
1
1
  module OpenAI
2
- VERSION = "4.3.2".freeze
2
+ VERSION = "5.1.0".freeze
3
3
  end
data/lib/openai.rb CHANGED
@@ -7,6 +7,7 @@ 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/audio"
10
11
  require_relative "openai/version"
11
12
 
12
13
  module OpenAI
@@ -51,4 +52,16 @@ module OpenAI
51
52
  def self.configure
52
53
  yield(configuration)
53
54
  end
55
+
56
+ # Estimate the number of tokens in a string, using the rules of thumb from OpenAI:
57
+ # https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
58
+ def self.rough_token_count(content = "")
59
+ raise ArgumentError, "rough_token_count requires a string" unless content.is_a? String
60
+ return 0 if content.empty?
61
+
62
+ count_by_chars = content.size / 4.0
63
+ count_by_words = content.split.size * 4.0 / 3
64
+ estimate = ((count_by_chars + count_by_words) / 2.0).round
65
+ [1, estimate].max
66
+ end
54
67
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-08-14 00:00:00.000000000 Z
11
+ date: 2023-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -63,6 +63,7 @@ files:
63
63
  - bin/console
64
64
  - bin/setup
65
65
  - lib/openai.rb
66
+ - lib/openai/audio.rb
66
67
  - lib/openai/client.rb
67
68
  - lib/openai/compatibility.rb
68
69
  - lib/openai/files.rb