ruby-openai 4.2.0 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +16 -0
- data/.devcontainer/devcontainer.json +36 -0
- data/.devcontainer/docker-compose.yml +19 -0
- data/.github/FUNDING.yml +13 -0
- data/.gitignore +65 -7
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile.lock +4 -2
- data/README.md +103 -10
- data/lib/openai/audio.rb +15 -0
- data/lib/openai/client.rb +33 -21
- data/lib/openai/files.rb +7 -8
- data/lib/openai/finetunes.rb +8 -9
- data/lib/openai/http.rb +34 -16
- data/lib/openai/images.rb +5 -6
- data/lib/openai/models.rb +4 -5
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +16 -1
- data/ruby-openai.gemspec +1 -0
- metadata +32 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 996d39cd32c3c05c73efea0177c12d0751b5dda208b2855aaac440af7b2702d8
|
4
|
+
data.tar.gz: 65471a670e34f537fe4878322c87978f1c2beaf93336a7f2104baaa86b018c60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: deab41c7c7f4ee21b4ed1a17f289b147b2e4960b33fd12ce863d5bdb8c835a955215d01438890c1ab8d9a1c7026faba0e5b8359c1fe3d9139082f8de58dce616
|
7
|
+
data.tar.gz: 3309d1c3a68736816c4f3bd1d465021ee3f162b5f5c3dbb7915ed5ce6f3a8d7014f9f1c4b07cf630f3f90201bdbe0ec308f1dc00fb6b075f45546fe519afb553
|
@@ -0,0 +1,16 @@
|
|
1
|
+
FROM ruby:3.2.2-slim-bullseye
|
2
|
+
|
3
|
+
ENV TZ="Europe/London"
|
4
|
+
|
5
|
+
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
6
|
+
&& apt-get -y install --no-install-recommends \
|
7
|
+
apt-utils \
|
8
|
+
build-essential \
|
9
|
+
curl \
|
10
|
+
git \
|
11
|
+
vim \
|
12
|
+
zsh
|
13
|
+
|
14
|
+
RUN gem install bundler
|
15
|
+
|
16
|
+
WORKDIR /workspace
|
@@ -0,0 +1,36 @@
|
|
1
|
+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
2
|
+
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/ruby-rails-postgres
|
3
|
+
// Update the VARIANT arg in docker-compose.yml to pick a Ruby version
|
4
|
+
{
|
5
|
+
"name": "ruby-openai",
|
6
|
+
"dockerComposeFile": "docker-compose.yml",
|
7
|
+
"service": "app",
|
8
|
+
"workspaceFolder": "/workspace",
|
9
|
+
"containerEnv": {
|
10
|
+
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
|
11
|
+
"GITHUB_USER": "${localEnv:GITHUB_USER}"
|
12
|
+
},
|
13
|
+
// Configure tool-specific properties.
|
14
|
+
"customizations": {
|
15
|
+
// Configure properties specific to VS Code.
|
16
|
+
"vscode": {
|
17
|
+
// Add the IDs of extensions you want installed when the container is created.
|
18
|
+
"extensions": [
|
19
|
+
"rebornix.Ruby",
|
20
|
+
"sleistner.vscode-fileutils",
|
21
|
+
"ms-azuretools.vscode-docker",
|
22
|
+
"samverschueren.final-newline",
|
23
|
+
"GitHub.copilot",
|
24
|
+
"usernamehw.remove-empty-lines",
|
25
|
+
"wingrunr21.vscode-ruby",
|
26
|
+
]
|
27
|
+
}
|
28
|
+
},
|
29
|
+
// Use 'postCreateCommand' to run commands after the container is created.
|
30
|
+
"postCreateCommand": "bundle install",
|
31
|
+
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
32
|
+
"features": {
|
33
|
+
"git": "os-provided",
|
34
|
+
"github-cli": "latest"
|
35
|
+
}
|
36
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
version: "3"
|
2
|
+
|
3
|
+
services:
|
4
|
+
app:
|
5
|
+
build:
|
6
|
+
context: ..
|
7
|
+
dockerfile: .devcontainer/Dockerfile
|
8
|
+
|
9
|
+
volumes:
|
10
|
+
- ..:/workspace:cached
|
11
|
+
- bundle_cache:/bundle
|
12
|
+
|
13
|
+
command: sleep infinity
|
14
|
+
|
15
|
+
environment:
|
16
|
+
TZ: Europe/London
|
17
|
+
|
18
|
+
volumes:
|
19
|
+
bundle_cache:
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# These are supported funding model platforms
|
2
|
+
|
3
|
+
github: alexrudall
|
4
|
+
patreon: # Replace with a single Patreon username
|
5
|
+
open_collective: # Replace with a single Open Collective username
|
6
|
+
ko_fi: # Replace with a single Ko-fi username
|
7
|
+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
8
|
+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
9
|
+
liberapay: # Replace with a single Liberapay username
|
10
|
+
issuehunt: # Replace with a single IssueHunt username
|
11
|
+
otechie: # Replace with a single Otechie username
|
12
|
+
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
13
|
+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
data/.gitignore
CHANGED
@@ -1,16 +1,74 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
### Ruby ###
|
2
|
+
*.gem
|
3
|
+
*.rbc
|
4
|
+
/.config
|
4
5
|
/coverage/
|
5
|
-
/
|
6
|
+
/InstalledFiles
|
6
7
|
/pkg/
|
7
8
|
/spec/reports/
|
9
|
+
/spec/examples.txt
|
10
|
+
/test/tmp/
|
11
|
+
/test/version_tmp/
|
8
12
|
/tmp/
|
13
|
+
/.bundle/
|
14
|
+
/.yardoc
|
15
|
+
/_yardoc/
|
16
|
+
/doc/
|
17
|
+
|
18
|
+
|
19
|
+
# Used by dotenv library to load environment variables.
|
20
|
+
.env
|
21
|
+
|
22
|
+
# Ignore Byebug command history file.
|
23
|
+
.byebug_history
|
24
|
+
|
25
|
+
## Specific to RubyMotion:
|
26
|
+
.dat*
|
27
|
+
.repl_history
|
28
|
+
build/
|
29
|
+
*.bridgesupport
|
30
|
+
build-iPhoneOS/
|
31
|
+
build-iPhoneSimulator/
|
32
|
+
|
33
|
+
## Specific to RubyMotion (use of CocoaPods):
|
34
|
+
#
|
35
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
36
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
37
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
38
|
+
# vendor/Pods/
|
39
|
+
|
40
|
+
## Documentation cache and generated files:
|
41
|
+
/.yardoc/
|
42
|
+
/_yardoc/
|
43
|
+
/doc/
|
44
|
+
/rdoc/
|
45
|
+
|
46
|
+
## Environment normalization:
|
47
|
+
/.bundle/
|
48
|
+
/vendor/bundle
|
49
|
+
/lib/bundler/man/
|
50
|
+
|
51
|
+
# for a library or gem, you might want to ignore these files since the code is
|
52
|
+
# intended to run in multiple environments; otherwise, check them in:
|
53
|
+
# Gemfile.lock
|
54
|
+
# .ruby-version
|
55
|
+
# .ruby-gemset
|
56
|
+
|
57
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
58
|
+
.rvmrc
|
59
|
+
|
60
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
61
|
+
# .rubocop-https?--*
|
9
62
|
|
10
63
|
# rspec failure tracking
|
11
64
|
.rspec_status
|
12
65
|
|
13
|
-
|
14
|
-
.
|
66
|
+
# IDE
|
67
|
+
.idea
|
68
|
+
.idea/
|
69
|
+
.idea/*
|
70
|
+
.vscode
|
71
|
+
.vs/
|
15
72
|
|
16
|
-
|
73
|
+
# Mac
|
74
|
+
.DS_Store
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,51 @@ 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
|
+
## [5.2.0] - 2023-10-30
|
9
|
+
|
10
|
+
### Fix
|
11
|
+
|
12
|
+
- Added more spec-compliant SSE parsing: see here https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
13
|
+
- Fixes issue where OpenAI or an intermediary returns only partial JSON per chunk of streamed data
|
14
|
+
- Huge thanks to [@atesgoral](https://github.com/atesgoral) for this important fix!
|
15
|
+
|
16
|
+
## [5.1.0] - 2023-08-20
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- 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!
|
21
|
+
|
22
|
+
## [5.0.0] - 2023-08-14
|
23
|
+
|
24
|
+
### Added
|
25
|
+
|
26
|
+
- 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.
|
27
|
+
- [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.
|
28
|
+
- 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.
|
29
|
+
|
30
|
+
### Changed
|
31
|
+
|
32
|
+
- [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`.
|
33
|
+
|
34
|
+
## [4.3.2] - 2023-08-14
|
35
|
+
|
36
|
+
### Fixed
|
37
|
+
|
38
|
+
- Don't overwrite config extra-headers when making a client without different ones. Thanks to [@swistaczek](https://github.com/swistaczek) for raising this!
|
39
|
+
- Include extra-headers for Azure requests.
|
40
|
+
|
41
|
+
## [4.3.1] - 2023-08-13
|
42
|
+
|
43
|
+
### Fixed
|
44
|
+
|
45
|
+
- Tempfiles can now be sent to the API as well as Files, eg for Whisper. Thanks to [@codergeek121](https://github.com/codergeek121) for the fix!
|
46
|
+
|
47
|
+
## [4.3.0] - 2023-08-12
|
48
|
+
|
49
|
+
### Added
|
50
|
+
|
51
|
+
- Add extra-headers to config to allow setting openai-caching-proxy-worker TTL, Helicone Auth and anything else ya need. Ty to [@deltaguita](https://github.com/deltaguita) and [@marckohlbrugge](https://github.com/marckohlbrugge) for the PR!
|
52
|
+
|
8
53
|
## [4.2.0] - 2023-06-20
|
9
54
|
|
10
55
|
### Added
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ruby-openai (
|
4
|
+
ruby-openai (5.2.0)
|
5
|
+
event_stream_parser (>= 0.3.0, < 1.0.0)
|
5
6
|
faraday (>= 1)
|
6
7
|
faraday-multipart (>= 1)
|
7
8
|
|
@@ -16,7 +17,8 @@ GEM
|
|
16
17
|
rexml
|
17
18
|
diff-lcs (1.5.0)
|
18
19
|
dotenv (2.8.1)
|
19
|
-
|
20
|
+
event_stream_parser (0.3.0)
|
21
|
+
faraday (2.7.10)
|
20
22
|
faraday-net_http (>= 2.0, < 3.1)
|
21
23
|
ruby2_keywords (>= 0.0.4)
|
22
24
|
faraday-multipart (1.0.4)
|
data/README.md
CHANGED
@@ -8,11 +8,9 @@ Use the [OpenAI API](https://openai.com/blog/openai-api/) with Ruby! 🤖❤️
|
|
8
8
|
|
9
9
|
Stream text with GPT-4, transcribe and translate audio with Whisper, or create images with DALL·E...
|
10
10
|
|
11
|
-
|
11
|
+
🚢 Based in the UK and want to hire me? Now you can! [railsai.com](https://railsai.com?utm_source=ruby-openai&utm_medium=readme&utm_id=26072023)
|
12
12
|
|
13
|
-
[
|
14
|
-
|
15
|
-
Follow me on [Twitter](https://twitter.com/alexrudall) for more Ruby / AI content
|
13
|
+
[🎮 Ruby AI Builders Discord](https://discord.gg/k4Uc224xVD) | [🐦 Twitter](https://twitter.com/alexrudall) | [🧠 Anthropic Gem](https://github.com/alexrudall/anthropic) | [🚂 Midjourney Gem](https://github.com/alexrudall/midjourney)
|
16
14
|
|
17
15
|
### Bundler
|
18
16
|
|
@@ -24,13 +22,17 @@ gem "ruby-openai"
|
|
24
22
|
|
25
23
|
And then execute:
|
26
24
|
|
25
|
+
```bash
|
27
26
|
$ bundle install
|
27
|
+
```
|
28
28
|
|
29
29
|
### Gem install
|
30
30
|
|
31
31
|
Or install with:
|
32
32
|
|
33
|
+
```bash
|
33
34
|
$ gem install ruby-openai
|
35
|
+
```
|
34
36
|
|
35
37
|
and require with:
|
36
38
|
|
@@ -68,15 +70,27 @@ Then you can create a client like this:
|
|
68
70
|
client = OpenAI::Client.new
|
69
71
|
```
|
70
72
|
|
73
|
+
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:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
client = OpenAI::Client.new(access_token: "access_token_goes_here")
|
77
|
+
```
|
78
|
+
|
71
79
|
#### Custom timeout or base URI
|
72
80
|
|
73
|
-
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):
|
81
|
+
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):
|
74
82
|
|
75
83
|
```ruby
|
76
84
|
client = OpenAI::Client.new(
|
77
85
|
access_token: "access_token_goes_here",
|
78
86
|
uri_base: "https://oai.hconeai.com/",
|
79
|
-
request_timeout: 240
|
87
|
+
request_timeout: 240,
|
88
|
+
extra_headers: {
|
89
|
+
"X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
|
90
|
+
"X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
|
91
|
+
"Helicone-Auth": "Bearer HELICONE_API_KEY", # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
|
92
|
+
"helicone-stream-force-format" => "true", # Use this with Helicone otherwise streaming drops chunks # https://github.com/alexrudall/ruby-openai/issues/251
|
93
|
+
}
|
80
94
|
)
|
81
95
|
```
|
82
96
|
|
@@ -88,10 +102,15 @@ OpenAI.configure do |config|
|
|
88
102
|
config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
|
89
103
|
config.uri_base = "https://oai.hconeai.com/" # Optional
|
90
104
|
config.request_timeout = 240 # Optional
|
105
|
+
config.extra_headers = {
|
106
|
+
"X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
|
107
|
+
"X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
|
108
|
+
"Helicone-Auth": "Bearer HELICONE_API_KEY" # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
|
109
|
+
} # Optional
|
91
110
|
end
|
92
111
|
```
|
93
112
|
|
94
|
-
|
113
|
+
#### Azure
|
95
114
|
|
96
115
|
To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/) API, you can configure the gem like this:
|
97
116
|
|
@@ -106,6 +125,18 @@ To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognit
|
|
106
125
|
|
107
126
|
where `AZURE_OPENAI_URI` is e.g. `https://custom-domain.openai.azure.com/openai/deployments/gpt-35-turbo`
|
108
127
|
|
128
|
+
### Counting Tokens
|
129
|
+
|
130
|
+
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.
|
131
|
+
|
132
|
+
To estimate the token-count of your text:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
OpenAI.rough_token_count("Your text")
|
136
|
+
```
|
137
|
+
|
138
|
+
If you need a more accurate count, try [tiktoken_ruby](https://github.com/IAPark/tiktoken_ruby).
|
139
|
+
|
109
140
|
### Models
|
110
141
|
|
111
142
|
There are different models that can be used to generate text. For a full list and to retrieve information about a single model:
|
@@ -164,6 +195,68 @@ client.chat(
|
|
164
195
|
# => "Anna is a young woman in her mid-twenties, with wavy chestnut hair that falls to her shoulders..."
|
165
196
|
```
|
166
197
|
|
198
|
+
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).
|
199
|
+
|
200
|
+
### Functions
|
201
|
+
|
202
|
+
You can describe and pass in functions and the model will intelligently choose to output a JSON object containing arguments to call those them. For example, if you want the model to use your method `get_current_weather` to get the current weather in a given location:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
def get_current_weather(location:, unit: "fahrenheit")
|
206
|
+
# use a weather api to fetch weather
|
207
|
+
end
|
208
|
+
|
209
|
+
response =
|
210
|
+
client.chat(
|
211
|
+
parameters: {
|
212
|
+
model: "gpt-3.5-turbo-0613",
|
213
|
+
messages: [
|
214
|
+
{
|
215
|
+
"role": "user",
|
216
|
+
"content": "What is the weather like in San Francisco?",
|
217
|
+
},
|
218
|
+
],
|
219
|
+
functions: [
|
220
|
+
{
|
221
|
+
name: "get_current_weather",
|
222
|
+
description: "Get the current weather in a given location",
|
223
|
+
parameters: {
|
224
|
+
type: :object,
|
225
|
+
properties: {
|
226
|
+
location: {
|
227
|
+
type: :string,
|
228
|
+
description: "The city and state, e.g. San Francisco, CA",
|
229
|
+
},
|
230
|
+
unit: {
|
231
|
+
type: "string",
|
232
|
+
enum: %w[celsius fahrenheit],
|
233
|
+
},
|
234
|
+
},
|
235
|
+
required: ["location"],
|
236
|
+
},
|
237
|
+
},
|
238
|
+
],
|
239
|
+
},
|
240
|
+
)
|
241
|
+
|
242
|
+
message = response.dig("choices", 0, "message")
|
243
|
+
|
244
|
+
if message["role"] == "assistant" && message["function_call"]
|
245
|
+
function_name = message.dig("function_call", "name")
|
246
|
+
args =
|
247
|
+
JSON.parse(
|
248
|
+
message.dig("function_call", "arguments"),
|
249
|
+
{ symbolize_names: true },
|
250
|
+
)
|
251
|
+
|
252
|
+
case function_name
|
253
|
+
when "get_current_weather"
|
254
|
+
get_current_weather(**args)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
# => "The weather is nice 🌞"
|
258
|
+
```
|
259
|
+
|
167
260
|
### Completions
|
168
261
|
|
169
262
|
Hit the OpenAI API for a completion using other GPT-3 models:
|
@@ -202,7 +295,7 @@ You can use the embeddings endpoint to get a vector of numbers representing an i
|
|
202
295
|
```ruby
|
203
296
|
response = client.embeddings(
|
204
297
|
parameters: {
|
205
|
-
model: "
|
298
|
+
model: "text-embedding-ada-002",
|
206
299
|
input: "The food was delicious and the waiter..."
|
207
300
|
}
|
208
301
|
)
|
@@ -339,7 +432,7 @@ Whisper is a speech to text model that can be used to generate text based on aud
|
|
339
432
|
The translations API takes as input the audio file in any of the supported languages and transcribes the audio into English.
|
340
433
|
|
341
434
|
```ruby
|
342
|
-
response = client.translate(
|
435
|
+
response = client.audio.translate(
|
343
436
|
parameters: {
|
344
437
|
model: "whisper-1",
|
345
438
|
file: File.open("path_to_file", "rb"),
|
@@ -353,7 +446,7 @@ puts response["text"]
|
|
353
446
|
The transcriptions API takes as input the audio file you want to transcribe and returns the text in the desired output file format.
|
354
447
|
|
355
448
|
```ruby
|
356
|
-
response = client.transcribe(
|
449
|
+
response = client.audio.transcribe(
|
357
450
|
parameters: {
|
358
451
|
model: "whisper-1",
|
359
452
|
file: File.open("path_to_file", "rb"),
|
data/lib/openai/audio.rb
ADDED
@@ -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,56 +1,68 @@
|
|
1
1
|
module OpenAI
|
2
2
|
class Client
|
3
|
-
|
3
|
+
include OpenAI::HTTP
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
10
22
|
end
|
11
23
|
|
12
24
|
def chat(parameters: {})
|
13
|
-
|
25
|
+
json_post(path: "/chat/completions", parameters: parameters)
|
14
26
|
end
|
15
27
|
|
16
28
|
def completions(parameters: {})
|
17
|
-
|
29
|
+
json_post(path: "/completions", parameters: parameters)
|
18
30
|
end
|
19
31
|
|
20
32
|
def edits(parameters: {})
|
21
|
-
|
33
|
+
json_post(path: "/edits", parameters: parameters)
|
22
34
|
end
|
23
35
|
|
24
36
|
def embeddings(parameters: {})
|
25
|
-
|
37
|
+
json_post(path: "/embeddings", parameters: parameters)
|
38
|
+
end
|
39
|
+
|
40
|
+
def audio
|
41
|
+
@audio ||= OpenAI::Audio.new(client: self)
|
26
42
|
end
|
27
43
|
|
28
44
|
def files
|
29
|
-
@files ||= OpenAI::Files.new
|
45
|
+
@files ||= OpenAI::Files.new(client: self)
|
30
46
|
end
|
31
47
|
|
32
48
|
def finetunes
|
33
|
-
@finetunes ||= OpenAI::Finetunes.new
|
49
|
+
@finetunes ||= OpenAI::Finetunes.new(client: self)
|
34
50
|
end
|
35
51
|
|
36
52
|
def images
|
37
|
-
@images ||= OpenAI::Images.new
|
53
|
+
@images ||= OpenAI::Images.new(client: self)
|
38
54
|
end
|
39
55
|
|
40
56
|
def models
|
41
|
-
@models ||= OpenAI::Models.new
|
57
|
+
@models ||= OpenAI::Models.new(client: self)
|
42
58
|
end
|
43
59
|
|
44
60
|
def moderations(parameters: {})
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
def transcribe(parameters: {})
|
49
|
-
OpenAI::Client.multipart_post(path: "/audio/transcriptions", parameters: parameters)
|
61
|
+
json_post(path: "/moderations", parameters: parameters)
|
50
62
|
end
|
51
63
|
|
52
|
-
def
|
53
|
-
|
64
|
+
def azure?
|
65
|
+
@api_type&.to_sym == :azure
|
54
66
|
end
|
55
67
|
end
|
56
68
|
end
|
data/lib/openai/files.rb
CHANGED
@@ -1,33 +1,32 @@
|
|
1
1
|
module OpenAI
|
2
2
|
class Files
|
3
|
-
def initialize(
|
4
|
-
|
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
|
-
|
8
|
+
@client.get(path: "/files")
|
10
9
|
end
|
11
10
|
|
12
11
|
def upload(parameters: {})
|
13
12
|
validate(file: parameters[:file])
|
14
13
|
|
15
|
-
|
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
|
-
|
21
|
+
@client.get(path: "/files/#{id}")
|
23
22
|
end
|
24
23
|
|
25
24
|
def content(id:)
|
26
|
-
|
25
|
+
@client.get(path: "/files/#{id}/content")
|
27
26
|
end
|
28
27
|
|
29
28
|
def delete(id:)
|
30
|
-
|
29
|
+
@client.delete(path: "/files/#{id}")
|
31
30
|
end
|
32
31
|
|
33
32
|
private
|
data/lib/openai/finetunes.rb
CHANGED
@@ -1,28 +1,27 @@
|
|
1
1
|
module OpenAI
|
2
2
|
class Finetunes
|
3
|
-
def initialize(
|
4
|
-
|
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
|
-
|
8
|
+
@client.get(path: "/fine-tunes")
|
10
9
|
end
|
11
10
|
|
12
11
|
def create(parameters: {})
|
13
|
-
|
12
|
+
@client.json_post(path: "/fine-tunes", parameters: parameters)
|
14
13
|
end
|
15
14
|
|
16
15
|
def retrieve(id:)
|
17
|
-
|
16
|
+
@client.get(path: "/fine-tunes/#{id}")
|
18
17
|
end
|
19
18
|
|
20
19
|
def cancel(id:)
|
21
|
-
|
20
|
+
@client.multipart_post(path: "/fine-tunes/#{id}/cancel")
|
22
21
|
end
|
23
22
|
|
24
23
|
def events(id:)
|
25
|
-
|
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
|
-
|
32
|
+
@client.delete(path: "/models/#{fine_tuned_model}")
|
34
33
|
end
|
35
34
|
end
|
36
35
|
end
|
data/lib/openai/http.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "event_stream_parser"
|
2
|
+
|
1
3
|
module OpenAI
|
2
4
|
module HTTP
|
3
5
|
def get(path:)
|
@@ -53,53 +55,69 @@ module OpenAI
|
|
53
55
|
# @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
|
54
56
|
# @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
|
55
57
|
def to_json_stream(user_proc:)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
58
|
+
parser = EventStreamParser::Parser.new
|
59
|
+
|
60
|
+
proc do |chunk, _bytes, env|
|
61
|
+
if env && env.status != 200
|
62
|
+
emit_json(json: chunk, user_proc: user_proc)
|
63
|
+
else
|
64
|
+
parser.feed(chunk) do |_type, data|
|
65
|
+
emit_json(json: data, user_proc: user_proc) unless data == "[DONE]"
|
66
|
+
end
|
61
67
|
end
|
62
68
|
end
|
63
69
|
end
|
64
70
|
|
71
|
+
def emit_json(json:, user_proc:)
|
72
|
+
user_proc.call(JSON.parse(json))
|
73
|
+
rescue JSON::ParserError
|
74
|
+
# Ignore invalid JSON.
|
75
|
+
end
|
76
|
+
|
65
77
|
def conn(multipart: false)
|
66
78
|
Faraday.new do |f|
|
67
|
-
f.options[:timeout] =
|
79
|
+
f.options[:timeout] = @request_timeout
|
68
80
|
f.request(:multipart) if multipart
|
69
81
|
end
|
70
82
|
end
|
71
83
|
|
72
84
|
def uri(path:)
|
73
|
-
if
|
74
|
-
base = File.join(
|
75
|
-
"#{base}?api-version=#{
|
85
|
+
if azure?
|
86
|
+
base = File.join(@uri_base, path)
|
87
|
+
"#{base}?api-version=#{@api_version}"
|
76
88
|
else
|
77
|
-
File.join(
|
89
|
+
File.join(@uri_base, @api_version, path)
|
78
90
|
end
|
79
91
|
end
|
80
92
|
|
81
93
|
def headers
|
82
|
-
|
94
|
+
if azure?
|
95
|
+
azure_headers
|
96
|
+
else
|
97
|
+
openai_headers
|
98
|
+
end.merge(@extra_headers || {})
|
99
|
+
end
|
83
100
|
|
101
|
+
def openai_headers
|
84
102
|
{
|
85
103
|
"Content-Type" => "application/json",
|
86
|
-
"Authorization" => "Bearer #{
|
87
|
-
"OpenAI-Organization" =>
|
104
|
+
"Authorization" => "Bearer #{@access_token}",
|
105
|
+
"OpenAI-Organization" => @organization_id
|
88
106
|
}
|
89
107
|
end
|
90
108
|
|
91
109
|
def azure_headers
|
92
110
|
{
|
93
111
|
"Content-Type" => "application/json",
|
94
|
-
"api-key" =>
|
112
|
+
"api-key" => @access_token
|
95
113
|
}
|
96
114
|
end
|
97
115
|
|
98
116
|
def multipart_parameters(parameters)
|
99
117
|
parameters&.transform_values do |value|
|
100
|
-
next value unless value.
|
118
|
+
next value unless value.respond_to?(:close) # File or IO object.
|
101
119
|
|
102
|
-
# Doesn't seem like OpenAI
|
120
|
+
# Doesn't seem like OpenAI needs mime_type yet, so not worth
|
103
121
|
# the library to figure this out. Hence the empty string
|
104
122
|
# as the second argument.
|
105
123
|
Faraday::UploadIO.new(value, "", value.path)
|
data/lib/openai/images.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
module OpenAI
|
2
2
|
class Images
|
3
|
-
def initialize(
|
4
|
-
|
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
|
-
|
8
|
+
@client.json_post(path: "/images/generations", parameters: parameters)
|
10
9
|
end
|
11
10
|
|
12
11
|
def edit(parameters: {})
|
13
|
-
|
12
|
+
@client.multipart_post(path: "/images/edits", parameters: open_files(parameters))
|
14
13
|
end
|
15
14
|
|
16
15
|
def variations(parameters: {})
|
17
|
-
|
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(
|
4
|
-
|
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
|
-
|
8
|
+
@client.get(path: "/models")
|
10
9
|
end
|
11
10
|
|
12
11
|
def retrieve(id:)
|
13
|
-
|
12
|
+
@client.get(path: "/models/#{id}")
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
data/lib/openai/version.rb
CHANGED
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
|
@@ -15,7 +16,8 @@ module OpenAI
|
|
15
16
|
|
16
17
|
class Configuration
|
17
18
|
attr_writer :access_token
|
18
|
-
attr_accessor :api_type, :api_version, :organization_id, :uri_base, :request_timeout
|
19
|
+
attr_accessor :api_type, :api_version, :organization_id, :uri_base, :request_timeout,
|
20
|
+
:extra_headers
|
19
21
|
|
20
22
|
DEFAULT_API_VERSION = "v1".freeze
|
21
23
|
DEFAULT_URI_BASE = "https://api.openai.com/".freeze
|
@@ -28,6 +30,7 @@ module OpenAI
|
|
28
30
|
@organization_id = nil
|
29
31
|
@uri_base = DEFAULT_URI_BASE
|
30
32
|
@request_timeout = DEFAULT_REQUEST_TIMEOUT
|
33
|
+
@extra_headers = nil
|
31
34
|
end
|
32
35
|
|
33
36
|
def access_token
|
@@ -49,4 +52,16 @@ module OpenAI
|
|
49
52
|
def self.configure
|
50
53
|
yield(configuration)
|
51
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
|
52
67
|
end
|
data/ruby-openai.gemspec
CHANGED
@@ -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", "< 1.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
|
+
version: 5.2.0
|
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-
|
11
|
+
date: 2023-10-30 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: 1.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: 1.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,6 +87,7 @@ files:
|
|
63
87
|
- bin/console
|
64
88
|
- bin/setup
|
65
89
|
- lib/openai.rb
|
90
|
+
- lib/openai/audio.rb
|
66
91
|
- lib/openai/client.rb
|
67
92
|
- lib/openai/compatibility.rb
|
68
93
|
- lib/openai/files.rb
|
@@ -82,7 +107,7 @@ metadata:
|
|
82
107
|
source_code_uri: https://github.com/alexrudall/ruby-openai
|
83
108
|
changelog_uri: https://github.com/alexrudall/ruby-openai/blob/main/CHANGELOG.md
|
84
109
|
rubygems_mfa_required: 'true'
|
85
|
-
post_install_message:
|
110
|
+
post_install_message:
|
86
111
|
rdoc_options: []
|
87
112
|
require_paths:
|
88
113
|
- lib
|
@@ -97,8 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
122
|
- !ruby/object:Gem::Version
|
98
123
|
version: '0'
|
99
124
|
requirements: []
|
100
|
-
rubygems_version: 3.4.
|
101
|
-
signing_key:
|
125
|
+
rubygems_version: 3.4.10
|
126
|
+
signing_key:
|
102
127
|
specification_version: 4
|
103
128
|
summary: "OpenAI API + Ruby! \U0001F916❤️"
|
104
129
|
test_files: []
|