ruby-openai 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # Ruby OpenAI
2
-
3
2
  [![Gem Version](https://img.shields.io/gem/v/ruby-openai.svg)](https://rubygems.org/gems/ruby-openai)
4
3
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/alexrudall/ruby-openai/blob/main/LICENSE.txt)
5
4
  [![CircleCI Build Status](https://circleci.com/gh/alexrudall/ruby-openai.svg?style=shield)](https://circleci.com/gh/alexrudall/ruby-openai)
6
5
 
7
6
  Use the [OpenAI API](https://openai.com/blog/openai-api/) with Ruby! 🤖❤️
8
7
 
9
- Stream text with GPT-4o, transcribe and translate audio with Whisper, or create images with DALL·E...
8
+ Stream text with GPT-4, transcribe and translate audio with Whisper, or create images with DALL·E...
9
+
10
+ 💥 Click [subscribe now](https://mailchi.mp/8c7b574726a9/ruby-openai) to hear first about new releases in the Rails AI newsletter!
10
11
 
11
- [📚 Rails AI (FREE Book)](https://railsai.com) | [🎮 Ruby AI Builders Discord](https://discord.gg/k4Uc224xVD) | [🐦 X](https://x.com/alexrudall) | [🧠 Anthropic Gem](https://github.com/alexrudall/anthropic) | [🚂 Midjourney Gem](https://github.com/alexrudall/midjourney)
12
+ [![RailsAI Newsletter](https://github.com/user-attachments/assets/737cbb99-6029-42b8-9f22-a106725a4b1f)](https://mailchi.mp/8c7b574726a9/ruby-openai)
13
+
14
+ [🎮 Ruby AI Builders Discord](https://discord.gg/k4Uc224xVD) | [🐦 X](https://x.com/alexrudall) | [🧠 Anthropic Gem](https://github.com/alexrudall/anthropic) | [🚂 Midjourney Gem](https://github.com/alexrudall/midjourney)
12
15
 
13
16
  ## Contents
14
17
 
@@ -17,7 +20,7 @@ Stream text with GPT-4o, transcribe and translate audio with Whisper, or create
17
20
  - [Installation](#installation)
18
21
  - [Bundler](#bundler)
19
22
  - [Gem install](#gem-install)
20
- - [Usage](#usage)
23
+ - [How to use](#how-to-use)
21
24
  - [Quickstart](#quickstart)
22
25
  - [With Config](#with-config)
23
26
  - [Custom timeout or base URI](#custom-timeout-or-base-uri)
@@ -49,7 +52,9 @@ Stream text with GPT-4o, transcribe and translate audio with Whisper, or create
49
52
  - [Threads and Messages](#threads-and-messages)
50
53
  - [Runs](#runs)
51
54
  - [Create and Run](#create-and-run)
55
+ - [Vision in a thread](#vision-in-a-thread)
52
56
  - [Runs involving function tools](#runs-involving-function-tools)
57
+ - [Exploring chunks used in File Search](#exploring-chunks-used-in-file-search)
53
58
  - [Image Generation](#image-generation)
54
59
  - [DALL·E 2](#dalle-2)
55
60
  - [DALL·E 3](#dalle-3)
@@ -60,6 +65,7 @@ Stream text with GPT-4o, transcribe and translate audio with Whisper, or create
60
65
  - [Translate](#translate)
61
66
  - [Transcribe](#transcribe)
62
67
  - [Speech](#speech)
68
+ - [Usage](#usage)
63
69
  - [Errors](#errors-1)
64
70
  - [Development](#development)
65
71
  - [Release](#release)
@@ -97,7 +103,7 @@ and require with:
97
103
  require "openai"
98
104
  ```
99
105
 
100
- ## Usage
106
+ ## How to use
101
107
 
102
108
  - Get your API key from [https://platform.openai.com/account/api-keys](https://platform.openai.com/account/api-keys)
103
109
  - If you belong to multiple organizations, you can get your Organization ID from [https://platform.openai.com/account/org-settings](https://platform.openai.com/account/org-settings)
@@ -120,6 +126,7 @@ For a more robust setup, you can configure the gem with your API keys, for examp
120
126
  ```ruby
121
127
  OpenAI.configure do |config|
122
128
  config.access_token = ENV.fetch("OPENAI_ACCESS_TOKEN")
129
+ config.admin_token = ENV.fetch("OPENAI_ADMIN_TOKEN") # Optional, used for admin endpoints, created here: https://platform.openai.com/settings/organization/admin-keys
123
130
  config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
124
131
  config.log_errors = true # Highly recommended in development, so you can see what errors OpenAI is returning. Not recommended in production because it could leak private data to your logs.
125
132
  end
@@ -131,10 +138,10 @@ Then you can create a client like this:
131
138
  client = OpenAI::Client.new
132
139
  ```
133
140
 
134
- 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:
141
+ 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 and admin_token overridden:
135
142
 
136
143
  ```ruby
137
- client = OpenAI::Client.new(access_token: "access_token_goes_here")
144
+ client = OpenAI::Client.new(access_token: "access_token_goes_here", admin_token: "admin_token_goes_here")
138
145
  ```
139
146
 
140
147
  #### Custom timeout or base URI
@@ -145,15 +152,15 @@ client = OpenAI::Client.new(access_token: "access_token_goes_here")
145
152
 
146
153
  ```ruby
147
154
  client = OpenAI::Client.new(
148
- access_token: "access_token_goes_here",
149
- uri_base: "https://oai.hconeai.com/",
150
- request_timeout: 240,
151
- extra_headers: {
152
- "X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
153
- "X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
154
- "Helicone-Auth": "Bearer HELICONE_API_KEY", # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
155
- "helicone-stream-force-format" => "true", # Use this with Helicone otherwise streaming drops chunks # https://github.com/alexrudall/ruby-openai/issues/251
156
- }
155
+ access_token: "access_token_goes_here",
156
+ uri_base: "https://oai.hconeai.com/",
157
+ request_timeout: 240,
158
+ extra_headers: {
159
+ "X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
160
+ "X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
161
+ "Helicone-Auth": "Bearer HELICONE_API_KEY", # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
162
+ "helicone-stream-force-format" => "true", # Use this with Helicone otherwise streaming drops chunks # https://github.com/alexrudall/ruby-openai/issues/251
163
+ }
157
164
  )
158
165
  ```
159
166
 
@@ -161,16 +168,17 @@ or when configuring the gem:
161
168
 
162
169
  ```ruby
163
170
  OpenAI.configure do |config|
164
- config.access_token = ENV.fetch("OPENAI_ACCESS_TOKEN")
165
- config.log_errors = true # Optional
166
- config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
167
- config.uri_base = "https://oai.hconeai.com/" # Optional
168
- config.request_timeout = 240 # Optional
169
- config.extra_headers = {
170
- "X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
171
- "X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
172
- "Helicone-Auth": "Bearer HELICONE_API_KEY" # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
173
- } # Optional
171
+ config.access_token = ENV.fetch("OPENAI_ACCESS_TOKEN")
172
+ config.admin_token = ENV.fetch("OPENAI_ADMIN_TOKEN") # Optional, used for admin endpoints, created here: https://platform.openai.com/settings/organization/admin-keys
173
+ config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID") # Optional
174
+ config.log_errors = true # Optional
175
+ config.uri_base = "https://oai.hconeai.com/" # Optional
176
+ config.request_timeout = 240 # Optional
177
+ config.extra_headers = {
178
+ "X-Proxy-TTL" => "43200", # For https://github.com/6/openai-caching-proxy-worker#specifying-a-cache-ttl
179
+ "X-Proxy-Refresh": "true", # For https://github.com/6/openai-caching-proxy-worker#refreshing-the-cache
180
+ "Helicone-Auth": "Bearer HELICONE_API_KEY" # For https://docs.helicone.ai/getting-started/integration-method/openai-proxy
181
+ } # Optional
174
182
  end
175
183
  ```
176
184
 
@@ -192,7 +200,7 @@ By default, `ruby-openai` does not log any `Faraday::Error`s encountered while e
192
200
  If you would like to enable this functionality, you can set `log_errors` to `true` when configuring the client:
193
201
 
194
202
  ```ruby
195
- client = OpenAI::Client.new(log_errors: true)
203
+ client = OpenAI::Client.new(log_errors: true)
196
204
  ```
197
205
 
198
206
  ##### Faraday middleware
@@ -200,9 +208,9 @@ If you would like to enable this functionality, you can set `log_errors` to `tru
200
208
  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):
201
209
 
202
210
  ```ruby
203
- client = OpenAI::Client.new do |f|
204
- f.response :logger, Logger.new($stdout), bodies: true
205
- end
211
+ client = OpenAI::Client.new do |f|
212
+ f.response :logger, Logger.new($stdout), bodies: true
213
+ end
206
214
  ```
207
215
 
208
216
  #### Azure
@@ -210,12 +218,12 @@ You can pass [Faraday middleware](https://lostisland.github.io/faraday/#/middlew
210
218
  To use the [Azure OpenAI Service](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/) API, you can configure the gem like this:
211
219
 
212
220
  ```ruby
213
- OpenAI.configure do |config|
214
- config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY")
215
- config.uri_base = ENV.fetch("AZURE_OPENAI_URI")
216
- config.api_type = :azure
217
- config.api_version = "2023-03-15-preview"
218
- end
221
+ OpenAI.configure do |config|
222
+ config.access_token = ENV.fetch("AZURE_OPENAI_API_KEY")
223
+ config.uri_base = ENV.fetch("AZURE_OPENAI_URI")
224
+ config.api_type = :azure
225
+ config.api_version = "2023-03-15-preview"
226
+ end
219
227
  ```
220
228
 
221
229
  where `AZURE_OPENAI_URI` is e.g. `https://custom-domain.openai.azure.com/openai/deployments/gpt-35-turbo`
@@ -240,14 +248,15 @@ client = OpenAI::Client.new(
240
248
  )
241
249
 
242
250
  client.chat(
243
- parameters: {
244
- model: "llama3", # Required.
245
- messages: [{ role: "user", content: "Hello!"}], # Required.
246
- temperature: 0.7,
247
- stream: proc do |chunk, _bytesize|
248
- print chunk.dig("choices", 0, "delta", "content")
249
- end
250
- })
251
+ parameters: {
252
+ model: "llama3", # Required.
253
+ messages: [{ role: "user", content: "Hello!"}], # Required.
254
+ temperature: 0.7,
255
+ stream: proc do |chunk, _bytesize|
256
+ print chunk.dig("choices", 0, "delta", "content")
257
+ end
258
+ }
259
+ )
251
260
 
252
261
  # => Hi! It's nice to meet you. Is there something I can help you with, or would you like to chat?
253
262
  ```
@@ -257,20 +266,21 @@ client.chat(
257
266
  [Groq API Chat](https://console.groq.com/docs/quickstart) is broadly compatible with the OpenAI API, with a [few minor differences](https://console.groq.com/docs/openai). Get an access token from [here](https://console.groq.com/keys), then:
258
267
 
259
268
  ```ruby
260
- client = OpenAI::Client.new(
261
- access_token: "groq_access_token_goes_here",
262
- uri_base: "https://api.groq.com/openai"
263
- )
269
+ client = OpenAI::Client.new(
270
+ access_token: "groq_access_token_goes_here",
271
+ uri_base: "https://api.groq.com/openai"
272
+ )
264
273
 
265
- client.chat(
266
- parameters: {
267
- model: "llama3-8b-8192", # Required.
268
- messages: [{ role: "user", content: "Hello!"}], # Required.
269
- temperature: 0.7,
270
- stream: proc do |chunk, _bytesize|
271
- print chunk.dig("choices", 0, "delta", "content")
272
- end
273
- })
274
+ client.chat(
275
+ parameters: {
276
+ model: "llama3-8b-8192", # Required.
277
+ messages: [{ role: "user", content: "Hello!"}], # Required.
278
+ temperature: 0.7,
279
+ stream: proc do |chunk, _bytesize|
280
+ print chunk.dig("choices", 0, "delta", "content")
281
+ end
282
+ }
283
+ )
274
284
  ```
275
285
 
276
286
  ### Counting Tokens
@@ -300,11 +310,12 @@ GPT is a model that can be used to generate text in a conversational style. You
300
310
 
301
311
  ```ruby
302
312
  response = client.chat(
303
- parameters: {
304
- model: "gpt-4o", # Required.
305
- messages: [{ role: "user", content: "Hello!"}], # Required.
306
- temperature: 0.7,
307
- })
313
+ parameters: {
314
+ model: "gpt-4o", # Required.
315
+ messages: [{ role: "user", content: "Hello!"}], # Required.
316
+ temperature: 0.7,
317
+ }
318
+ )
308
319
  puts response.dig("choices", 0, "message", "content")
309
320
  # => "Hello! How may I assist you today?"
310
321
  ```
@@ -317,14 +328,15 @@ You can stream from the API in realtime, which can be much faster and used to cr
317
328
 
318
329
  ```ruby
319
330
  client.chat(
320
- parameters: {
321
- model: "gpt-4o", # Required.
322
- messages: [{ role: "user", content: "Describe a character called Anna!"}], # Required.
323
- temperature: 0.7,
324
- stream: proc do |chunk, _bytesize|
325
- print chunk.dig("choices", 0, "delta", "content")
326
- end
327
- })
331
+ parameters: {
332
+ model: "gpt-4o", # Required.
333
+ messages: [{ role: "user", content: "Describe a character called Anna!"}], # Required.
334
+ temperature: 0.7,
335
+ stream: proc do |chunk, _bytesize|
336
+ print chunk.dig("choices", 0, "delta", "content")
337
+ end
338
+ }
339
+ )
328
340
  # => "Anna is a young woman in her mid-twenties, with wavy chestnut hair that falls to her shoulders..."
329
341
  ```
330
342
 
@@ -333,12 +345,13 @@ Note: In order to get usage information, you can provide the [`stream_options` p
333
345
  ```ruby
334
346
  stream_proc = proc { |chunk, _bytesize| puts "--------------"; puts chunk.inspect; }
335
347
  client.chat(
336
- parameters: {
337
- model: "gpt-4o",
338
- stream: stream_proc,
339
- stream_options: { include_usage: true },
340
- messages: [{ role: "user", content: "Hello!"}],
341
- })
348
+ parameters: {
349
+ model: "gpt-4o",
350
+ stream: stream_proc,
351
+ stream_options: { include_usage: true },
352
+ messages: [{ role: "user", content: "Hello!"}],
353
+ }
354
+ )
342
355
  # => --------------
343
356
  # => {"id"=>"chatcmpl-7bbq05PiZqlHxjV1j7OHnKKDURKaf", "object"=>"chat.completion.chunk", "created"=>1718750612, "model"=>"gpt-4o-2024-05-13", "system_fingerprint"=>"fp_9cb5d38cf7", "choices"=>[{"index"=>0, "delta"=>{"role"=>"assistant", "content"=>""}, "logprobs"=>nil, "finish_reason"=>nil}], "usage"=>nil}
344
357
  # => --------------
@@ -365,10 +378,11 @@ messages = [
365
378
  }
366
379
  ]
367
380
  response = client.chat(
368
- parameters: {
369
- model: "gpt-4-vision-preview", # Required.
370
- messages: [{ role: "user", content: messages}], # Required.
371
- })
381
+ parameters: {
382
+ model: "gpt-4-vision-preview", # Required.
383
+ messages: [{ role: "user", content: messages}], # Required.
384
+ }
385
+ )
372
386
  puts response.dig("choices", 0, "message", "content")
373
387
  # => "The image depicts a serene natural landscape featuring a long wooden boardwalk extending straight ahead"
374
388
  ```
@@ -378,21 +392,22 @@ puts response.dig("choices", 0, "message", "content")
378
392
  You can set the response_format to ask for responses in JSON:
379
393
 
380
394
  ```ruby
381
- response = client.chat(
382
- parameters: {
383
- model: "gpt-4o",
384
- response_format: { type: "json_object" },
385
- messages: [{ role: "user", content: "Hello! Give me some JSON please."}],
386
- temperature: 0.7,
387
- })
388
- puts response.dig("choices", 0, "message", "content")
389
- {
390
- "name": "John",
391
- "age": 30,
392
- "city": "New York",
393
- "hobbies": ["reading", "traveling", "hiking"],
394
- "isStudent": false
395
- }
395
+ response = client.chat(
396
+ parameters: {
397
+ model: "gpt-4o",
398
+ response_format: { type: "json_object" },
399
+ messages: [{ role: "user", content: "Hello! Give me some JSON please."}],
400
+ temperature: 0.7,
401
+ })
402
+ puts response.dig("choices", 0, "message", "content")
403
+ # =>
404
+ # {
405
+ # "name": "John",
406
+ # "age": 30,
407
+ # "city": "New York",
408
+ # "hobbies": ["reading", "traveling", "hiking"],
409
+ # "isStudent": false
410
+ # }
396
411
  ```
397
412
 
398
413
  You can stream it as well!
@@ -402,26 +417,28 @@ You can stream it as well!
402
417
  parameters: {
403
418
  model: "gpt-4o",
404
419
  messages: [{ role: "user", content: "Can I have some JSON please?"}],
405
- response_format: { type: "json_object" },
406
- stream: proc do |chunk, _bytesize|
407
- print chunk.dig("choices", 0, "delta", "content")
408
- end
409
- })
410
- {
411
- "message": "Sure, please let me know what specific JSON data you are looking for.",
412
- "JSON_data": {
413
- "example_1": {
414
- "key_1": "value_1",
415
- "key_2": "value_2",
416
- "key_3": "value_3"
417
- },
418
- "example_2": {
419
- "key_4": "value_4",
420
- "key_5": "value_5",
421
- "key_6": "value_6"
422
- }
420
+ response_format: { type: "json_object" },
421
+ stream: proc do |chunk, _bytesize|
422
+ print chunk.dig("choices", 0, "delta", "content")
423
+ end
423
424
  }
424
- }
425
+ )
426
+ # =>
427
+ # {
428
+ # "message": "Sure, please let me know what specific JSON data you are looking for.",
429
+ # "JSON_data": {
430
+ # "example_1": {
431
+ # "key_1": "value_1",
432
+ # "key_2": "value_2",
433
+ # "key_3": "value_3"
434
+ # },
435
+ # "example_2": {
436
+ # "key_4": "value_4",
437
+ # "key_5": "value_5",
438
+ # "key_6": "value_6"
439
+ # }
440
+ # }
441
+ # }
425
442
  ```
426
443
 
427
444
  ### Functions
@@ -429,7 +446,6 @@ You can stream it as well!
429
446
  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)).
430
447
 
431
448
  ```ruby
432
-
433
449
  def get_current_weather(location:, unit: "fahrenheit")
434
450
  # Here you could use a weather api to fetch the weather.
435
451
  "The weather in #{location} is nice 🌞 #{unit}"
@@ -470,8 +486,9 @@ response =
470
486
  },
471
487
  }
472
488
  ],
473
- tool_choice: "required" # Optional, defaults to "auto"
474
- # Can also put "none" or specific functions, see docs
489
+ # Optional, defaults to "auto"
490
+ # Can also put "none" or specific functions, see docs
491
+ tool_choice: "required"
475
492
  },
476
493
  )
477
494
 
@@ -485,12 +502,13 @@ if message["role"] == "assistant" && message["tool_calls"]
485
502
  tool_call.dig("function", "arguments"),
486
503
  { symbolize_names: true },
487
504
  )
488
- function_response = case function_name
505
+ function_response =
506
+ case function_name
489
507
  when "get_current_weather"
490
508
  get_current_weather(**function_args) # => "The weather is nice 🌞"
491
509
  else
492
510
  # decide how to handle
493
- end
511
+ end
494
512
 
495
513
  # For a subsequent message with the role "tool", OpenAI requires the preceding message to have a tool_calls argument.
496
514
  messages << message
@@ -507,7 +525,8 @@ if message["role"] == "assistant" && message["tool_calls"]
507
525
  parameters: {
508
526
  model: "gpt-4o",
509
527
  messages: messages
510
- })
528
+ }
529
+ )
511
530
 
512
531
  puts second_response.dig("choices", 0, "message", "content")
513
532
 
@@ -523,11 +542,12 @@ Hit the OpenAI API for a completion using other GPT-3 models:
523
542
 
524
543
  ```ruby
525
544
  response = client.completions(
526
- parameters: {
527
- model: "gpt-4o",
528
- prompt: "Once upon a time",
529
- max_tokens: 5
530
- })
545
+ parameters: {
546
+ model: "gpt-4o",
547
+ prompt: "Once upon a time",
548
+ max_tokens: 5
549
+ }
550
+ )
531
551
  puts response["choices"].map { |c| c["text"] }
532
552
  # => [", there lived a great"]
533
553
  ```
@@ -538,10 +558,10 @@ You can use the embeddings endpoint to get a vector of numbers representing an i
538
558
 
539
559
  ```ruby
540
560
  response = client.embeddings(
541
- parameters: {
542
- model: "text-embedding-ada-002",
543
- input: "The food was delicious and the waiter..."
544
- }
561
+ parameters: {
562
+ model: "text-embedding-ada-002",
563
+ input: "The food was delicious and the waiter..."
564
+ }
545
565
  )
546
566
 
547
567
  puts response.dig("data", 0, "embedding")
@@ -687,9 +707,9 @@ You can then use this file ID to create a fine tuning job:
687
707
 
688
708
  ```ruby
689
709
  response = client.finetunes.create(
690
- parameters: {
691
- training_file: file_id,
692
- model: "gpt-4o"
710
+ parameters: {
711
+ training_file: file_id,
712
+ model: "gpt-4o"
693
713
  })
694
714
  fine_tune_id = response["id"]
695
715
  ```
@@ -712,17 +732,17 @@ This fine-tuned model name can then be used in chat completions:
712
732
 
713
733
  ```ruby
714
734
  response = client.chat(
715
- parameters: {
716
- model: fine_tuned_model,
717
- messages: [{ role: "user", content: "I love Mondays!"}]
718
- }
735
+ parameters: {
736
+ model: fine_tuned_model,
737
+ messages: [{ role: "user", content: "I love Mondays!" }]
738
+ }
719
739
  )
720
740
  response.dig("choices", 0, "message", "content")
721
741
  ```
722
742
 
723
743
  You can also capture the events for a job:
724
744
 
725
- ```
745
+ ```ruby
726
746
  client.finetunes.list_events(id: fine_tune_id)
727
747
  ```
728
748
 
@@ -867,25 +887,26 @@ To create a new assistant:
867
887
 
868
888
  ```ruby
869
889
  response = client.assistants.create(
870
- parameters: {
871
- model: "gpt-4o",
872
- name: "OpenAI-Ruby test assistant",
873
- description: nil,
874
- instructions: "You are a Ruby dev bot. When asked a question, write and run Ruby code to answer the question",
875
- tools: [
876
- { type: "code_interpreter" },
877
- { type: "file_search" }
878
- ],
879
- tool_resources: {
880
- code_interpreter: {
881
- file_ids: [] # See Files section above for how to upload files
882
- },
883
- file_search: {
884
- vector_store_ids: [] # See Vector Stores section above for how to add vector stores
885
- }
886
- },
887
- "metadata": { my_internal_version_id: "1.0.0" }
888
- })
890
+ parameters: {
891
+ model: "gpt-4o",
892
+ name: "OpenAI-Ruby test assistant",
893
+ description: nil,
894
+ instructions: "You are a Ruby dev bot. When asked a question, write and run Ruby code to answer the question",
895
+ tools: [
896
+ { type: "code_interpreter" },
897
+ { type: "file_search" }
898
+ ],
899
+ tool_resources: {
900
+ code_interpreter: {
901
+ file_ids: [] # See Files section above for how to upload files
902
+ },
903
+ file_search: {
904
+ vector_store_ids: [] # See Vector Stores section above for how to add vector stores
905
+ }
906
+ },
907
+ "metadata": { my_internal_version_id: "1.0.0" }
908
+ }
909
+ )
889
910
  assistant_id = response["id"]
890
911
  ```
891
912
 
@@ -905,16 +926,17 @@ You can modify an existing assistant using the assistant's id (see [API document
905
926
 
906
927
  ```ruby
907
928
  response = client.assistants.modify(
908
- id: assistant_id,
909
- parameters: {
910
- name: "Modified Test Assistant for OpenAI-Ruby",
911
- metadata: { my_internal_version_id: '1.0.1' }
912
- })
929
+ id: assistant_id,
930
+ parameters: {
931
+ name: "Modified Test Assistant for OpenAI-Ruby",
932
+ metadata: { my_internal_version_id: '1.0.1' }
933
+ }
934
+ )
913
935
  ```
914
936
 
915
937
  You can delete assistants:
916
938
 
917
- ```
939
+ ```ruby
918
940
  client.assistants.delete(id: assistant_id)
919
941
  ```
920
942
 
@@ -930,11 +952,12 @@ thread_id = response["id"]
930
952
 
931
953
  # Add initial message from user (see https://platform.openai.com/docs/api-reference/messages/createMessage)
932
954
  message_id = client.messages.create(
933
- thread_id: thread_id,
934
- parameters: {
935
- role: "user", # Required for manually created messages
936
- content: "Can you help me write an API library to interact with the OpenAI API please?"
937
- })["id"]
955
+ thread_id: thread_id,
956
+ parameters: {
957
+ role: "user", # Required for manually created messages
958
+ content: "Can you help me write an API library to interact with the OpenAI API please?"
959
+ }
960
+ )["id"]
938
961
 
939
962
  # Retrieve individual message
940
963
  message = client.messages.retrieve(thread_id: thread_id, id: message_id)
@@ -958,32 +981,38 @@ To submit a thread to be evaluated with the model of an assistant, create a `Run
958
981
 
959
982
  ```ruby
960
983
  # Create run (will use instruction/model/tools from Assistant's definition)
961
- response = client.runs.create(thread_id: thread_id,
962
- parameters: {
963
- assistant_id: assistant_id,
964
- max_prompt_tokens: 256,
965
- max_completion_tokens: 16
966
- })
984
+ response = client.runs.create(
985
+ thread_id: thread_id,
986
+ parameters: {
987
+ assistant_id: assistant_id,
988
+ max_prompt_tokens: 256,
989
+ max_completion_tokens: 16
990
+ }
991
+ )
967
992
  run_id = response['id']
968
993
  ```
969
994
 
970
995
  You can stream the message chunks as they come through:
971
996
 
972
997
  ```ruby
973
- client.runs.create(thread_id: thread_id,
974
- parameters: {
975
- assistant_id: assistant_id,
976
- max_prompt_tokens: 256,
977
- max_completion_tokens: 16,
978
- stream: proc do |chunk, _bytesize|
979
- print chunk.dig("delta", "content", 0, "text", "value") if chunk["object"] == "thread.message.delta"
980
- end
981
- })
998
+ client.runs.create(
999
+ thread_id: thread_id,
1000
+ parameters: {
1001
+ assistant_id: assistant_id,
1002
+ max_prompt_tokens: 256,
1003
+ max_completion_tokens: 16,
1004
+ stream: proc do |chunk, _bytesize|
1005
+ if chunk["object"] == "thread.message.delta"
1006
+ print chunk.dig("delta", "content", 0, "text", "value")
1007
+ end
1008
+ end
1009
+ }
1010
+ )
982
1011
  ```
983
1012
 
984
1013
  To get the status of a Run:
985
1014
 
986
- ```
1015
+ ```ruby
987
1016
  response = client.runs.retrieve(id: run_id, thread_id: thread_id)
988
1017
  status = response['status']
989
1018
  ```
@@ -992,23 +1021,23 @@ The `status` response can include the following strings `queued`, `in_progress`,
992
1021
 
993
1022
  ```ruby
994
1023
  while true do
995
- response = client.runs.retrieve(id: run_id, thread_id: thread_id)
996
- status = response['status']
997
-
998
- case status
999
- when 'queued', 'in_progress', 'cancelling'
1000
- puts 'Sleeping'
1001
- sleep 1 # Wait one second and poll again
1002
- when 'completed'
1003
- break # Exit loop and report result to user
1004
- when 'requires_action'
1005
- # Handle tool calls (see below)
1006
- when 'cancelled', 'failed', 'expired'
1007
- puts response['last_error'].inspect
1008
- break # or `exit`
1009
- else
1010
- puts "Unknown status response: #{status}"
1011
- end
1024
+ response = client.runs.retrieve(id: run_id, thread_id: thread_id)
1025
+ status = response['status']
1026
+
1027
+ case status
1028
+ when 'queued', 'in_progress', 'cancelling'
1029
+ puts 'Sleeping'
1030
+ sleep 1 # Wait one second and poll again
1031
+ when 'completed'
1032
+ break # Exit loop and report result to user
1033
+ when 'requires_action'
1034
+ # Handle tool calls (see below)
1035
+ when 'cancelled', 'failed', 'expired'
1036
+ puts response['last_error'].inspect
1037
+ break # or `exit`
1038
+ else
1039
+ puts "Unknown status response: #{status}"
1040
+ end
1012
1041
  end
1013
1042
  ```
1014
1043
 
@@ -1020,30 +1049,30 @@ messages = client.messages.list(thread_id: thread_id, parameters: { order: 'asc'
1020
1049
 
1021
1050
  # Alternatively retrieve the `run steps` for the run which link to the messages:
1022
1051
  run_steps = client.run_steps.list(thread_id: thread_id, run_id: run_id, parameters: { order: 'asc' })
1023
- new_message_ids = run_steps['data'].filter_map { |step|
1052
+ new_message_ids = run_steps['data'].filter_map do |step|
1024
1053
  if step['type'] == 'message_creation'
1025
1054
  step.dig('step_details', "message_creation", "message_id")
1026
1055
  end # Ignore tool calls, because they don't create new messages.
1027
- }
1056
+ end
1028
1057
 
1029
1058
  # Retrieve the individual messages
1030
- new_messages = new_message_ids.map { |msg_id|
1059
+ new_messages = new_message_ids.map do |msg_id|
1031
1060
  client.messages.retrieve(id: msg_id, thread_id: thread_id)
1032
- }
1061
+ end
1033
1062
 
1034
1063
  # Find the actual response text in the content array of the messages
1035
- new_messages.each { |msg|
1036
- msg['content'].each { |content_item|
1037
- case content_item['type']
1038
- when 'text'
1039
- puts content_item.dig('text', 'value')
1040
- # Also handle annotations
1041
- when 'image_file'
1042
- # Use File endpoint to retrieve file contents via id
1043
- id = content_item.dig('image_file', 'file_id')
1044
- end
1045
- }
1046
- }
1064
+ new_messages.each do |msg|
1065
+ msg['content'].each do |content_item|
1066
+ case content_item['type']
1067
+ when 'text'
1068
+ puts content_item.dig('text', 'value')
1069
+ # Also handle annotations
1070
+ when 'image_file'
1071
+ # Use File endpoint to retrieve file contents via id
1072
+ id = content_item.dig('image_file', 'file_id')
1073
+ end
1074
+ end
1075
+ end
1047
1076
  ```
1048
1077
 
1049
1078
  You can also update the metadata on messages, including messages that come from the assistant.
@@ -1052,7 +1081,11 @@ You can also update the metadata on messages, including messages that come from
1052
1081
  metadata = {
1053
1082
  user_id: "abc123"
1054
1083
  }
1055
- message = client.messages.modify(id: message_id, thread_id: thread_id, parameters: { metadata: metadata })
1084
+ message = client.messages.modify(
1085
+ id: message_id,
1086
+ thread_id: thread_id,
1087
+ parameters: { metadata: metadata },
1088
+ )
1056
1089
  ```
1057
1090
 
1058
1091
  At any time you can list all runs which have been performed on a particular thread or are currently running:
@@ -1071,41 +1104,117 @@ run_id = response['id']
1071
1104
  thread_id = response['thread_id']
1072
1105
  ```
1073
1106
 
1107
+ #### Vision in a thread
1108
+
1109
+ You can include images in a thread and they will be described & read by the LLM. In this example I'm using [this file](https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png):
1110
+
1111
+ ```ruby
1112
+ require "openai"
1113
+
1114
+ # Make a client
1115
+ client = OpenAI::Client.new(
1116
+ access_token: "access_token_goes_here",
1117
+ log_errors: true # Don't log errors in production.
1118
+ )
1119
+
1120
+ # Upload image as a file
1121
+ file_id = client.files.upload(
1122
+ parameters: {
1123
+ file: "path/to/example.png",
1124
+ purpose: "assistants",
1125
+ }
1126
+ )["id"]
1127
+
1128
+ # Create assistant (You could also use an existing one here)
1129
+ assistant_id = client.assistants.create(
1130
+ parameters: {
1131
+ model: "gpt-4o",
1132
+ name: "Image reader",
1133
+ instructions: "You are an image describer. You describe the contents of images.",
1134
+ }
1135
+ )["id"]
1136
+
1137
+ # Create thread
1138
+ thread_id = client.threads.create["id"]
1139
+
1140
+ # Add image in message
1141
+ client.messages.create(
1142
+ thread_id: thread_id,
1143
+ parameters: {
1144
+ role: "user", # Required for manually created messages
1145
+ content: [
1146
+ {
1147
+ "type": "text",
1148
+ "text": "What's in this image?"
1149
+ },
1150
+ {
1151
+ "type": "image_file",
1152
+ "image_file": { "file_id": file_id }
1153
+ }
1154
+ ]
1155
+ }
1156
+ )
1157
+
1158
+ # Run thread
1159
+ run_id = client.runs.create(
1160
+ thread_id: thread_id,
1161
+ parameters: { assistant_id: assistant_id }
1162
+ )["id"]
1163
+
1164
+ # Wait until run in complete
1165
+ status = nil
1166
+ until status == "completed" do
1167
+ sleep(0.1)
1168
+ status = client.runs.retrieve(id: run_id, thread_id: thread_id)['status']
1169
+ end
1170
+
1171
+ # Get the response
1172
+ messages = client.messages.list(thread_id: thread_id, parameters: { order: 'asc' })
1173
+ messages.dig("data", -1, "content", 0, "text", "value")
1174
+ => "The image contains a placeholder graphic with a tilted, stylized representation of a postage stamp in the top part, which includes an abstract landscape with hills and a sun. Below the stamp, in the middle of the image, there is italicized text in a light golden color that reads, \"This is just an example.\" The background is a light pastel shade, and a yellow border frames the entire image."
1175
+ ```
1176
+
1074
1177
  #### Runs involving function tools
1075
1178
 
1076
1179
  In case you are allowing the assistant to access `function` tools (they are defined in the same way as functions during chat completion), you might get a status code of `requires_action` when the assistant wants you to evaluate one or more function tools:
1077
1180
 
1078
1181
  ```ruby
1079
1182
  def get_current_weather(location:, unit: "celsius")
1080
- # Your function code goes here
1081
- if location =~ /San Francisco/i
1082
- return unit == "celsius" ? "The weather is nice 🌞 at 27°C" : "The weather is nice 🌞 at 80°F"
1083
- else
1084
- return unit == "celsius" ? "The weather is icy 🥶 at -5°C" : "The weather is icy 🥶 at 23°F"
1085
- end
1183
+ # Your function code goes here
1184
+ if location =~ /San Francisco/i
1185
+ return unit == "celsius" ? "The weather is nice 🌞 at 27°C" : "The weather is nice 🌞 at 80°F"
1186
+ else
1187
+ return unit == "celsius" ? "The weather is icy 🥶 at -5°C" : "The weather is icy 🥶 at 23°F"
1188
+ end
1086
1189
  end
1087
1190
 
1088
1191
  if status == 'requires_action'
1192
+ tools_to_call = response.dig('required_action', 'submit_tool_outputs', 'tool_calls')
1089
1193
 
1090
- tools_to_call = response.dig('required_action', 'submit_tool_outputs', 'tool_calls')
1091
-
1092
- my_tool_outputs = tools_to_call.map { |tool|
1093
- # Call the functions based on the tool's name
1094
- function_name = tool.dig('function', 'name')
1095
- arguments = JSON.parse(
1096
- tool.dig("function", "arguments"),
1097
- { symbolize_names: true },
1098
- )
1194
+ my_tool_outputs = tools_to_call.map { |tool|
1195
+ # Call the functions based on the tool's name
1196
+ function_name = tool.dig('function', 'name')
1197
+ arguments = JSON.parse(
1198
+ tool.dig("function", "arguments"),
1199
+ { symbolize_names: true },
1200
+ )
1099
1201
 
1100
- tool_output = case function_name
1101
- when "get_current_weather"
1102
- get_current_weather(**arguments)
1103
- end
1202
+ tool_output = case function_name
1203
+ when "get_current_weather"
1204
+ get_current_weather(**arguments)
1205
+ end
1104
1206
 
1105
- { tool_call_id: tool['id'], output: tool_output }
1207
+ {
1208
+ tool_call_id: tool['id'],
1209
+ output: tool_output,
1106
1210
  }
1211
+ }
1107
1212
 
1108
- client.runs.submit_tool_outputs(thread_id: thread_id, run_id: run_id, parameters: { tool_outputs: my_tool_outputs })
1213
+ client.runs.submit_tool_outputs(
1214
+ thread_id: thread_id,
1215
+ run_id: run_id,
1216
+ parameters: { tool_outputs: my_tool_outputs }
1217
+ )
1109
1218
  end
1110
1219
  ```
1111
1220
 
@@ -1115,19 +1224,19 @@ Note that you have 10 minutes to submit your tool output before the run expires.
1115
1224
 
1116
1225
  Take a deep breath. You might need a drink for this one.
1117
1226
 
1118
- It's possible for OpenAI to share what chunks it used in its internal RAG Pipeline to create its filesearch example.
1227
+ It's possible for OpenAI to share what chunks it used in its internal RAG Pipeline to create its filesearch results.
1119
1228
 
1120
1229
  An example spec can be found [here](https://github.com/alexrudall/ruby-openai/blob/main/spec/openai/client/assistant_file_search_spec.rb) that does this, just so you know it's possible.
1121
1230
 
1122
1231
  Here's how to get the chunks used in a file search. In this example I'm using [this file](https://css4.pub/2015/textbook/somatosensory.pdf):
1123
1232
 
1124
- ```
1233
+ ```ruby
1125
1234
  require "openai"
1126
1235
 
1127
1236
  # Make a client
1128
1237
  client = OpenAI::Client.new(
1129
1238
  access_token: "access_token_goes_here",
1130
- log_errors: true # Don't do this in production.
1239
+ log_errors: true # Don't log errors in production.
1131
1240
  )
1132
1241
 
1133
1242
  # Upload your file(s)
@@ -1191,9 +1300,6 @@ steps = client.run_steps.list(
1191
1300
  parameters: { order: "asc" }
1192
1301
  )
1193
1302
 
1194
- # Get the last step ID (or whichever one you want to look at)
1195
- step_id = steps["data"].first["id"]
1196
-
1197
1303
  # Retrieve all the steps. Include the "GIVE ME THE CHUNKS" incantation again.
1198
1304
  steps = steps["data"].map do |step|
1199
1305
  client.run_steps.retrieve(
@@ -1230,7 +1336,12 @@ Generate images using DALL·E 2 or DALL·E 3!
1230
1336
  For DALL·E 2 the size of any generated images must be one of `256x256`, `512x512` or `1024x1024` - if not specified the image will default to `1024x1024`.
1231
1337
 
1232
1338
  ```ruby
1233
- response = client.images.generate(parameters: { prompt: "A baby sea otter cooking pasta wearing a hat of some sort", size: "256x256" })
1339
+ response = client.images.generate(
1340
+ parameters: {
1341
+ prompt: "A baby sea otter cooking pasta wearing a hat of some sort",
1342
+ size: "256x256",
1343
+ }
1344
+ )
1234
1345
  puts response.dig("data", 0, "url")
1235
1346
  # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."
1236
1347
  ```
@@ -1242,7 +1353,14 @@ puts response.dig("data", 0, "url")
1242
1353
  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`.
1243
1354
 
1244
1355
  ```ruby
1245
- 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" })
1356
+ response = client.images.generate(
1357
+ parameters: {
1358
+ prompt: "A springer spaniel cooking pasta wearing a hat of some sort",
1359
+ model: "dall-e-3",
1360
+ size: "1024x1792",
1361
+ quality: "standard",
1362
+ }
1363
+ )
1246
1364
  puts response.dig("data", 0, "url")
1247
1365
  # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."
1248
1366
  ```
@@ -1254,7 +1372,13 @@ puts response.dig("data", 0, "url")
1254
1372
  Fill in the transparent part of an image, or upload a mask with transparent sections to indicate the parts of an image that can be changed according to your prompt...
1255
1373
 
1256
1374
  ```ruby
1257
- response = client.images.edit(parameters: { prompt: "A solid red Ruby on a blue background", image: "image.png", mask: "mask.png" })
1375
+ response = client.images.edit(
1376
+ parameters: {
1377
+ prompt: "A solid red Ruby on a blue background",
1378
+ image: "image.png",
1379
+ mask: "mask.png",
1380
+ }
1381
+ )
1258
1382
  puts response.dig("data", 0, "url")
1259
1383
  # => "https://oaidalleapiprodscus.blob.core.windows.net/private/org-Rf437IxKhh..."
1260
1384
  ```
@@ -1294,10 +1418,11 @@ The translations API takes as input the audio file in any of the supported langu
1294
1418
 
1295
1419
  ```ruby
1296
1420
  response = client.audio.translate(
1297
- parameters: {
1298
- model: "whisper-1",
1299
- file: File.open("path_to_file", "rb"),
1300
- })
1421
+ parameters: {
1422
+ model: "whisper-1",
1423
+ file: File.open("path_to_file", "rb"),
1424
+ }
1425
+ )
1301
1426
  puts response["text"]
1302
1427
  # => "Translation of the text"
1303
1428
  ```
@@ -1310,11 +1435,12 @@ You can pass the language of the audio file to improve transcription quality. Su
1310
1435
 
1311
1436
  ```ruby
1312
1437
  response = client.audio.transcribe(
1313
- parameters: {
1314
- model: "whisper-1",
1315
- file: File.open("path_to_file", "rb"),
1316
- language: "en" # Optional
1317
- })
1438
+ parameters: {
1439
+ model: "whisper-1",
1440
+ file: File.open("path_to_file", "rb"),
1441
+ language: "en", # Optional
1442
+ }
1443
+ )
1318
1444
  puts response["text"]
1319
1445
  # => "Transcription of the text"
1320
1446
  ```
@@ -1330,23 +1456,81 @@ response = client.audio.speech(
1330
1456
  input: "This is a speech test!",
1331
1457
  voice: "alloy",
1332
1458
  response_format: "mp3", # Optional
1333
- speed: 1.0 # Optional
1459
+ speed: 1.0, # Optional
1334
1460
  }
1335
1461
  )
1336
1462
  File.binwrite('demo.mp3', response)
1337
1463
  # => mp3 file that plays: "This is a speech test!"
1338
1464
  ```
1339
1465
 
1340
- ### Errors
1466
+ ### Usage
1467
+ The Usage API provides information about the cost of various OpenAI services within your organization.
1468
+ To use Admin APIs like Usage, you need to set an OPENAI_ADMIN_TOKEN, which can be generated [here](https://platform.openai.com/settings/organization/admin-keys).
1341
1469
 
1342
- HTTP errors can be caught like this:
1470
+ ```ruby
1471
+ OpenAI.configure do |config|
1472
+ config.admin_token = ENV.fetch("OPENAI_ADMIN_TOKEN")
1473
+ end
1474
+
1475
+ # or
1343
1476
 
1477
+ client = OpenAI::Client.new(admin_token: "123abc")
1344
1478
  ```
1345
- begin
1346
- OpenAI::Client.new.models.retrieve(id: "gpt-4o")
1347
- rescue Faraday::Error => e
1348
- raise "Got a Faraday error: #{e}"
1479
+
1480
+ You can retrieve usage data for different endpoints and time periods:
1481
+
1482
+ ```ruby
1483
+ one_day_ago = Time.now.to_i - 86_400
1484
+
1485
+ # Retrieve costs data
1486
+ response = client.usage.costs(parameters: { start_time: one_day_ago })
1487
+ response["data"].each do |bucket|
1488
+ bucket["results"].each do |result|
1489
+ puts "#{Time.at(bucket["start_time"]).to_date}: $#{result.dig("amount", "value").round(2)}"
1349
1490
  end
1491
+ end
1492
+ => 2025-02-09: $0.0
1493
+ => 2025-02-10: $0.42
1494
+
1495
+ # Retrieve completions usage data
1496
+ response = client.usage.completions(parameters: { start_time: one_day_ago })
1497
+ puts response["data"]
1498
+
1499
+ # Retrieve embeddings usage data
1500
+ response = client.usage.embeddings(parameters: { start_time: one_day_ago })
1501
+ puts response["data"]
1502
+
1503
+ # Retrieve moderations usage data
1504
+ response = client.usage.moderations(parameters: { start_time: one_day_ago })
1505
+ puts response["data"]
1506
+
1507
+ # Retrieve image generation usage data
1508
+ response = client.usage.images(parameters: { start_time: one_day_ago })
1509
+ puts response["data"]
1510
+
1511
+ # Retrieve audio speech usage data
1512
+ response = client.usage.audio_speeches(parameters: { start_time: one_day_ago })
1513
+ puts response["data"]
1514
+
1515
+ # Retrieve audio transcription usage data
1516
+ response = client.usage.audio_transcriptions(parameters: { start_time: one_day_ago })
1517
+ puts response["data"]
1518
+
1519
+ # Retrieve vector stores usage data
1520
+ response = client.usage.vector_stores(parameters: { start_time: one_day_ago })
1521
+ puts response["data"]
1522
+ ```
1523
+
1524
+ ### Errors
1525
+
1526
+ HTTP errors can be caught like this:
1527
+
1528
+ ```ruby
1529
+ begin
1530
+ OpenAI::Client.new.models.retrieve(id: "gpt-4o")
1531
+ rescue Faraday::Error => e
1532
+ raise "Got a Faraday error: #{e}"
1533
+ end
1350
1534
  ```
1351
1535
 
1352
1536
  ## Development
@@ -1358,15 +1542,11 @@ To install this gem onto your local machine, run `bundle exec rake install`.
1358
1542
  To run all tests, execute the command `bundle exec rake`, which will also run the linter (Rubocop). This repository uses [VCR](https://github.com/vcr/vcr) to log API requests.
1359
1543
 
1360
1544
  > [!WARNING]
1361
- > If you have an `OPENAI_ACCESS_TOKEN` in your `ENV`, running the specs will use this to run the specs against the actual API, which will be slow and cost you money - 2 cents or more! Remove it from your environment with `unset` or similar if you just want to run the specs against the stored VCR responses.
1545
+ > If you have an `OPENAI_ACCESS_TOKEN` and `OPENAI_ADMIN_TOKEN` in your `ENV`, running the specs will hit the actual API, which will be slow and cost you money - 2 cents or more! Remove them from your environment with `unset` or similar if you just want to run the specs against the stored VCR responses.
1362
1546
 
1363
1547
  ## Release
1364
1548
 
1365
- First run the specs without VCR so they actually hit the API. This will cost 2 cents or more. Set OPENAI_ACCESS_TOKEN in your environment or pass it in like this:
1366
-
1367
- ```
1368
- OPENAI_ACCESS_TOKEN=123abc bundle exec rspec
1369
- ```
1549
+ First run the specs without VCR so they actually hit the API. This will cost 2 cents or more. Set OPENAI_ACCESS_TOKEN and OPENAI_ADMIN_TOKEN in your environment.
1370
1550
 
1371
1551
  Then update the version number in `version.rb`, update `CHANGELOG.md`, run `bundle install` to update Gemfile.lock, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1372
1552