llm-gemini 0.13.1__tar.gz → 0.14.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: llm-gemini
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: LLM plugin to access Google's Gemini family of models
5
5
  Author: Simon Willison
6
6
  License: Apache-2.0
@@ -17,6 +17,7 @@ Requires-Dist: ijson
17
17
  Provides-Extra: test
18
18
  Requires-Dist: pytest; extra == "test"
19
19
  Requires-Dist: pytest-recording; extra == "test"
20
+ Requires-Dist: pytest-asyncio; extra == "test"
20
21
  Requires-Dist: nest-asyncio; extra == "test"
21
22
 
22
23
  # llm-gemini
@@ -145,7 +146,7 @@ llm chat -m gemini-1.5-pro-latest
145
146
 
146
147
  ## Embeddings
147
148
 
148
- The plugin also adds support for the `text-embedding-004` embedding model.
149
+ The plugin also adds support for the `gemini-embedding-exp-03-07` and `text-embedding-004` embedding models.
149
150
 
150
151
  Run that against a single string like this:
151
152
  ```bash
@@ -153,10 +154,20 @@ llm embed -m text-embedding-004 -c 'hello world'
153
154
  ```
154
155
  This returns a JSON array of 768 numbers.
155
156
 
157
+ The `gemini-embedding-exp-03-07` model is larger, returning 3072 numbers. You can also use variants of it that are truncated down to smaller sizes:
158
+
159
+ - `gemini-embedding-exp-03-07` - 3072 numbers
160
+ - `gemini-embedding-exp-03-07-2048` - 2048 numbers
161
+ - `gemini-embedding-exp-03-07-1024` - 1024 numbers
162
+ - `gemini-embedding-exp-03-07-512` - 512 numbers
163
+ - `gemini-embedding-exp-03-07-256` - 256 numbers
164
+ - `gemini-embedding-exp-03-07-128` - 128 numbers
165
+
156
166
  This command will embed every `README.md` file in child directories of the current directory and store the results in a SQLite database called `embed.db` in a collection called `readmes`:
157
167
 
158
168
  ```bash
159
- llm embed-multi readmes --files . '*/README.md' -d embed.db -m text-embedding-004
169
+ llm embed-multi readmes -d embed.db -m gemini-embedding-exp-03-07-128 \
170
+ --files . '*/README.md'
160
171
  ```
161
172
  You can then run similarity searches against that collection like this:
162
173
  ```bash
@@ -124,7 +124,7 @@ llm chat -m gemini-1.5-pro-latest
124
124
 
125
125
  ## Embeddings
126
126
 
127
- The plugin also adds support for the `text-embedding-004` embedding model.
127
+ The plugin also adds support for the `gemini-embedding-exp-03-07` and `text-embedding-004` embedding models.
128
128
 
129
129
  Run that against a single string like this:
130
130
  ```bash
@@ -132,10 +132,20 @@ llm embed -m text-embedding-004 -c 'hello world'
132
132
  ```
133
133
  This returns a JSON array of 768 numbers.
134
134
 
135
+ The `gemini-embedding-exp-03-07` model is larger, returning 3072 numbers. You can also use variants of it that are truncated down to smaller sizes:
136
+
137
+ - `gemini-embedding-exp-03-07` - 3072 numbers
138
+ - `gemini-embedding-exp-03-07-2048` - 2048 numbers
139
+ - `gemini-embedding-exp-03-07-1024` - 1024 numbers
140
+ - `gemini-embedding-exp-03-07-512` - 512 numbers
141
+ - `gemini-embedding-exp-03-07-256` - 256 numbers
142
+ - `gemini-embedding-exp-03-07-128` - 128 numbers
143
+
135
144
  This command will embed every `README.md` file in child directories of the current directory and store the results in a SQLite database called `embed.db` in a collection called `readmes`:
136
145
 
137
146
  ```bash
138
- llm embed-multi readmes --files . '*/README.md' -d embed.db -m text-embedding-004
147
+ llm embed-multi readmes -d embed.db -m gemini-embedding-exp-03-07-128 \
148
+ --files . '*/README.md'
139
149
  ```
140
150
  You can then run similarity searches against that collection like this:
141
151
  ```bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: llm-gemini
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: LLM plugin to access Google's Gemini family of models
5
5
  Author: Simon Willison
6
6
  License: Apache-2.0
@@ -17,6 +17,7 @@ Requires-Dist: ijson
17
17
  Provides-Extra: test
18
18
  Requires-Dist: pytest; extra == "test"
19
19
  Requires-Dist: pytest-recording; extra == "test"
20
+ Requires-Dist: pytest-asyncio; extra == "test"
20
21
  Requires-Dist: nest-asyncio; extra == "test"
21
22
 
22
23
  # llm-gemini
@@ -145,7 +146,7 @@ llm chat -m gemini-1.5-pro-latest
145
146
 
146
147
  ## Embeddings
147
148
 
148
- The plugin also adds support for the `text-embedding-004` embedding model.
149
+ The plugin also adds support for the `gemini-embedding-exp-03-07` and `text-embedding-004` embedding models.
149
150
 
150
151
  Run that against a single string like this:
151
152
  ```bash
@@ -153,10 +154,20 @@ llm embed -m text-embedding-004 -c 'hello world'
153
154
  ```
154
155
  This returns a JSON array of 768 numbers.
155
156
 
157
+ The `gemini-embedding-exp-03-07` model is larger, returning 3072 numbers. You can also use variants of it that are truncated down to smaller sizes:
158
+
159
+ - `gemini-embedding-exp-03-07` - 3072 numbers
160
+ - `gemini-embedding-exp-03-07-2048` - 2048 numbers
161
+ - `gemini-embedding-exp-03-07-1024` - 1024 numbers
162
+ - `gemini-embedding-exp-03-07-512` - 512 numbers
163
+ - `gemini-embedding-exp-03-07-256` - 256 numbers
164
+ - `gemini-embedding-exp-03-07-128` - 128 numbers
165
+
156
166
  This command will embed every `README.md` file in child directories of the current directory and store the results in a SQLite database called `embed.db` in a collection called `readmes`:
157
167
 
158
168
  ```bash
159
- llm embed-multi readmes --files . '*/README.md' -d embed.db -m text-embedding-004
169
+ llm embed-multi readmes -d embed.db -m gemini-embedding-exp-03-07-128 \
170
+ --files . '*/README.md'
160
171
  ```
161
172
  You can then run similarity searches against that collection like this:
162
173
  ```bash
@@ -5,4 +5,5 @@ ijson
5
5
  [test]
6
6
  pytest
7
7
  pytest-recording
8
+ pytest-asyncio
8
9
  nest-asyncio
@@ -88,18 +88,24 @@ def resolve_type(attachment):
88
88
  return mime_type
89
89
 
90
90
 
91
- def cleanup_schema(schema):
91
+ def cleanup_schema(schema, in_properties=False):
92
92
  "Gemini supports only a subset of JSON schema"
93
93
  keys_to_remove = ("$schema", "additionalProperties", "title")
94
- # Recursively remove them
94
+
95
95
  if isinstance(schema, dict):
96
- for key in keys_to_remove:
97
- schema.pop(key, None)
98
- for value in schema.values():
99
- cleanup_schema(value)
96
+ # Only remove keys if we're not inside a 'properties' block.
97
+ if not in_properties:
98
+ for key in keys_to_remove:
99
+ schema.pop(key, None)
100
+ for key, value in list(schema.items()):
101
+ # If the key is 'properties', set the flag for its value.
102
+ if key == "properties" and isinstance(value, dict):
103
+ cleanup_schema(value, in_properties=True)
104
+ else:
105
+ cleanup_schema(value, in_properties=in_properties)
100
106
  elif isinstance(schema, list):
101
- for value in schema:
102
- cleanup_schema(value)
107
+ for item in schema:
108
+ cleanup_schema(item, in_properties=in_properties)
103
109
  return schema
104
110
 
105
111
 
@@ -378,9 +384,19 @@ class AsyncGeminiPro(_SharedGemini, llm.AsyncKeyModel):
378
384
 
379
385
  @llm.hookimpl
380
386
  def register_embedding_models(register):
387
+ register(GeminiEmbeddingModel("text-embedding-004", "text-embedding-004"))
388
+ # gemini-embedding-exp-03-07 in different truncation sizes
381
389
  register(
382
- GeminiEmbeddingModel("text-embedding-004", "text-embedding-004"),
390
+ GeminiEmbeddingModel(
391
+ "gemini-embedding-exp-03-07", "gemini-embedding-exp-03-07"
392
+ ),
383
393
  )
394
+ for i in (128, 256, 512, 1024, 2048):
395
+ register(
396
+ GeminiEmbeddingModel(
397
+ f"gemini-embedding-exp-03-07-{i}", f"gemini-embedding-exp-03-07", i
398
+ ),
399
+ )
384
400
 
385
401
 
386
402
  class GeminiEmbeddingModel(llm.EmbeddingModel):
@@ -388,9 +404,10 @@ class GeminiEmbeddingModel(llm.EmbeddingModel):
388
404
  key_env_var = "LLM_GEMINI_KEY"
389
405
  batch_size = 20
390
406
 
391
- def __init__(self, model_id, gemini_model_id):
407
+ def __init__(self, model_id, gemini_model_id, truncate=None):
392
408
  self.model_id = model_id
393
409
  self.gemini_model_id = gemini_model_id
410
+ self.truncate = truncate
394
411
 
395
412
  def embed_batch(self, items):
396
413
  headers = {
@@ -416,4 +433,7 @@ class GeminiEmbeddingModel(llm.EmbeddingModel):
416
433
  )
417
434
 
418
435
  response.raise_for_status()
419
- return [item["values"] for item in response.json()["embeddings"]]
436
+ values = [item["values"] for item in response.json()["embeddings"]]
437
+ if self.truncate:
438
+ values = [value[: self.truncate] for value in values]
439
+ return values
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llm-gemini"
3
- version = "0.13.1"
3
+ version = "0.14.1"
4
4
  description = "LLM plugin to access Google's Gemini family of models"
5
5
  readme = "README.md"
6
6
  authors = [{name = "Simon Willison"}]
@@ -24,4 +24,8 @@ CI = "https://github.com/simonw/llm-gemini/actions"
24
24
  gemini = "llm_gemini"
25
25
 
26
26
  [project.optional-dependencies]
27
- test = ["pytest", "pytest-recording", "nest-asyncio"]
27
+ test = ["pytest", "pytest-recording", "pytest-asyncio", "nest-asyncio"]
28
+
29
+ [tool.pytest.ini_options]
30
+ asyncio_mode = "strict"
31
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,212 @@
1
+ import llm
2
+ import nest_asyncio
3
+ import json
4
+ import os
5
+ import pytest
6
+ import pydantic
7
+ from llm_gemini import cleanup_schema
8
+
9
+ nest_asyncio.apply()
10
+
11
+ GEMINI_API_KEY = os.environ.get("PYTEST_GEMINI_API_KEY", None) or "gm-..."
12
+
13
+
14
+ @pytest.mark.vcr
15
+ @pytest.mark.asyncio
16
+ async def test_prompt():
17
+ model = llm.get_model("gemini-1.5-flash-latest")
18
+ response = model.prompt("Name for a pet pelican, just the name", key=GEMINI_API_KEY)
19
+ assert str(response) == "Percy\n"
20
+ assert response.response_json == {
21
+ "candidates": [
22
+ {
23
+ "finishReason": "STOP",
24
+ "safetyRatings": [
25
+ {
26
+ "category": "HARM_CATEGORY_HATE_SPEECH",
27
+ "probability": "NEGLIGIBLE",
28
+ },
29
+ {
30
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
31
+ "probability": "NEGLIGIBLE",
32
+ },
33
+ {
34
+ "category": "HARM_CATEGORY_HARASSMENT",
35
+ "probability": "NEGLIGIBLE",
36
+ },
37
+ {
38
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
39
+ "probability": "NEGLIGIBLE",
40
+ },
41
+ ],
42
+ }
43
+ ],
44
+ "modelVersion": "gemini-1.5-flash-latest",
45
+ }
46
+ assert response.token_details == {
47
+ "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 9}],
48
+ "candidatesTokensDetails": [{"modality": "TEXT", "tokenCount": 2}],
49
+ }
50
+ assert response.input_tokens == 9
51
+ assert response.output_tokens == 2
52
+
53
+ # And try it async too
54
+ async_model = llm.get_async_model("gemini-1.5-flash-latest")
55
+ response = await async_model.prompt(
56
+ "Name for a pet pelican, just the name", key=GEMINI_API_KEY
57
+ )
58
+ text = await response.text()
59
+ assert text == "Percy\n"
60
+
61
+
62
+ @pytest.mark.vcr
63
+ @pytest.mark.asyncio
64
+ async def test_prompt_with_pydantic_schema():
65
+ class Dog(pydantic.BaseModel):
66
+ name: str
67
+ age: int
68
+ bio: str
69
+
70
+ model = llm.get_model("gemini-1.5-flash-latest")
71
+ response = model.prompt(
72
+ "Invent a cool dog", key=GEMINI_API_KEY, schema=Dog, stream=False
73
+ )
74
+ assert json.loads(response.text()) == {
75
+ "age": 3,
76
+ "bio": "A fluffy Samoyed with exceptional intelligence and a love for belly rubs. He's mastered several tricks, including fetching the newspaper and opening doors.",
77
+ "name": "Cloud",
78
+ }
79
+ assert response.response_json == {
80
+ "candidates": [
81
+ {
82
+ "finishReason": "STOP",
83
+ "safetyRatings": [
84
+ {
85
+ "category": "HARM_CATEGORY_HATE_SPEECH",
86
+ "probability": "NEGLIGIBLE",
87
+ },
88
+ {
89
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
90
+ "probability": "NEGLIGIBLE",
91
+ },
92
+ {
93
+ "category": "HARM_CATEGORY_HARASSMENT",
94
+ "probability": "NEGLIGIBLE",
95
+ },
96
+ {
97
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
98
+ "probability": "NEGLIGIBLE",
99
+ },
100
+ ],
101
+ }
102
+ ],
103
+ "modelVersion": "gemini-1.5-flash-latest",
104
+ }
105
+ assert response.input_tokens == 10
106
+
107
+
108
+ @pytest.mark.vcr
109
+ @pytest.mark.parametrize(
110
+ "model_id",
111
+ (
112
+ "gemini-embedding-exp-03-07",
113
+ "gemini-embedding-exp-03-07-128",
114
+ "gemini-embedding-exp-03-07-512",
115
+ ),
116
+ )
117
+ def test_embedding(model_id, monkeypatch):
118
+ monkeypatch.setenv("LLM_GEMINI_KEY", GEMINI_API_KEY)
119
+ model = llm.get_embedding_model(model_id)
120
+ response = model.embed("Some text goes here")
121
+ expected_length = 3072
122
+ if model_id.endswith("-128"):
123
+ expected_length = 128
124
+ elif model_id.endswith("-512"):
125
+ expected_length = 512
126
+ assert len(response) == expected_length
127
+
128
+
129
+ @pytest.mark.parametrize(
130
+ "schema,expected",
131
+ [
132
+ # Test 1: Top-level keys removal
133
+ (
134
+ {
135
+ "$schema": "http://json-schema.org/draft-07/schema#",
136
+ "title": "Example Schema",
137
+ "additionalProperties": False,
138
+ "type": "object",
139
+ },
140
+ {"type": "object"},
141
+ ),
142
+ # Test 2: Preserve keys within a "properties" block
143
+ (
144
+ {
145
+ "type": "object",
146
+ "properties": {
147
+ "authors": {"type": "string"},
148
+ "title": {"type": "string"},
149
+ "reference": {"type": "string"},
150
+ "year": {"type": "string"},
151
+ },
152
+ "title": "This should be removed from the top-level",
153
+ },
154
+ {
155
+ "type": "object",
156
+ "properties": {
157
+ "authors": {"type": "string"},
158
+ "title": {"type": "string"},
159
+ "reference": {"type": "string"},
160
+ "year": {"type": "string"},
161
+ },
162
+ },
163
+ ),
164
+ # Test 3: Nested keys outside and inside properties block
165
+ (
166
+ {
167
+ "definitions": {
168
+ "info": {
169
+ "title": "Info title", # should be removed because it's not inside a "properties" block
170
+ "description": "A description",
171
+ "properties": {
172
+ "name": {
173
+ "title": "Name Title",
174
+ "type": "string",
175
+ }, # title here should be preserved
176
+ "$schema": {
177
+ "type": "string"
178
+ }, # should be preserved as it's within properties
179
+ },
180
+ }
181
+ },
182
+ "$schema": "http://example.com/schema",
183
+ },
184
+ {
185
+ "definitions": {
186
+ "info": {
187
+ "description": "A description",
188
+ "properties": {
189
+ "name": {"title": "Name Title", "type": "string"},
190
+ "$schema": {"type": "string"},
191
+ },
192
+ }
193
+ }
194
+ },
195
+ ),
196
+ # Test 4: List of schemas
197
+ (
198
+ [
199
+ {
200
+ "$schema": "http://json-schema.org/draft-07/schema#",
201
+ "type": "object",
202
+ },
203
+ {"title": "Should be removed", "type": "array"},
204
+ ],
205
+ [{"type": "object"}, {"type": "array"}],
206
+ ),
207
+ ],
208
+ )
209
+ def test_cleanup_schema(schema, expected):
210
+ # Use a deep copy so the original test data remains unchanged.
211
+ result = cleanup_schema(schema)
212
+ assert result == expected
@@ -1,104 +0,0 @@
1
- import llm
2
- import nest_asyncio
3
- import json
4
- import os
5
- import pytest
6
- import pydantic
7
-
8
- nest_asyncio.apply()
9
-
10
- GEMINI_API_KEY = os.environ.get("PYTEST_GEMINI_API_KEY", None) or "gm-..."
11
-
12
-
13
- @pytest.mark.vcr
14
- @pytest.mark.asyncio
15
- async def test_prompt():
16
- model = llm.get_model("gemini-1.5-flash-latest")
17
- response = model.prompt("Name for a pet pelican, just the name", key=GEMINI_API_KEY)
18
- assert str(response) == "Percy\n"
19
- assert response.response_json == {
20
- "candidates": [
21
- {
22
- "finishReason": "STOP",
23
- "safetyRatings": [
24
- {
25
- "category": "HARM_CATEGORY_HATE_SPEECH",
26
- "probability": "NEGLIGIBLE",
27
- },
28
- {
29
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
30
- "probability": "NEGLIGIBLE",
31
- },
32
- {
33
- "category": "HARM_CATEGORY_HARASSMENT",
34
- "probability": "NEGLIGIBLE",
35
- },
36
- {
37
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
38
- "probability": "NEGLIGIBLE",
39
- },
40
- ],
41
- }
42
- ],
43
- "modelVersion": "gemini-1.5-flash-latest",
44
- }
45
- assert response.token_details == {
46
- "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 9}],
47
- "candidatesTokensDetails": [{"modality": "TEXT", "tokenCount": 2}],
48
- }
49
- assert response.input_tokens == 9
50
- assert response.output_tokens == 2
51
-
52
- # And try it async too
53
- async_model = llm.get_async_model("gemini-1.5-flash-latest")
54
- response = await async_model.prompt(
55
- "Name for a pet pelican, just the name", key=GEMINI_API_KEY
56
- )
57
- text = await response.text()
58
- assert text == "Percy\n"
59
-
60
-
61
- @pytest.mark.vcr
62
- @pytest.mark.asyncio
63
- async def test_prompt_with_pydantic_schema():
64
- class Dog(pydantic.BaseModel):
65
- name: str
66
- age: int
67
- bio: str
68
-
69
- model = llm.get_model("gemini-1.5-flash-latest")
70
- response = model.prompt(
71
- "Invent a cool dog", key=GEMINI_API_KEY, schema=Dog, stream=False
72
- )
73
- assert json.loads(response.text()) == {
74
- "age": 3,
75
- "bio": "A fluffy Samoyed with exceptional intelligence and a love for belly rubs. He's mastered several tricks, including fetching the newspaper and opening doors.",
76
- "name": "Cloud",
77
- }
78
- assert response.response_json == {
79
- "candidates": [
80
- {
81
- "finishReason": "STOP",
82
- "safetyRatings": [
83
- {
84
- "category": "HARM_CATEGORY_HATE_SPEECH",
85
- "probability": "NEGLIGIBLE",
86
- },
87
- {
88
- "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
89
- "probability": "NEGLIGIBLE",
90
- },
91
- {
92
- "category": "HARM_CATEGORY_HARASSMENT",
93
- "probability": "NEGLIGIBLE",
94
- },
95
- {
96
- "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
97
- "probability": "NEGLIGIBLE",
98
- },
99
- ],
100
- }
101
- ],
102
- "modelVersion": "gemini-1.5-flash-latest",
103
- }
104
- assert response.input_tokens == 10
File without changes
File without changes