ruby-openai 6.5.0 → 7.0.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: f3db6f0c15b1015a875950a23ad157cb9f6f4eaed2005c35f75fd97197ee3dd6
4
- data.tar.gz: e11f7020b2db2d627646c584feb7dfb2163f3bd8233860e5115ffe0f8d65c681
3
+ metadata.gz: 8334c2f0658ff33f39e96e8316b5c7122ca8810f5bfaca945c8d5f1add38b85a
4
+ data.tar.gz: b761ed842f7c27ba7f3e6e3137f6d60f2d51cb1a32f585989fd2b1992d256b46
5
5
  SHA512:
6
- metadata.gz: 76100b527ed276b83190df15c1241be39d58288285e93caaeb20ca94c937082e1d19dd99b9ea69e1758352105e749e38940f1f0192b97c50fb18a8e81d321911
7
- data.tar.gz: 425db82e5ae928dba3136351b697ac53335ffbce5afb81c98920efda2a0b4c600cf83a8273c8c627ad4b25a352ba76d2e77d88f00a48d4993dcbbc318077be53
6
+ metadata.gz: 7c53448def0a2a9849744a09c10f91fb4c16eb23fbf0d52be748574a9a96b05cb5641821c7728b9d95764cf50399f23c48bfdf3444d86ab80e65602e2264f6ba
7
+ data.tar.gz: 8806bbe03d2cde1b1fdfb691bcc2c4d803837faed6f001027135bdf9ed54a4bdf4eb01128c36d9d84b35622876ec087c494980e67b01a72a8ecb01040180cc72
data/.circleci/config.yml CHANGED
@@ -8,7 +8,7 @@ jobs:
8
8
  rubocop:
9
9
  parallelism: 1
10
10
  docker:
11
- - image: cimg/ruby:3.1-node
11
+ - image: cimg/ruby:3.2-node
12
12
  steps:
13
13
  - checkout
14
14
  - ruby/install-deps
@@ -43,3 +43,4 @@ workflows:
43
43
  - cimg/ruby:3.0-node
44
44
  - cimg/ruby:3.1-node
45
45
  - cimg/ruby:3.2-node
46
+ - cimg/ruby:3.3-node
data/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ 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
+ ## [7.0.0] - 2024-04-27
9
+
10
+ ### Added
11
+
12
+ - Add support for Batches, thanks to [@simonx1](https://github.com/simonx1) for the PR!
13
+ - Allow use of local LLMs like Ollama! Thanks to [@ThomasSevestre](https://github.com/ThomasSevestre)
14
+ - Update to v2 of the Assistants beta & add documentation on streaming from an Assistant.
15
+ - Add Assistants endpoint to create and run a thread in one go, thank you [@quocphien90](https://github.com/
16
+ quocphien90)
17
+ - Add missing parameters (order, limit, etc) to Runs, RunSteps and Messages - thanks to [@shalecraig](https://github.com/shalecraig) and [@coezbek](https://github.com/coezbek)
18
+ - Add missing Messages#list spec - thanks [@adammeghji](https://github.com/adammeghji)
19
+ - Add Messages#modify to README - thanks to [@nas887](https://github.com/nas887)
20
+ - Don't add the api_version (`/v1/`) to base_uris that already include it - thanks to [@kaiwren](https://github.com/kaiwren) for raising this issue
21
+ - Allow passing a `StringIO` to Files#upload - thanks again to [@simonx1](https://github.com/simonx1)
22
+ - Add Ruby 3.3 to CI
23
+
24
+ ### Security
25
+
26
+ - [BREAKING] ruby-openai will no longer log out API errors by default - you can reenable by passing `log_errors: true` to your client. This will help to prevent leaking secrets to logs. Thanks to [@lalunamel](https://github.com/lalunamel) for this PR.
27
+
28
+ ### Removed
29
+
30
+ - [BREAKING] Remove deprecated edits endpoint.
31
+
32
+ ### Fixed
33
+
34
+ - Fix README DALL·E 3 error - thanks to [@clayton](https://github.com/clayton)
35
+ - Fix README tool_calls error and add missing tool_choice info - thanks to [@Jbrito6492](https://github.com/Jbrito6492)
36
+
8
37
  ## [6.5.0] - 2024-03-31
9
38
 
10
39
  ### Added
@@ -67,13 +96,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
67
96
  - [BREAKING] Switch from legacy Finetunes to the new Fine-tune-jobs endpoints. Implemented by [@lancecarlson](https://github.com/lancecarlson)
68
97
  - [BREAKING] Remove deprecated Completions endpoints - use Chat instead.
69
98
 
70
- ### Fix
99
+ ### Fixed
71
100
 
72
101
  - [BREAKING] Fix issue where :stream parameters were replaced by a boolean in the client application. Thanks to [@martinjaimem](https://github.com/martinjaimem), [@vickymadrid03](https://github.com/vickymadrid03) and [@nicastelo](https://github.com/nicastelo) for spotting and fixing this issue.
73
102
 
74
103
  ## [5.2.0] - 2023-10-30
75
104
 
76
- ### Fix
105
+ ### Fixed
77
106
 
78
107
  - Added more spec-compliant SSE parsing: see here https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
79
108
  - Fixes issue where OpenAI or an intermediary returns only partial JSON per chunk of streamed data
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ gemspec
6
6
  gem "byebug", "~> 11.1.3"
7
7
  gem "dotenv", "~> 2.8.1"
8
8
  gem "rake", "~> 13.1"
9
- gem "rspec", "~> 3.12"
9
+ gem "rspec", "~> 3.13"
10
10
  gem "rubocop", "~> 1.50.2"
11
11
  gem "vcr", "~> 6.1.0"
12
12
  gem "webmock", "~> 3.19.1"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ruby-openai (6.5.0)
4
+ ruby-openai (7.0.0)
5
5
  event_stream_parser (>= 0.3.0, < 2.0.0)
6
6
  faraday (>= 1)
7
7
  faraday-multipart (>= 1)
@@ -16,10 +16,10 @@ GEM
16
16
  byebug (11.1.3)
17
17
  crack (0.4.5)
18
18
  rexml
19
- diff-lcs (1.5.0)
19
+ diff-lcs (1.5.1)
20
20
  dotenv (2.8.1)
21
21
  event_stream_parser (1.0.0)
22
- faraday (2.7.12)
22
+ faraday (2.8.1)
23
23
  base64
24
24
  faraday-net_http (>= 2.0, < 3.1)
25
25
  ruby2_keywords (>= 0.0.4)
@@ -37,19 +37,19 @@ GEM
37
37
  rake (13.1.0)
38
38
  regexp_parser (2.8.0)
39
39
  rexml (3.2.6)
40
- rspec (3.12.0)
41
- rspec-core (~> 3.12.0)
42
- rspec-expectations (~> 3.12.0)
43
- rspec-mocks (~> 3.12.0)
44
- rspec-core (3.12.0)
45
- rspec-support (~> 3.12.0)
46
- rspec-expectations (3.12.2)
40
+ rspec (3.13.0)
41
+ rspec-core (~> 3.13.0)
42
+ rspec-expectations (~> 3.13.0)
43
+ rspec-mocks (~> 3.13.0)
44
+ rspec-core (3.13.0)
45
+ rspec-support (~> 3.13.0)
46
+ rspec-expectations (3.13.0)
47
47
  diff-lcs (>= 1.2.0, < 2.0)
48
- rspec-support (~> 3.12.0)
49
- rspec-mocks (3.12.3)
48
+ rspec-support (~> 3.13.0)
49
+ rspec-mocks (3.13.0)
50
50
  diff-lcs (>= 1.2.0, < 2.0)
51
- rspec-support (~> 3.12.0)
52
- rspec-support (3.12.0)
51
+ rspec-support (~> 3.13.0)
52
+ rspec-support (3.13.1)
53
53
  rubocop (1.50.2)
54
54
  json (~> 2.3)
55
55
  parallel (~> 1.10)
@@ -78,7 +78,7 @@ DEPENDENCIES
78
78
  byebug (~> 11.1.3)
79
79
  dotenv (~> 2.8.1)
80
80
  rake (~> 13.1)
81
- rspec (~> 3.12)
81
+ rspec (~> 3.13)
82
82
  rubocop (~> 1.50.2)
83
83
  ruby-openai!
84
84
  vcr (~> 6.1.0)
data/README.md CHANGED
@@ -22,11 +22,13 @@ Stream text with GPT-4, transcribe and translate audio with Whisper, or create i
22
22
  - [With Config](#with-config)
23
23
  - [Custom timeout or base URI](#custom-timeout-or-base-uri)
24
24
  - [Extra Headers per Client](#extra-headers-per-client)
25
- - [Verbose Logging](#verbose-logging)
25
+ - [Logging](#logging)
26
+ - [Errors](#errors)
27
+ - [Faraday middleware](#faraday-middleware)
26
28
  - [Azure](#azure)
29
+ - [Ollama](#ollama)
27
30
  - [Counting Tokens](#counting-tokens)
28
31
  - [Models](#models)
29
- - [Examples](#examples)
30
32
  - [Chat](#chat)
31
33
  - [Streaming Chat](#streaming-chat)
32
34
  - [Vision](#vision)
@@ -34,6 +36,7 @@ Stream text with GPT-4, transcribe and translate audio with Whisper, or create i
34
36
  - [Functions](#functions)
35
37
  - [Edits](#edits)
36
38
  - [Embeddings](#embeddings)
39
+ - [Batches](#batches)
37
40
  - [Files](#files)
38
41
  - [Finetunes](#finetunes)
39
42
  - [Assistants](#assistants)
@@ -146,6 +149,7 @@ or when configuring the gem:
146
149
  ```ruby
147
150
  OpenAI.configure do |config|
148
151
  config.access_token = ENV.fetch("OPENAI_ACCESS_TOKEN")
152
+ config.log_errors = true # Optional
149
153
  config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
150
154
  config.uri_base = "https://oai.hconeai.com/" # Optional
151
155
  config.request_timeout = 240 # Optional
@@ -166,7 +170,19 @@ client = OpenAI::Client.new(access_token: "access_token_goes_here")
166
170
  client.add_headers("X-Proxy-TTL" => "43200")
167
171
  ```
168
172
 
169
- #### Verbose Logging
173
+ #### Logging
174
+
175
+ ##### Errors
176
+
177
+ By default, `ruby-openai` does not log any `Faraday::Error`s encountered while executing a network request to avoid leaking data (e.g. 400s, 500s, SSL errors and more - see [here](https://www.rubydoc.info/github/lostisland/faraday/Faraday/Error) for a complete list of subclasses of `Faraday::Error` and what can cause them).
178
+
179
+ If you would like to enable this functionality, you can set `log_errors` to `true` when configuring the client:
180
+
181
+ ```ruby
182
+ client = OpenAI::Client.new(log_errors: true)
183
+ ```
184
+
185
+ ##### Faraday middleware
170
186
 
171
187
  You can pass [Faraday middleware](https://lostisland.github.io/faraday/#/middleware/index) to the client in a block, eg. to enable verbose logging with Ruby's [Logger](https://ruby-doc.org/3.2.2/stdlibs/logger/Logger.html):
172
188
 
@@ -191,6 +207,38 @@ To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognit
191
207
 
192
208
  where `AZURE_OPENAI_URI` is e.g. `https://custom-domain.openai.azure.com/openai/deployments/gpt-35-turbo`
193
209
 
210
+ #### Ollama
211
+
212
+ Ollama allows you to run open-source LLMs, such as Llama 3, locally. It [offers chat compatibility](https://github.com/ollama/ollama/blob/main/docs/openai.md) with the OpenAI API.
213
+
214
+ You can download Ollama [here](https://ollama.com/). On macOS you can install and run Ollama like this:
215
+
216
+ ```bash
217
+ brew install ollama
218
+ ollama serve
219
+ ollama pull llama3:latest # In new terminal tab.
220
+ ```
221
+
222
+ Create a client using your Ollama server and the pulled model, and stream a conversation for free:
223
+
224
+ ```ruby
225
+ client = OpenAI::Client.new(
226
+ uri_base: "http://localhost:11434"
227
+ )
228
+
229
+ client.chat(
230
+ parameters: {
231
+ model: "llama3", # Required.
232
+ messages: [{ role: "user", content: "Hello!"}], # Required.
233
+ temperature: 0.7,
234
+ stream: proc do |chunk, _bytesize|
235
+ print chunk.dig("choices", 0, "delta", "content")
236
+ end
237
+ })
238
+
239
+ # => Hi! It's nice to meet you. Is there something I can help you with, or would you like to chat?
240
+ ```
241
+
194
242
  ### Counting Tokens
195
243
 
196
244
  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.
@@ -209,24 +257,9 @@ There are different models that can be used to generate text. For a full list an
209
257
 
210
258
  ```ruby
211
259
  client.models.list
212
- client.models.retrieve(id: "text-ada-001")
260
+ client.models.retrieve(id: "gpt-3.5-turbo")
213
261
  ```
214
262
 
215
- #### Examples
216
-
217
- - [GPT-4 (limited beta)](https://platform.openai.com/docs/models/gpt-4)
218
- - gpt-4 (uses current version)
219
- - gpt-4-0314
220
- - gpt-4-32k
221
- - [GPT-3.5](https://platform.openai.com/docs/models/gpt-3-5)
222
- - gpt-3.5-turbo
223
- - gpt-3.5-turbo-0301
224
- - text-davinci-003
225
- - [GPT-3](https://platform.openai.com/docs/models/gpt-3)
226
- - text-ada-001
227
- - text-babbage-001
228
- - text-curie-001
229
-
230
263
  ### Chat
231
264
 
232
265
  GPT is a model that can be used to generate text in a conversational style. You can use it to [generate a response](https://platform.openai.com/docs/api-reference/chat/create) to a sequence of [messages](https://platform.openai.com/docs/guides/chat/introduction):
@@ -338,9 +371,10 @@ You can stream it as well!
338
371
 
339
372
  ### Functions
340
373
 
341
- 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:
374
+ You can describe and pass in functions and the model will intelligently choose to output a JSON object containing arguments to call them - eg., to use your method `get_current_weather` to get the weather in a given location. Note that tool_choice is optional, but if you exclude it, the model will choose whether to use the function or not ([see here](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice)).
342
375
 
343
376
  ```ruby
377
+
344
378
  def get_current_weather(location:, unit: "fahrenheit")
345
379
  # use a weather api to fetch weather
346
380
  end
@@ -348,7 +382,7 @@ end
348
382
  response =
349
383
  client.chat(
350
384
  parameters: {
351
- model: "gpt-3.5-turbo-0613",
385
+ model: "gpt-3.5-turbo",
352
386
  messages: [
353
387
  {
354
388
  "role": "user",
@@ -378,16 +412,22 @@ response =
378
412
  },
379
413
  }
380
414
  ],
415
+ tool_choice: {
416
+ type: "function",
417
+ function: {
418
+ name: "get_current_weather"
419
+ }
420
+ }
381
421
  },
382
422
  )
383
423
 
384
424
  message = response.dig("choices", 0, "message")
385
425
 
386
426
  if message["role"] == "assistant" && message["tool_calls"]
387
- function_name = message.dig("tool_calls", "function", "name")
427
+ function_name = message.dig("tool_calls", 0, "function", "name")
388
428
  args =
389
429
  JSON.parse(
390
- message.dig("tool_calls", "function", "arguments"),
430
+ message.dig("tool_calls", 0, "function", "arguments"),
391
431
  { symbolize_names: true },
392
432
  )
393
433
 
@@ -406,7 +446,7 @@ Hit the OpenAI API for a completion using other GPT-3 models:
406
446
  ```ruby
407
447
  response = client.completions(
408
448
  parameters: {
409
- model: "text-davinci-001",
449
+ model: "gpt-3.5-turbo",
410
450
  prompt: "Once upon a time",
411
451
  max_tokens: 5
412
452
  })
@@ -414,22 +454,6 @@ puts response["choices"].map { |c| c["text"] }
414
454
  # => [", there lived a great"]
415
455
  ```
416
456
 
417
- ### Edits
418
-
419
- Send a string and some instructions for what to do to the string:
420
-
421
- ```ruby
422
- response = client.edits(
423
- parameters: {
424
- model: "text-davinci-edit-001",
425
- input: "What day of the wek is it?",
426
- instruction: "Fix the spelling mistakes"
427
- }
428
- )
429
- puts response.dig("choices", 0, "text")
430
- # => What day of the week is it?
431
- ```
432
-
433
457
  ### Embeddings
434
458
 
435
459
  You can use the embeddings endpoint to get a vector of numbers representing an input. You can then compare these vectors for different inputs to efficiently check how similar the inputs are.
@@ -446,6 +470,94 @@ puts response.dig("data", 0, "embedding")
446
470
  # => Vector representation of your embedding
447
471
  ```
448
472
 
473
+ ### Batches
474
+
475
+ The Batches endpoint allows you to create and manage large batches of API requests to run asynchronously. Currently, only the `/v1/chat/completions` endpoint is supported for batches.
476
+
477
+ To use the Batches endpoint, you need to first upload a JSONL file containing the batch requests using the Files endpoint. The file must be uploaded with the purpose set to `batch`. Each line in the JSONL file represents a single request and should have the following format:
478
+
479
+ ```json
480
+ {
481
+ "custom_id": "request-1",
482
+ "method": "POST",
483
+ "url": "/v1/chat/completions",
484
+ "body": {
485
+ "model": "gpt-3.5-turbo",
486
+ "messages": [
487
+ { "role": "system", "content": "You are a helpful assistant." },
488
+ { "role": "user", "content": "What is 2+2?" }
489
+ ]
490
+ }
491
+ }
492
+ ```
493
+
494
+ Once you have uploaded the JSONL file, you can create a new batch by providing the file ID, endpoint, and completion window:
495
+
496
+ ```ruby
497
+ response = client.batches.create(
498
+ parameters: {
499
+ input_file_id: "file-abc123",
500
+ endpoint: "/v1/chat/completions",
501
+ completion_window: "24h"
502
+ }
503
+ )
504
+ batch_id = response["id"]
505
+ ```
506
+
507
+ You can retrieve information about a specific batch using its ID:
508
+
509
+ ```ruby
510
+ batch = client.batches.retrieve(id: batch_id)
511
+ ```
512
+
513
+ To cancel a batch that is in progress:
514
+
515
+ ```ruby
516
+ client.batches.cancel(id: batch_id)
517
+ ```
518
+
519
+ You can also list all the batches:
520
+
521
+ ```ruby
522
+ client.batches.list
523
+ ```
524
+
525
+ Once the batch["completed_at"] is present, you can fetch the output or error files:
526
+
527
+ ```ruby
528
+ batch = client.batches.retrieve(id: batch_id)
529
+ output_file_id = batch["output_file_id"]
530
+ output_response = client.files.content(id: output_file_id)
531
+ error_file_id = batch["error_file_id"]
532
+ error_response = client.files.content(id: error_file_id)
533
+ ```
534
+
535
+ These files are in JSONL format, with each line representing the output or error for a single request. The lines can be in any order:
536
+
537
+ ```json
538
+ {
539
+ "id": "response-1",
540
+ "custom_id": "request-1",
541
+ "response": {
542
+ "id": "chatcmpl-abc123",
543
+ "object": "chat.completion",
544
+ "created": 1677858242,
545
+ "model": "gpt-3.5-turbo-0301",
546
+ "choices": [
547
+ {
548
+ "index": 0,
549
+ "message": {
550
+ "role": "assistant",
551
+ "content": "2+2 equals 4."
552
+ }
553
+ }
554
+ ]
555
+ }
556
+ }
557
+ ```
558
+
559
+ If a request fails with a non-HTTP error, the error object will contain more information about the cause of the failure.
560
+
449
561
  ### Files
450
562
 
451
563
  Put your data in a `.jsonl` file like this:
@@ -455,7 +567,7 @@ Put your data in a `.jsonl` file like this:
455
567
  {"prompt":"@lakers disappoint for a third straight night ->", "completion":" negative"}
456
568
  ```
457
569
 
458
- and pass the path to `client.files.upload` to upload it to OpenAI, and then interact with it:
570
+ and pass the path (or a StringIO object) to `client.files.upload` to upload it to OpenAI, and then interact with it:
459
571
 
460
572
  ```ruby
461
573
  client.files.upload(parameters: { file: "path/to/sentiment.jsonl", purpose: "fine-tune" })
@@ -480,7 +592,7 @@ You can then use this file ID to create a fine tuning job:
480
592
  response = client.finetunes.create(
481
593
  parameters: {
482
594
  training_file: file_id,
483
- model: "gpt-3.5-turbo-0613"
595
+ model: "gpt-3.5-turbo"
484
596
  })
485
597
  fine_tune_id = response["id"]
486
598
  ```
@@ -519,9 +631,9 @@ client.finetunes.list_events(id: fine_tune_id)
519
631
 
520
632
  ### Assistants
521
633
 
522
- Assistants can call models to interact with threads and use tools to perform tasks (see [Assistant Overview](https://platform.openai.com/docs/assistants/overview)).
634
+ Assistants are stateful actors that can have many conversations and use tools to perform tasks (see [Assistant Overview](https://platform.openai.com/docs/assistants/overview)).
523
635
 
524
- To create a new assistant (see [API documentation](https://platform.openai.com/docs/api-reference/assistants/createAssistant)):
636
+ To create a new assistant:
525
637
 
526
638
  ```ruby
527
639
  response = client.assistants.create(
@@ -605,17 +717,36 @@ client.messages.retrieve(thread_id: thread_id, id: message_id) # -> Fails after
605
717
 
606
718
  ### Runs
607
719
 
608
- To submit a thread to be evaluated with the model of an assistant, create a `Run` as follows (Note: This is one place where OpenAI will take your money):
720
+ To submit a thread to be evaluated with the model of an assistant, create a `Run` as follows:
609
721
 
610
722
  ```ruby
611
723
  # Create run (will use instruction/model/tools from Assistant's definition)
612
724
  response = client.runs.create(thread_id: thread_id,
613
725
  parameters: {
614
- assistant_id: assistant_id
726
+ assistant_id: assistant_id,
727
+ max_prompt_tokens: 256,
728
+ max_completion_tokens: 16
615
729
  })
616
730
  run_id = response['id']
731
+ ```
617
732
 
618
- # Retrieve/poll Run to observe status
733
+ You can stream the message chunks as they come through:
734
+
735
+ ```ruby
736
+ client.runs.create(thread_id: thread_id,
737
+ parameters: {
738
+ assistant_id: assistant_id,
739
+ max_prompt_tokens: 256,
740
+ max_completion_tokens: 16,
741
+ stream: proc do |chunk, _bytesize|
742
+ print chunk.dig("delta", "content", 0, "text", "value") if chunk["object"] == "thread.message.delta"
743
+ end
744
+ })
745
+ ```
746
+
747
+ To get the status of a Run:
748
+
749
+ ```
619
750
  response = client.runs.retrieve(id: run_id, thread_id: thread_id)
620
751
  status = response['status']
621
752
  ```
@@ -624,23 +755,22 @@ The `status` response can include the following strings `queued`, `in_progress`,
624
755
 
625
756
  ```ruby
626
757
  while true do
627
-
628
758
  response = client.runs.retrieve(id: run_id, thread_id: thread_id)
629
759
  status = response['status']
630
760
 
631
761
  case status
632
762
  when 'queued', 'in_progress', 'cancelling'
633
- puts 'Sleeping'
634
- sleep 1 # Wait one second and poll again
763
+ puts 'Sleeping'
764
+ sleep 1 # Wait one second and poll again
635
765
  when 'completed'
636
- break # Exit loop and report result to user
766
+ break # Exit loop and report result to user
637
767
  when 'requires_action'
638
- # Handle tool calls (see below)
768
+ # Handle tool calls (see below)
639
769
  when 'cancelled', 'failed', 'expired'
640
- puts response['last_error'].inspect
641
- break # or `exit`
770
+ puts response['last_error'].inspect
771
+ break # or `exit`
642
772
  else
643
- puts "Unknown status response: #{status}"
773
+ puts "Unknown status response: #{status}"
644
774
  end
645
775
  end
646
776
  ```
@@ -649,10 +779,10 @@ If the `status` response indicates that the `run` is `completed`, the associated
649
779
 
650
780
  ```ruby
651
781
  # Either retrieve all messages in bulk again, or...
652
- messages = client.messages.list(thread_id: thread_id) # Note: as of 2023-12-11 adding limit or order options isn't working, yet
782
+ messages = client.messages.list(thread_id: thread_id, parameters: { order: 'asc' })
653
783
 
654
784
  # Alternatively retrieve the `run steps` for the run which link to the messages:
655
- run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id)
785
+ run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id, parameters: { order: 'asc' })
656
786
  new_message_ids = run_steps['data'].filter_map { |step|
657
787
  if step['type'] == 'message_creation'
658
788
  step.dig('step_details', "message_creation", "message_id")
@@ -679,10 +809,33 @@ new_messages.each { |msg|
679
809
  }
680
810
  ```
681
811
 
682
- At any time you can list all runs which have been performed on a particular thread or are currently running (in descending/newest first order):
812
+ You can also update the metadata on messages, including messages that come from the assistant.
683
813
 
684
814
  ```ruby
685
- client.runs.list(thread_id: thread_id)
815
+ metadata = {
816
+ user_id: "abc123"
817
+ }
818
+ message = client.messages.modify(id: message_id, thread_id: thread_id, parameters: { metadata: metadata })
819
+ ```
820
+
821
+ At any time you can list all runs which have been performed on a particular thread or are currently running:
822
+
823
+ ```ruby
824
+ client.runs.list(thread_id: thread_id, parameters: { order: "asc", limit: 3 })
825
+ ```
826
+
827
+ #### Create and Run
828
+
829
+ You can also create a thread and run in one call like this:
830
+
831
+ ```ruby
832
+ response = client.threads.create_and_run(
833
+ parameters: {
834
+ model: 'gpt-3.5-turbo',
835
+ messages: [{ role: 'user', content: "What's deep learning?"}]
836
+ })
837
+ run_id = response['id']
838
+ thread_id = response['thread_id']
686
839
  ```
687
840
 
688
841
  #### Runs involving function tools
@@ -746,7 +899,7 @@ puts response.dig("data", 0, "url")
746
899
  For DALL·E 3 the size of any generated images must be one of `1024x1024`, `1024x1792` or `1792x1024`. Additionally the quality of the image can be specified to either `standard` or `hd`.
747
900
 
748
901
  ```ruby
749
- response = client.images.generate(parameters: { prompt: "A springer spaniel cooking pasta wearing a hat of some sort", size: "1024x1792", quality: "standard" })
902
+ response = client.images.generate(parameters: { prompt: "A springer spaniel cooking pasta wearing a hat of some sort", model: "dall-e-3", size: "1024x1792", quality: "standard" })
750
903
  puts response.dig("data", 0, "url")
751
904
  # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."
752
905
  ```
@@ -845,7 +998,7 @@ HTTP errors can be caught like this:
845
998
 
846
999
  ```
847
1000
  begin
848
- OpenAI::Client.new.models.retrieve(id: "text-ada-001")
1001
+ OpenAI::Client.new.models.retrieve(id: "gpt-3.5-turbo")
849
1002
  rescue Faraday::Error => e
850
1003
  raise "Got a Faraday error: #{e}"
851
1004
  end
@@ -1,7 +1,7 @@
1
1
  module OpenAI
2
2
  class Assistants
3
3
  def initialize(client:)
4
- @client = client.beta(assistants: "v1")
4
+ @client = client.beta(assistants: "v2")
5
5
  end
6
6
 
7
7
  def list
@@ -0,0 +1,23 @@
1
+ module OpenAI
2
+ class Batches
3
+ def initialize(client:)
4
+ @client = client.beta(assistants: "v1")
5
+ end
6
+
7
+ def list
8
+ @client.get(path: "/batches")
9
+ end
10
+
11
+ def retrieve(id:)
12
+ @client.get(path: "/batches/#{id}")
13
+ end
14
+
15
+ def create(parameters: {})
16
+ @client.json_post(path: "/batches", parameters: parameters)
17
+ end
18
+
19
+ def cancel(id:)
20
+ @client.post(path: "/batches/#{id}/cancel")
21
+ end
22
+ end
23
+ end
data/lib/openai/client.rb CHANGED
@@ -6,6 +6,7 @@ module OpenAI
6
6
  api_type
7
7
  api_version
8
8
  access_token
9
+ log_errors
9
10
  organization_id
10
11
  uri_base
11
12
  request_timeout
@@ -17,7 +18,10 @@ module OpenAI
17
18
  CONFIG_KEYS.each do |key|
18
19
  # Set instance variables like api_type & access_token. Fall back to global config
19
20
  # if not present.
20
- instance_variable_set("@#{key}", config[key] || OpenAI.configuration.send(key))
21
+ instance_variable_set(
22
+ "@#{key}",
23
+ config[key].nil? ? OpenAI.configuration.send(key) : config[key]
24
+ )
21
25
  end
22
26
  @faraday_middleware = faraday_middleware
23
27
  end
@@ -26,10 +30,6 @@ module OpenAI
26
30
  json_post(path: "/chat/completions", parameters: parameters)
27
31
  end
28
32
 
29
- def edits(parameters: {})
30
- json_post(path: "/edits", parameters: parameters)
31
- end
32
-
33
33
  def embeddings(parameters: {})
34
34
  json_post(path: "/embeddings", parameters: parameters)
35
35
  end
@@ -78,6 +78,10 @@ module OpenAI
78
78
  @run_steps ||= OpenAI::RunSteps.new(client: self)
79
79
  end
80
80
 
81
+ def batches
82
+ @batches ||= OpenAI::Batches.new(client: self)
83
+ end
84
+
81
85
  def moderations(parameters: {})
82
86
  json_post(path: "/moderations", parameters: parameters)
83
87
  end
data/lib/openai/files.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  module OpenAI
2
2
  class Files
3
+ PURPOSES = %w[
4
+ assistants
5
+ batch
6
+ fine-tune
7
+ ].freeze
8
+
3
9
  def initialize(client:)
4
10
  @client = client
5
11
  end
@@ -9,12 +15,17 @@ module OpenAI
9
15
  end
10
16
 
11
17
  def upload(parameters: {})
12
- validate(file: parameters[:file]) if parameters[:file].include?(".jsonl")
18
+ file_input = parameters[:file]
19
+ file = prepare_file_input(file_input: file_input)
20
+
21
+ validate(file: file, purpose: parameters[:purpose], file_input: file_input)
13
22
 
14
23
  @client.multipart_post(
15
24
  path: "/files",
16
- parameters: parameters.merge(file: File.open(parameters[:file]))
25
+ parameters: parameters.merge(file: file)
17
26
  )
27
+ ensure
28
+ file.close if file.is_a?(File)
18
29
  end
19
30
 
20
31
  def retrieve(id:)
@@ -31,12 +42,33 @@ module OpenAI
31
42
 
32
43
  private
33
44
 
34
- def validate(file:)
35
- File.open(file).each_line.with_index do |line, index|
45
+ def prepare_file_input(file_input:)
46
+ if file_input.is_a?(String)
47
+ File.open(file_input)
48
+ elsif file_input.respond_to?(:read) && file_input.respond_to?(:rewind)
49
+ file_input
50
+ else
51
+ raise ArgumentError, "Invalid file - must be a StringIO object or a path to a file."
52
+ end
53
+ end
54
+
55
+ def validate(file:, purpose:, file_input:)
56
+ raise ArgumentError, "`file` is required" if file.nil?
57
+ unless PURPOSES.include?(purpose)
58
+ raise ArgumentError, "`purpose` must be one of `#{PURPOSES.join(',')}`"
59
+ end
60
+
61
+ validate_jsonl(file: file) if file_input.is_a?(String) && file_input.end_with?(".jsonl")
62
+ end
63
+
64
+ def validate_jsonl(file:)
65
+ file.each_line.with_index do |line, index|
36
66
  JSON.parse(line)
37
67
  rescue JSON::ParserError => e
38
68
  raise JSON::ParserError, "#{e.message} - found on line #{index + 1} of #{file}"
39
69
  end
70
+ ensure
71
+ file.rewind
40
72
  end
41
73
  end
42
74
  end
data/lib/openai/http.rb CHANGED
@@ -6,8 +6,8 @@ module OpenAI
6
6
  module HTTP
7
7
  include HTTPHeaders
8
8
 
9
- def get(path:)
10
- parse_jsonl(conn.get(uri(path: path)) do |req|
9
+ def get(path:, parameters: nil)
10
+ parse_jsonl(conn.get(uri(path: path), parameters) do |req|
11
11
  req.headers = headers
12
12
  end&.body)
13
13
  end
@@ -74,7 +74,7 @@ module OpenAI
74
74
  connection = Faraday.new do |f|
75
75
  f.options[:timeout] = @request_timeout
76
76
  f.request(:multipart) if multipart
77
- f.use MiddlewareErrors
77
+ f.use MiddlewareErrors if @log_errors
78
78
  f.response :raise_error
79
79
  f.response :json
80
80
  end
@@ -88,6 +88,8 @@ module OpenAI
88
88
  if azure?
89
89
  base = File.join(@uri_base, path)
90
90
  "#{base}?api-version=#{@api_version}"
91
+ elsif @uri_base.include?(@api_version)
92
+ File.join(@uri_base, path)
91
93
  else
92
94
  File.join(@uri_base, @api_version, path)
93
95
  end
@@ -97,10 +99,14 @@ module OpenAI
97
99
  parameters&.transform_values do |value|
98
100
  next value unless value.respond_to?(:close) # File or IO object.
99
101
 
102
+ # Faraday::UploadIO does not require a path, so we will pass it
103
+ # only if it is available. This allows StringIO objects to be
104
+ # passed in as well.
105
+ path = value.respond_to?(:path) ? value.path : nil
100
106
  # Doesn't seem like OpenAI needs mime_type yet, so not worth
101
107
  # the library to figure this out. Hence the empty string
102
108
  # as the second argument.
103
- Faraday::UploadIO.new(value, "", value.path)
109
+ Faraday::UploadIO.new(value, "", path)
104
110
  end
105
111
  end
106
112
 
@@ -4,8 +4,8 @@ module OpenAI
4
4
  @client = client.beta(assistants: "v1")
5
5
  end
6
6
 
7
- def list(thread_id:)
8
- @client.get(path: "/threads/#{thread_id}/messages")
7
+ def list(thread_id:, parameters: {})
8
+ @client.get(path: "/threads/#{thread_id}/messages", parameters: parameters)
9
9
  end
10
10
 
11
11
  def retrieve(thread_id:, id:)
@@ -4,8 +4,8 @@ module OpenAI
4
4
  @client = client.beta(assistants: "v1")
5
5
  end
6
6
 
7
- def list(thread_id:, run_id:)
8
- @client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps")
7
+ def list(thread_id:, run_id:, parameters: {})
8
+ @client.get(path: "/threads/#{thread_id}/runs/#{run_id}/steps", parameters: parameters)
9
9
  end
10
10
 
11
11
  def retrieve(thread_id:, run_id:, id:)
data/lib/openai/runs.rb CHANGED
@@ -4,8 +4,8 @@ module OpenAI
4
4
  @client = client.beta(assistants: "v1")
5
5
  end
6
6
 
7
- def list(thread_id:)
8
- @client.get(path: "/threads/#{thread_id}/runs")
7
+ def list(thread_id:, parameters: {})
8
+ @client.get(path: "/threads/#{thread_id}/runs", parameters: parameters)
9
9
  end
10
10
 
11
11
  def retrieve(thread_id:, id:)
@@ -24,6 +24,10 @@ module OpenAI
24
24
  @client.post(path: "/threads/#{thread_id}/runs/#{id}/cancel")
25
25
  end
26
26
 
27
+ def create_thread_and_run(parameters: {})
28
+ @client.json_post(path: "/threads/runs", parameters: parameters)
29
+ end
30
+
27
31
  def submit_tool_outputs(thread_id:, run_id:, parameters: {})
28
32
  @client.json_post(path: "/threads/#{thread_id}/runs/#{run_id}/submit_tool_outputs",
29
33
  parameters: parameters)
@@ -1,3 +1,3 @@
1
1
  module OpenAI
2
- VERSION = "6.5.0".freeze
2
+ VERSION = "7.0.0".freeze
3
3
  end
data/lib/openai.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "openai/runs"
14
14
  require_relative "openai/run_steps"
15
15
  require_relative "openai/audio"
16
16
  require_relative "openai/version"
17
+ require_relative "openai/batches"
17
18
 
18
19
  module OpenAI
19
20
  class Error < StandardError; end
@@ -36,30 +37,30 @@ module OpenAI
36
37
  end
37
38
 
38
39
  class Configuration
39
- attr_writer :access_token
40
- attr_accessor :api_type, :api_version, :organization_id, :uri_base, :request_timeout,
40
+ attr_accessor :access_token,
41
+ :api_type,
42
+ :api_version,
43
+ :log_errors,
44
+ :organization_id,
45
+ :uri_base,
46
+ :request_timeout,
41
47
  :extra_headers
42
48
 
43
49
  DEFAULT_API_VERSION = "v1".freeze
44
50
  DEFAULT_URI_BASE = "https://api.openai.com/".freeze
45
51
  DEFAULT_REQUEST_TIMEOUT = 120
52
+ DEFAULT_LOG_ERRORS = false
46
53
 
47
54
  def initialize
48
55
  @access_token = nil
49
56
  @api_type = nil
50
57
  @api_version = DEFAULT_API_VERSION
58
+ @log_errors = DEFAULT_LOG_ERRORS
51
59
  @organization_id = nil
52
60
  @uri_base = DEFAULT_URI_BASE
53
61
  @request_timeout = DEFAULT_REQUEST_TIMEOUT
54
62
  @extra_headers = {}
55
63
  end
56
-
57
- def access_token
58
- return @access_token if @access_token
59
-
60
- error_text = "OpenAI access token missing! See https://github.com/alexrudall/ruby-openai#usage"
61
- raise ConfigurationError, error_text
62
- end
63
64
  end
64
65
 
65
66
  class << self
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")
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: 6.5.0
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-31 00:00:00.000000000 Z
11
+ date: 2024-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -89,6 +89,7 @@ files:
89
89
  - lib/openai.rb
90
90
  - lib/openai/assistants.rb
91
91
  - lib/openai/audio.rb
92
+ - lib/openai/batches.rb
92
93
  - lib/openai/client.rb
93
94
  - lib/openai/compatibility.rb
94
95
  - lib/openai/files.rb
@@ -129,8 +130,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
130
  - !ruby/object:Gem::Version
130
131
  version: '0'
131
132
  requirements: []
132
- rubygems_version: 3.4.22
133
+ rubygems_version: 3.5.9
133
134
  signing_key:
134
135
  specification_version: 4
135
- summary: "OpenAI API + Ruby! \U0001F916\U0001FA75"
136
+ summary: "OpenAI API + Ruby! \U0001F916❤️"
136
137
  test_files: []