Gemini AI
A Ruby Gem for interacting with Gemini through Vertex AI, Generative Language API, or AI Studio, Google's generative AI services.
This Gem is designed to provide low-level access to Gemini, enabling people to build abstractions on top of it. If you are interested in more high-level abstractions or more user-friendly tools, you may want to consider Nano Bots π π€.
TL;DR and Quick Start
gem 'gemini-ai', '~> 4.2.0'
require 'gemini-ai'
client = Gemini.new(
credentials: {
service: 'generative-language-api',
api_key: ENV['GOOGLE_API_KEY']
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_path: 'google-credentials.json',
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_contents: File.read('google-credentials.json'),
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
result = client.stream_generate_content({
contents: { role: 'user', parts: { text: 'hi!' } }
})
Result:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }]
Index
Setup
Installing
gem install gemini-ai -v 4.2.0
gem 'gemini-ai', '~> 4.2.0'
Credentials
β οΈ DISCLAIMER: Be careful with what you are doing, and never trust others' code related to this. These commands and instructions alter the level of access to your Google Cloud Account, and running them naively can lead to security risks as well as financial risks. People with access to your account can use it to steal data or incur charges. Run these commands at your own responsibility and due diligence; expect no warranties from the contributors of this project.
Option 1: API Key (Generative Language API)
You need a Google Cloud Project, and then you can generate an API Key through the Google Cloud Console here.
You also need to enable the Generative Language API service in your Google Cloud Console, which can be done here.
Alternatively, you can generate an API Key through Google AI Studio here. However, this approach will automatically create a project for you in your Google Cloud Account.
Option 2: Service Account Credentials File (Vertex AI API)
You need a Google Cloud Project and a Service Account to use Vertex AI API.
After creating them, you need to enable the Vertex AI API for your project by clicking Enable
here: Vertex AI API.
You can create credentials for your Service Account here, where you will be able to download a JSON file named google-credentials.json
that should have content similar to this:
{
"type": "service_account",
"project_id": "YOUR_PROJECT_ID",
"private_key_id": "a00...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "PROJECT_ID@PROJECT_ID.iam.gserviceaccount.com",
"client_id": "000...",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/..."
}
You need to have the necessary policies (roles/aiplatform.user
and possibly roles/ml.admin
) in place to use the Vertex AI API.
You can add them by navigating to the IAM Console and clicking on the "Edit principal" (βοΈ pencil icon) next to your Service Account.
Alternatively, you can add them through the gcloud CLI as follows:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='serviceAccount:PROJECT_ID@PROJECT_ID.iam.gserviceaccount.com' \
--role='roles/aiplatform.user'
Some people reported having trouble accessing the API, and adding the role roles/ml.admin
fixed it:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='serviceAccount:PROJECT_ID@PROJECT_ID.iam.gserviceaccount.com' \
--role='roles/ml.admin'
If you are not using a Service Account:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='user:YOUR@MAIL.COM' \
--role='roles/aiplatform.user'
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='user:YOUR@MAIL.COM' \
--role='roles/ml.admin'
Option 3: Application Default Credentials (Vertex AI API)
Similar to Option 2, but you don't need to download a google-credentials.json
. Application Default Credentials automatically find credentials based on the application environment.
For local development, you can generate your default credentials using the gcloud CLI as follows:
gcloud auth application-default login
For more details about alternative methods and different environments, check the official documentation:
Set up Application Default Credentials
Required Data
After choosing an option, you should have all the necessary data and access to use Gemini.
Option 1, for API Key:
{
service: 'generative-language-api',
api_key: 'GOOGLE_API_KEY'
}
Remember that hardcoding your API key in code is unsafe; it's preferable to use environment variables:
{
service: 'generative-language-api',
api_key: ENV['GOOGLE_API_KEY']
}
Option 2: For the Service Account, provide a google-credentials.json
file and a REGION
:
{
service: 'vertex-ai-api',
file_path: 'google-credentials.json',
region: 'us-east4'
}
Alternatively, you can pass the file contents instead of the path:
{
service: 'vertex-ai-api',
file_contents: File.read('google-credentials.json'),
region: 'us-east4'
}
{
service: 'vertex-ai-api',
file_contents: ENV['GOOGLE_CREDENTIALS_FILE_CONTENTS'],
region: 'us-east4'
}
Option 3: For Application Default Credentials, omit both the api_key
and the file_path
:
{
service: 'vertex-ai-api',
region: 'us-east4'
}
As of the writing of this README, the following regions support Gemini:
Iowa (us-central1)
Las Vegas, Nevada (us-west4)
MontrΓ©al, Canada (northamerica-northeast1)
Northern Virginia (us-east4)
Oregon (us-west1)
Seoul, Korea (asia-northeast3)
Singapore (asia-southeast1)
Tokyo, Japan (asia-northeast1)
You can follow here if new regions are available: Gemini API
You might want to explicitly set a Google Cloud Project ID, which you can do as follows:
{
service: 'vertex-ai-api',
project_id: 'PROJECT_ID'
}
Custom Version
By default, the gem uses the v1
version of the APIs. You may want to use a different version:
client = Gemini.new(
credentials: {
service: 'generative-language-api',
api_key: ENV['GOOGLE_API_KEY'],
version: 'v1beta'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_path: 'google-credentials.json',
region: 'us-east4',
version: 'v1beta'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_contents: File.read('google-credentials.json'),
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
region: 'us-east4',
version: 'v1beta'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
Available Models
These models are accessible to the repository author as of June 2025 in the us-east4
region. Access to models may vary by region, user, and account. All models here are expected to work, if you can access them. This is just a reference of what a "typical" user may expect to have access to right away:
Model | Vertex AI | Generative Language |
---|
gemini-pro-vision | β
| π |
gemini-pro | β
| β
|
gemini-1.5-pro-preview-0514 | β
| π |
gemini-1.5-pro-preview-0409 | β
| π |
gemini-1.5-pro | β
| β
|
gemini-1.5-flash-preview-0514 | β
| π |
gemini-1.5-flash | β
| β
|
gemini-1.0-pro-vision-latest | π | π |
gemini-1.0-pro-vision-001 | β
| π |
gemini-1.0-pro-vision | β
| π |
gemini-1.0-pro-latest | π | β
|
gemini-1.0-pro-002 | β
| π |
gemini-1.0-pro-001 | β
| β
|
gemini-1.0-pro | β
| β
|
gemini-ultra | π | π |
gemini-1.0-ultra | π | π |
gemini-1.0-ultra-001 | π | π |
text-embedding-preview-0514 | π | π |
text-embedding-preview-0409 | π | π |
text-embedding-004 | β
| β
|
embedding-001 | π | β
|
text-multilingual-embedding-002 | β
| π |
textembedding-gecko-multilingual@001 | β
| π |
textembedding-gecko-multilingual@latest | β
| π |
textembedding-gecko@001 | β
| π |
textembedding-gecko@002 | β
| π |
textembedding-gecko@003 | β
| π |
textembedding-gecko@latest | β
| π |
You can follow new models at:
This is the code used for generating this table that you can use to explore your own access.
Usage
Client
Ensure that you have all the required data for authentication.
Create a new client:
require 'gemini-ai'
client = Gemini.new(
credentials: {
service: 'generative-language-api',
api_key: ENV['GOOGLE_API_KEY']
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_path: 'google-credentials.json',
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
file_contents: File.read('google-credentials.json'),
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
client = Gemini.new(
credentials: {
service: 'vertex-ai-api',
region: 'us-east4'
},
options: { model: 'gemini-pro', server_sent_events: true }
)
Methods
Chat
stream_generate_content
Receiving Stream Events
Ensure that you have enabled Server-Sent Events before using blocks for streaming:
client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
) do |event, parsed, raw|
puts event
end
Event:
{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }
Without Events
You can use stream_generate_content
without events:
result = client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
)
In this case, the result will be an array with all the received events:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }]
You can mix both as well:
result = client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
) do |event, parsed, raw|
puts event
end
generate_content
result = client.generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
)
Result:
{ 'candidates' =>
[{ 'content' => { 'parts' => [{ 'text' => 'Hello! How can I assist you today?' }], 'role' => 'model' },
'finishReason' => 'STOP',
'index' => 0,
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'promptFeedback' =>
{ 'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] } }
As of the writing of this README, only the generative-language-api
service supports the generate_content
method; vertex-ai-api
does not.
Embeddings
predict
Vertex AI API generates embeddings through the predict
method (documentation), and you need a client set up to use an embedding model (e.g. text-embedding-004
):
result = client.predict(
{ instances: [{ content: 'What is life?' }],
parameters: { autoTruncate: true } }
)
Result:
{ 'predictions' =>
[{ 'embeddings' =>
{ 'statistics' => { 'truncated' => false, 'token_count' => 4 },
'values' =>
[-0.006861076690256596,
0.00020840796059928834,
-0.028549950569868088,
0.0020092015620321035,
0.03279878571629524,
-0.014905261807143688] } }],
'metadata' => { 'billableCharacterCount' => 11 } }
embed_content
Generative Language API generates embeddings through the embed_content
method (documentation), and you need a client set up to use an embedding model (e.g. text-embedding-004
):
result = client.embed_content(
{ content: { parts: [{ text: 'What is life?' }] } }
)
Result:
{ 'embedding' =>
{ 'values' =>
[-0.0065307906,
-0.0001632607,
-0.028370803,
0.0019950708,
0.032798845,
-0.014878989] } }
Modes
Text
result = client.stream_generate_content({
contents: { role: 'user', parts: { text: 'hi!' } }
})
Result:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }]
Image
Courtesy of Unsplash
Switch to the gemini-pro-vision
model:
client = Gemini.new(
credentials: { service: 'vertex-ai-api', region: 'us-east4' },
options: { model: 'gemini-pro-vision', server_sent_events: true }
)
Then, encode the image as Base64 and add its MIME type:
require 'base64'
result = client.stream_generate_content(
{ contents: [
{ role: 'user', parts: [
{ text: 'Please describe this image.' },
{ inline_data: {
mime_type: 'image/jpeg',
data: Base64.strict_encode64(File.read('piano.jpg'))
} }
] }
] }
)
The result:
[{ 'candidates' =>
[{ 'content' =>
{ 'role' => 'model',
'parts' =>
[{ 'text' =>
' A black and white image of an old piano. The piano is an upright model, with the keys on the right side of the image. The piano is' }] },
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }] },
{ 'candidates' =>
[{ 'content' => { 'role' => 'model', 'parts' => [{ 'text' => ' sitting on a tiled floor. There is a small round object on the top of the piano.' }] },
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => { 'promptTokenCount' => 263, 'candidatesTokenCount' => 50, 'totalTokenCount' => 313 } }]
Video
https://gist.github.com/assets/29520/f82bccbf-02d2-4899-9c48-eb8a0a5ef741
ALT: A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred.
Courtesy of Pexels
Switch to the gemini-pro-vision
model:
client = Gemini.new(
credentials: { service: 'vertex-ai-api', region: 'us-east4' },
options: { model: 'gemini-pro-vision', server_sent_events: true }
)
Then, encode the video as Base64 and add its MIME type:
require 'base64'
result = client.stream_generate_content(
{ contents: [
{ role: 'user', parts: [
{ text: 'Please describe this video.' },
{ inline_data: {
mime_type: 'video/mp4',
data: Base64.strict_encode64(File.read('coffee.mp4'))
} }
] }
] }
)
The result:
[{"candidates"=>
[{"content"=>
{"role"=>"model",
"parts"=>
[{"text"=>
" A white and gold cup is being filled with coffee. The coffee is dark and rich. The cup is sitting on a black surface. The background is blurred"}]},
"safetyRatings"=>
[{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
"usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>31, "totalTokenCount"=>1068}},
{"candidates"=>
[{"content"=>{"role"=>"model", "parts"=>[{"text"=>"."}]},
"finishReason"=>"STOP",
"safetyRatings"=>
[{"category"=>"HARM_CATEGORY_HARASSMENT", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_HATE_SPEECH", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability"=>"NEGLIGIBLE"},
{"category"=>"HARM_CATEGORY_DANGEROUS_CONTENT", "probability"=>"NEGLIGIBLE"}]}],
"usageMetadata"=>{"promptTokenCount"=>1037, "candidatesTokenCount"=>32, "totalTokenCount"=>1069}}]
Streaming vs. Server-Sent Events (SSE)
Server-Sent Events (SSE) is a technology that allows certain endpoints to offer streaming capabilities, such as creating the impression that "the model is typing along with you," rather than delivering the entire answer all at once.
You can set up the client to use Server-Sent Events (SSE) for all supported endpoints:
client = Gemini.new(
credentials: { ... },
options: { model: 'gemini-pro', server_sent_events: true }
)
Or, you can decide on a request basis:
client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } },
server_sent_events: true
)
With Server-Sent Events (SSE) enabled, you can use a block to receive partial results via events. This feature is particularly useful for methods that offer streaming capabilities, such as stream_generate_content
:
client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
) do |event, parsed, raw|
puts event
end
Event:
{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }
Even though streaming methods utilize Server-Sent Events (SSE), using this feature doesn't necessarily mean streaming data. For example, when generate_content
is called with SSE enabled, you will receive all the data at once in a single event, rather than through multiple partial events. This occurs because generate_content
isn't designed for streaming, even though it is capable of utilizing Server-Sent Events.
Server-Sent Events (SSE) Hang
Method calls will hang until the server-sent events finish, so even without providing a block, you can obtain the final results of the received events:
result = client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'hi!' } } },
server_sent_events: true
)
Result:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }]
Non-Streaming
Depending on the service, you can use the generate_content
method, which does not stream the answer.
You can also use methods designed for streaming without necessarily processing partial events; instead, you can wait for the result of all received events:
result = client.stream_generate_content({
contents: { role: 'user', parts: { text: 'hi!' } },
server_sent_events: false
})
Result:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'text' => 'Hello! How may I assist you?' }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 2,
'candidatesTokenCount' => 8,
'totalTokenCount' => 10
} }]
Back-and-Forth Conversations
To maintain a back-and-forth conversation, you need to append the received responses and build a history for your requests:
result = client.stream_generate_content(
{ contents: [
{ role: 'user', parts: { text: 'Hi! My name is Purple.' } },
{ role: 'model', parts: { text: "Hello Purple! It's nice to meet you." } },
{ role: 'user', parts: { text: "What's my name?" } }
] }
)
Result:
[{ 'candidates' =>
[{ 'content' =>
{ 'role' => 'model',
'parts' => [
{ 'text' => "Purple.\n\nYou told me your name was Purple in your first message to me.\n\nIs there anything" }
] },
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }] },
{ 'candidates' =>
[{ 'content' => { 'role' => 'model', 'parts' => [{ 'text' => ' else I can help you with today, Purple?' }] },
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => {
'promptTokenCount' => 24,
'candidatesTokenCount' => 31,
'totalTokenCount' => 55
} }]
Safety Settings
You can configure safety attributes for your requests.
Harm Categories:
HARM_CATEGORY_UNSPECIFIED
, HARM_CATEGORY_HARASSMENT
, HARM_CATEGORY_HATE_SPEECH
, HARM_CATEGORY_SEXUALLY_EXPLICIT
, HARM_CATEGORY_DANGEROUS_CONTENT
.
Thresholds:
BLOCK_NONE
, BLOCK_ONLY_HIGH
, BLOCK_MEDIUM_AND_ABOVE
, BLOCK_LOW_AND_ABOVE
, HARM_BLOCK_THRESHOLD_UNSPECIFIED
.
Example:
client.stream_generate_content(
{
contents: { role: 'user', parts: { text: 'hi!' } },
safetySettings: [
{
category: 'HARM_CATEGORY_UNSPECIFIED',
threshold: 'BLOCK_ONLY_HIGH'
},
{
category: 'HARM_CATEGORY_HARASSMENT',
threshold: 'BLOCK_ONLY_HIGH'
},
{
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: 'BLOCK_ONLY_HIGH'
},
{
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
threshold: 'BLOCK_ONLY_HIGH'
},
{
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
threshold: 'BLOCK_ONLY_HIGH'
}
]
}
)
Google started to block the usage of BLOCK_NONE
unless:
User has requested a restricted HarmBlockThreshold setting BLOCK_NONE. You can get access either (a) through an allowlist via your Google account team, or (b) by switching your account type to monthly invoiced billing via this instruction: https://cloud.google.com/billing/docs/how-to/invoiced-billing
System Instructions
Some models support system instructions:
client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'Hi! Who are you?' } },
system_instruction: { role: 'user', parts: { text: 'Your name is Neko.' } } }
)
Output:
Hi! I'm Neko, a factual language model from Google AI.
client.stream_generate_content(
{ contents: { role: 'user', parts: { text: 'Hi! Who are you?' } },
system_instruction: {
role: 'user', parts: [
{ text: 'You are a cat.' },
{ text: 'Your name is Neko.' }
]
} }
)
Output:
Meow! I'm Neko, a fluffy and playful cat. :3
Counting Tokens
You can count tokens and preview how many tokens a request is expected to consume:
client.count_tokens(
{ contents: { role: 'user', parts: { text: 'hi!' } } }
)
Output for Generative Language API:
{ 'totalTokens' => 3 }
Output for Vertex AI API:
{ 'totalTokens' => 2, 'totalBillableCharacters' => 3 }
JSON Format Responses
As of the writing of this README, only the vertex-ai-api
service and gemini
models version 1.5
support this feature.
The Gemini API provides a configuration parameter to request a response in JSON format:
require 'json'
result = client.stream_generate_content(
{
contents: {
role: 'user',
parts: {
text: 'List 3 random colors.'
}
},
generation_config: {
response_mime_type: 'application/json'
}
}
)
json_string = result
.map { |response| response.dig('candidates', 0, 'content', 'parts') }
.map { |parts| parts.map { |part| part['text'] }.join }
.join
puts JSON.parse(json_string).inspect
Output:
{ 'colors' => ['Dark Salmon', 'Indigo', 'Lavender'] }
JSON Schema
While Gemini 1.5 Flash models only accept a text description of the JSON schema you want returned, the Gemini 1.5 Pro models let you pass a schema object (or a Python type equivalent), and the model output will strictly follow that schema. This is also known as controlled generation or constrained decoding.
You can also provide a JSON Schema for the expected JSON output:
require 'json'
result = client.stream_generate_content(
{
contents: {
role: 'user',
parts: {
text: 'List 3 random colors.'
}
},
generation_config: {
response_mime_type: 'application/json',
response_schema: {
type: 'object',
properties: {
colors: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
}
}
}
}
}
)
json_string = result
.map { |response| response.dig('candidates', 0, 'content', 'parts') }
.map { |parts| parts.map { |part| part['text'] }.join }
.join
puts JSON.parse(json_string).inspect
Output:
{ 'colors' => [
{ 'name' => 'Lavender Blush' },
{ 'name' => 'Medium Turquoise' },
{ 'name' => 'Dark Slate Gray' }
] }
Models That Support JSON
These models are accessible to the repository author as of June 2025 in the us-east4
region. Access to models may vary by region, user, and account.
- β Does not support JSON mode.
- π‘ Supports JSON mode but not Schema.
- β
Supports JSON mode and Schema.
- π I don't have access to the model.
Model | Vertex AI | Generative Language |
---|
gemini-pro-vision | β | π |
gemini-pro | π‘ | β |
gemini-1.5-pro-preview-0514 | β
| π |
gemini-1.5-pro-preview-0409 | β
| π |
gemini-1.5-pro | β
| β |
gemini-1.5-flash-preview-0514 | π‘ | π |
gemini-1.5-flash | π‘ | β |
gemini-1.0-pro-vision-latest | π | π |
gemini-1.0-pro-vision-001 | β | π |
gemini-1.0-pro-vision | β | π |
gemini-1.0-pro-latest | π | β |
gemini-1.0-pro-002 | π‘ | π |
gemini-1.0-pro-001 | β | β |
gemini-1.0-pro | π‘ | β |
gemini-ultra | π | π |
gemini-1.0-ultra | π | π |
gemini-1.0-ultra-001 | π | π |
Tools (Functions) Calling
As of the writing of this README, only the vertex-ai-api
service and the gemini-pro
model supports tools (functions) calls.
You can provide specifications for tools (functions) using JSON Schema to generate potential calls to them:
input = {
tools: {
function_declarations: [
{
name: 'date_and_time',
description: 'Returns the current date and time in the ISO 8601 format for a given timezone.',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: 'A string represents the timezone to be used for providing a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples include "Asia/Tokyo" and "Europe/Paris". If not provided, the default timezone is the user\'s current timezone.'
}
}
}
}
]
},
contents: [
{ role: 'user', parts: { text: 'What time is it?' } }
]
}
result = client.stream_generate_content(input)
Which may return a request to perform a call:
[{ 'candidates' =>
[{ 'content' => {
'role' => 'model',
'parts' => [{ 'functionCall' => {
'name' => 'date_and_time',
'args' => { 'timezone' => 'local' }
} }]
},
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => { 'promptTokenCount' => 5, 'totalTokenCount' => 5 } }]
Based on these results, you can perform the requested calls and provide their outputs:
gem 'tzinfo', '~> 2.0', '>= 2.0.6'
require 'tzinfo'
require 'time'
function_calls = result.dig(0, 'candidates', 0, 'content', 'parts').filter do |part|
part.key?('functionCall')
end
function_parts = []
function_calls.each do |function_call|
next unless function_call['functionCall']['name'] == 'date_and_time'
timezone = function_call.dig('functionCall', 'args', 'timezone')
time = if !timezone.nil? && timezone != '' && timezone.downcase != 'local'
TZInfo::Timezone.get(timezone).now
else
Time.now
end
function_output = time.iso8601
function_parts << {
functionResponse: {
name: function_call['functionCall']['name'],
response: {
name: function_call['functionCall']['name'],
content: function_output
}
}
}
end
input[:contents] << result.dig(0, 'candidates', 0, 'content')
input[:contents] << { role: 'function', parts: function_parts }
This will be equivalent to the following final input:
{ tools: { function_declarations: [
{ name: 'date_and_time',
description: 'Returns the current date and time in the ISO 8601 format for a given timezone.',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: "A string represents the timezone to be used for providing a datetime, following the IANA (Internet Assigned Numbers Authority) Time Zone Database. Examples include \"Asia/Tokyo\" and \"Europe/Paris\". If not provided, the default timezone is the user's current timezone."
}
}
} }
] },
contents: [
{ role: 'user', parts: { text: 'What time is it?' } },
{ role: 'model',
parts: [
{ functionCall: { name: 'date_and_time', args: { timezone: 'local' } } }
] },
{ role: 'function',
parts: [{ functionResponse: {
name: 'date_and_time',
response: {
name: 'date_and_time',
content: '2023-12-13T21:15:11-03:00'
}
} }] }
] }
With the input properly arranged, you can make another request:
result = client.stream_generate_content(input)
Which will result in:
[{ 'candidates' =>
[{ 'content' => { 'role' => 'model', 'parts' => [{ 'text' => 'It is 21:15.' }] },
'finishReason' => 'STOP',
'safetyRatings' =>
[{ 'category' => 'HARM_CATEGORY_HARASSMENT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_HATE_SPEECH', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'probability' => 'NEGLIGIBLE' },
{ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'probability' => 'NEGLIGIBLE' }] }],
'usageMetadata' => { 'promptTokenCount' => 5, 'candidatesTokenCount' => 9, 'totalTokenCount' => 14 } }]
New Functionalities and APIs
Google may launch a new endpoint that we haven't covered in the Gem yet. If that's the case, you may still be able to use it through the request
method. For example, stream_generate_content
is just a wrapper for models/gemini-pro:streamGenerateContent
(Generative Language API) or publishers/google/models/gemini-pro:streamGenerateContent
(Vertex AI API), which you can call directly like this:
result = client.request(
'models/gemini-pro:streamGenerateContent',
{ contents: { role: 'user', parts: { text: 'hi!' } } },
request_method: 'POST',
server_sent_events: true
)
result = client.request(
'publishers/google/models/gemini-pro:streamGenerateContent',
{ contents: { role: 'user', parts: { text: 'hi!' } } },
request_method: 'POST',
server_sent_events: true
)
Request Options
Adapter
To enable streaming, the gem uses Faraday with the Typhoeus adapter by default.
You can use a different adapter if you want:
require 'faraday/net_http'
client = Gemini.new(
credentials: { service: 'vertex-ai-api', region: 'us-east4' },
options: {
model: 'gemini-pro',
connection: { adapter: :net_http }
}
)
Timeout
You can set the maximum number of seconds to wait for the request to complete with the timeout
option:
client = Gemini.new(
credentials: { service: 'vertex-ai-api', region: 'us-east4' },
options: {
model: 'gemini-pro',
connection: { request: { timeout: 5 } }
}
)
You can also have more fine-grained control over Faraday's Request Options if you prefer:
client = Gemini.new(
credentials: { service: 'vertex-ai-api', region: 'us-east4' },
options: {
model: 'gemini-pro',
connection: {
request: {
timeout: 5,
open_timeout: 5,
read_timeout: 5,
write_timeout: 5
}
}
}
)
Error Handling
Rescuing
require 'gemini-ai'
begin
client.stream_generate_content({
contents: { role: 'user', parts: { text: 'hi!' } }
})
rescue Gemini::Errors::GeminiError => error
puts error.class
puts error.message
puts error.payload
puts error.request
end
For Short
require 'gemini-ai/errors'
begin
client.stream_generate_content({
contents: { role: 'user', parts: { text: 'hi!' } }
})
rescue GeminiError => error
puts error.class
end
Errors
GeminiError
MissingProjectIdError
UnsupportedServiceError
ConflictingCredentialsError
BlockWithoutServerSentEventsError
RequestError
Development
bundle
rubocop -A
rspec
bundle exec ruby spec/tasks/run-available-models.rb
bundle exec ruby spec/tasks/run-embed.rb
bundle exec ruby spec/tasks/run-generate.rb
bundle exec ruby spec/tasks/run-json.rb
bundle exec ruby spec/tasks/run-safety.rb
bundle exec ruby spec/tasks/run-system.rb
Purpose
This Gem is designed to provide low-level access to Gemini, enabling people to build abstractions on top of it. If you are interested in more high-level abstractions or more user-friendly tools, you may want to consider Nano Bots π π€.
Publish to RubyGems
gem build gemini-ai.gemspec
gem signin
gem push gemini-ai-4.2.0.gem
Updating the README
Install Babashka:
curl -s https://raw.githubusercontent.com/babashka/babashka/master/install | sudo bash
Update the template.md
file and then:
bb tasks/generate-readme.clj
Trick for automatically updating the README.md
when template.md
changes:
sudo pacman -S inotify-tools
sudo apt-get install inotify-tools
sudo dnf install inotify-tools
while inotifywait -e modify template.md; do bb tasks/generate-readme.clj; done
Trick for Markdown Live Preview:
pip install -U markdown_live_preview
mlp README.md -p 8076
Resources and References
These resources and references may be useful throughout your learning process.
Disclaimer
This is not an official Google project, nor is it affiliated with Google in any way.
This software is distributed under the MIT License. This license includes a disclaimer of warranty. Moreover, the authors assume no responsibility for any damage or costs that may result from using this project. Use the Gemini AI Ruby Gem at your own risk.