cortexgrid-infer 0.3.1__tar.gz → 0.3.3__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.
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/PKG-INFO +46 -5
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/README.md +44 -4
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/__init__.py +21 -6
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/core.py +19 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/importers/base.py +1 -1
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/importers/huggingface.py +1 -1
- cortexgrid_infer-0.3.3/cortexgrid_infer/protocols/__init__.py +14 -0
- {cortexgrid_infer-0.3.1/cortexgrid_infer → cortexgrid_infer-0.3.3/cortexgrid_infer/protocols}/imaging.py +1 -1
- {cortexgrid_infer-0.3.1/cortexgrid_infer → cortexgrid_infer-0.3.3/cortexgrid_infer/protocols}/meshing.py +3 -3
- cortexgrid_infer-0.3.3/cortexgrid_infer/protocols/rewriting.py +44 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/registry.py +1 -1
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/__init__.py +23 -0
- {cortexgrid_infer-0.3.1/cortexgrid_infer/models → cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps}/anthropic.py +2 -2
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/gemini/__init__.py +11 -0
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/gemini/base.py +50 -0
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/gemini/text2image.py +116 -0
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/gemini/text2text.py +146 -0
- {cortexgrid_infer-0.3.1/cortexgrid_infer/models → cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps}/image2mesh.py +5 -5
- {cortexgrid_infer-0.3.1/cortexgrid_infer/models → cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps}/text2image.py +3 -3
- {cortexgrid_infer-0.3.1/cortexgrid_infer/models → cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps}/text2text.py +2 -2
- cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps/text_rewriter.py +54 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/pyproject.toml +5 -2
- cortexgrid_infer-0.3.1/cortexgrid_infer/models/__init__.py +0 -18
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/.gitignore +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/LICENSE +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/compiling.py +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/device.py +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/importers/__init__.py +0 -0
- {cortexgrid_infer-0.3.1/cortexgrid_infer → cortexgrid_infer-0.3.3/cortexgrid_infer/protocols}/completion.py +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/py.typed +0 -0
- {cortexgrid_infer-0.3.1/cortexgrid_infer/models → cortexgrid_infer-0.3.3/cortexgrid_infer/serve_apps}/base.py +0 -0
- {cortexgrid_infer-0.3.1 → cortexgrid_infer-0.3.3}/cortexgrid_infer/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortexgrid-infer
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid
|
|
5
5
|
Project-URL: Homepage, https://github.com/robodatalab/cortexgrid-infer
|
|
6
6
|
Project-URL: Repository, https://github.com/robodatalab/cortexgrid-infer
|
|
@@ -12,6 +12,7 @@ Requires-Dist: anthropic>=0.40.0
|
|
|
12
12
|
Requires-Dist: cortexgrid>=0.3.4
|
|
13
13
|
Requires-Dist: diffusers>=0.38
|
|
14
14
|
Requires-Dist: fastapi>=0.133
|
|
15
|
+
Requires-Dist: google-genai>=1.0
|
|
15
16
|
Requires-Dist: httpx>=0.27.0
|
|
16
17
|
Requires-Dist: huggingface-hub>=0.20.0
|
|
17
18
|
Requires-Dist: numpy>=1.24
|
|
@@ -48,10 +49,10 @@ Upgrading from 0.2.x? See [MIGRATION.md](https://github.com/robodatalab/cortexgr
|
|
|
48
49
|
|
|
49
50
|
A model is two things, and the library keeps them apart:
|
|
50
51
|
|
|
51
|
-
- **What runs it** — a serve app in `cortexgrid_infer.
|
|
52
|
-
runs, not for where the model came from. It loads the weights, answers the task's
|
|
53
|
-
routes, and names the client that speaks
|
|
54
|
-
its task, whichever source staged it.
|
|
52
|
+
- **What runs it** — a serve app in `cortexgrid_infer.serve_apps`, named for the task
|
|
53
|
+
it runs, not for where the model came from. It loads the weights, answers the task's
|
|
54
|
+
routes (defined in `cortexgrid_infer.protocols`), and names the client that speaks
|
|
55
|
+
them. One serve app runs every model of its task, whichever source staged it.
|
|
55
56
|
- **Where its weights come from** — an importer in `cortexgrid_infer.importers`, one
|
|
56
57
|
per source. It names the model in the registry, downloads the weights, and sizes
|
|
57
58
|
them from the source's metadata. It is handed the serve app, so one importer takes
|
|
@@ -62,7 +63,10 @@ A model is two things, and the library keeps them apart:
|
|
|
62
63
|
| `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
|
|
63
64
|
| `Text2Image` | text (and image) to image | any diffusers pipeline | `ServedGeneratingModel` (`generate`) |
|
|
64
65
|
| `Image2Mesh` — a base: subclass it with the model's `load` and `make_mesh` | image to mesh | the model the subclass loads, behind the task's `POST /mesh` | `ServedMeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
|
|
66
|
+
| `TextRewriter` | text to text, rewritten | any encoder-decoder LM (T5, BART, Marian, ...) in the transformers layout, via `AutoModelForSeq2SeqLM` | `ServedRewritingModel` (`rewrite`: one text in, one text out) |
|
|
65
67
|
| `AnthropicText2Text` | text to text | the Anthropic API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
68
|
+
| `GeminiText2Text` | text to text | the Gemini API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
69
|
+
| `GeminiText2Image` | text to image | the Gemini API, forwarded from the cluster | `ServedGeneratingModel` (`generate`) |
|
|
66
70
|
|
|
67
71
|
| Importer | Source |
|
|
68
72
|
|---|---|
|
|
@@ -290,6 +294,42 @@ included. From there it deploys and streams exactly like a cluster-served model:
|
|
|
290
294
|
reports tool calls as structured blocks rather than as generated text, so the
|
|
291
295
|
serve app re-encodes them into the text form the client parses.
|
|
292
296
|
|
|
297
|
+
### Gemini
|
|
298
|
+
|
|
299
|
+
A Gemini model registers exactly like an Anthropic one, with the serve app for
|
|
300
|
+
its task — `GeminiText2Text` or `GeminiText2Image`. The model id passed to
|
|
301
|
+
`Hosted` is the Gemini model name the deployment calls:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
cortexgrid.set_secret("GEMINI_API_KEY", "AIza...") # once
|
|
305
|
+
|
|
306
|
+
entry = mg.Hosted("gemini-2.5-pro", mg.GeminiText2Text) # or "gemini-2.5-flash", ...
|
|
307
|
+
cortexgrid.register_model(
|
|
308
|
+
entry.serve_app,
|
|
309
|
+
family=entry.family, suffix=entry.suffix,
|
|
310
|
+
requirements=entry.requirements(), config=entry.config(),
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Everything said above for Anthropic holds: `config` carries the model name and
|
|
315
|
+
the name of the secret (`api_key_secret="..."` to use another), both editable on
|
|
316
|
+
the model card; a replica needs no hardware; and `entry.client(deployment.url)`
|
|
317
|
+
is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
|
|
318
|
+
text form the client parses.
|
|
319
|
+
|
|
320
|
+
`GeminiText2Image` answers the same `/generate` route as `Text2Image`, so its
|
|
321
|
+
client is a `ServedGeneratingModel`:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
|
|
325
|
+
...
|
|
326
|
+
picture = await mg.generate(entry.client(deployment.url), "a fox in the snow", size=2048)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
It takes a prompt only, and makes a square picture. `size` picks Gemini's size
|
|
330
|
+
tier (up to 1K, 2K or 4K) and `seed` is passed on; `steps` and `guidance` are
|
|
331
|
+
diffusion settings with no Gemini counterpart, and come back as `None`.
|
|
332
|
+
|
|
293
333
|
## API
|
|
294
334
|
|
|
295
335
|
| | |
|
|
@@ -300,6 +340,7 @@ serve app re-encodes them into the text form the client parses.
|
|
|
300
340
|
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`, `client(url)`. |
|
|
301
341
|
| `Text2Text`, `Text2Image`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
|
|
302
342
|
| `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
|
|
343
|
+
| `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
|
|
303
344
|
| `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `client(url, name)`. |
|
|
304
345
|
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(url, name)`. |
|
|
305
346
|
| `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
|
|
@@ -21,10 +21,10 @@ Upgrading from 0.2.x? See [MIGRATION.md](https://github.com/robodatalab/cortexgr
|
|
|
21
21
|
|
|
22
22
|
A model is two things, and the library keeps them apart:
|
|
23
23
|
|
|
24
|
-
- **What runs it** — a serve app in `cortexgrid_infer.
|
|
25
|
-
runs, not for where the model came from. It loads the weights, answers the task's
|
|
26
|
-
routes, and names the client that speaks
|
|
27
|
-
its task, whichever source staged it.
|
|
24
|
+
- **What runs it** — a serve app in `cortexgrid_infer.serve_apps`, named for the task
|
|
25
|
+
it runs, not for where the model came from. It loads the weights, answers the task's
|
|
26
|
+
routes (defined in `cortexgrid_infer.protocols`), and names the client that speaks
|
|
27
|
+
them. One serve app runs every model of its task, whichever source staged it.
|
|
28
28
|
- **Where its weights come from** — an importer in `cortexgrid_infer.importers`, one
|
|
29
29
|
per source. It names the model in the registry, downloads the weights, and sizes
|
|
30
30
|
them from the source's metadata. It is handed the serve app, so one importer takes
|
|
@@ -35,7 +35,10 @@ A model is two things, and the library keeps them apart:
|
|
|
35
35
|
| `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
|
|
36
36
|
| `Text2Image` | text (and image) to image | any diffusers pipeline | `ServedGeneratingModel` (`generate`) |
|
|
37
37
|
| `Image2Mesh` — a base: subclass it with the model's `load` and `make_mesh` | image to mesh | the model the subclass loads, behind the task's `POST /mesh` | `ServedMeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
|
|
38
|
+
| `TextRewriter` | text to text, rewritten | any encoder-decoder LM (T5, BART, Marian, ...) in the transformers layout, via `AutoModelForSeq2SeqLM` | `ServedRewritingModel` (`rewrite`: one text in, one text out) |
|
|
38
39
|
| `AnthropicText2Text` | text to text | the Anthropic API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
40
|
+
| `GeminiText2Text` | text to text | the Gemini API, forwarded from the cluster | `ServedCompletingModel` (`complete`) |
|
|
41
|
+
| `GeminiText2Image` | text to image | the Gemini API, forwarded from the cluster | `ServedGeneratingModel` (`generate`) |
|
|
39
42
|
|
|
40
43
|
| Importer | Source |
|
|
41
44
|
|---|---|
|
|
@@ -263,6 +266,42 @@ included. From there it deploys and streams exactly like a cluster-served model:
|
|
|
263
266
|
reports tool calls as structured blocks rather than as generated text, so the
|
|
264
267
|
serve app re-encodes them into the text form the client parses.
|
|
265
268
|
|
|
269
|
+
### Gemini
|
|
270
|
+
|
|
271
|
+
A Gemini model registers exactly like an Anthropic one, with the serve app for
|
|
272
|
+
its task — `GeminiText2Text` or `GeminiText2Image`. The model id passed to
|
|
273
|
+
`Hosted` is the Gemini model name the deployment calls:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
cortexgrid.set_secret("GEMINI_API_KEY", "AIza...") # once
|
|
277
|
+
|
|
278
|
+
entry = mg.Hosted("gemini-2.5-pro", mg.GeminiText2Text) # or "gemini-2.5-flash", ...
|
|
279
|
+
cortexgrid.register_model(
|
|
280
|
+
entry.serve_app,
|
|
281
|
+
family=entry.family, suffix=entry.suffix,
|
|
282
|
+
requirements=entry.requirements(), config=entry.config(),
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Everything said above for Anthropic holds: `config` carries the model name and
|
|
287
|
+
the name of the secret (`api_key_secret="..."` to use another), both editable on
|
|
288
|
+
the model card; a replica needs no hardware; and `entry.client(deployment.url)`
|
|
289
|
+
is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
|
|
290
|
+
text form the client parses.
|
|
291
|
+
|
|
292
|
+
`GeminiText2Image` answers the same `/generate` route as `Text2Image`, so its
|
|
293
|
+
client is a `ServedGeneratingModel`:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
|
|
297
|
+
...
|
|
298
|
+
picture = await mg.generate(entry.client(deployment.url), "a fox in the snow", size=2048)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
It takes a prompt only, and makes a square picture. `size` picks Gemini's size
|
|
302
|
+
tier (up to 1K, 2K or 4K) and `seed` is passed on; `steps` and `guidance` are
|
|
303
|
+
diffusion settings with no Gemini counterpart, and come back as `None`.
|
|
304
|
+
|
|
266
305
|
## API
|
|
267
306
|
|
|
268
307
|
| | |
|
|
@@ -273,6 +312,7 @@ serve app re-encodes them into the text form the client parses.
|
|
|
273
312
|
| `ModelEntry` | Base of both: `model_id`, `family`, `suffix`, `serve_app`, `requirements()`, `config()`, `client(url)`. |
|
|
274
313
|
| `Text2Text`, `Text2Image`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
|
|
275
314
|
| `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
|
|
315
|
+
| `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
|
|
276
316
|
| `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `client(url, name)`. |
|
|
277
317
|
| `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(url, name)`. |
|
|
278
318
|
| `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
|
|
@@ -7,8 +7,9 @@ nothing else, split along the two things a model is:
|
|
|
7
7
|
|
|
8
8
|
- where its weights come from - an importer per source (`importers`), which
|
|
9
9
|
names, fetches and sizes them;
|
|
10
|
-
- what runs it - a serve app per task (`
|
|
11
|
-
answers the task's routes, and names the client that speaks
|
|
10
|
+
- what runs it - a serve app per task (`serve_apps`), which loads the weights,
|
|
11
|
+
answers the task's routes (`protocols`), and names the client that speaks
|
|
12
|
+
them.
|
|
12
13
|
|
|
13
14
|
imp = cortexgrid_infer.HuggingFaceImporter("Qwen/Qwen2.5-0.5B-Instruct", cortexgrid_infer.Text2Text)
|
|
14
15
|
with imp:
|
|
@@ -37,24 +38,32 @@ from cortexgrid_infer.core import (
|
|
|
37
38
|
GeneratingModel,
|
|
38
39
|
MeshingModel,
|
|
39
40
|
Message,
|
|
41
|
+
RewritingModel,
|
|
40
42
|
Tool,
|
|
41
43
|
ToolCall,
|
|
42
44
|
ToolSpec,
|
|
43
45
|
complete,
|
|
44
46
|
generate,
|
|
45
47
|
mesh,
|
|
48
|
+
rewrite,
|
|
49
|
+
)
|
|
50
|
+
from cortexgrid_infer.protocols import (
|
|
51
|
+
ServedCompletingModel,
|
|
52
|
+
ServedGeneratingModel,
|
|
53
|
+
ServedMeshingModel,
|
|
54
|
+
ServedRewritingModel,
|
|
46
55
|
)
|
|
47
|
-
from cortexgrid_infer.completion import ServedCompletingModel
|
|
48
|
-
from cortexgrid_infer.imaging import ServedGeneratingModel
|
|
49
|
-
from cortexgrid_infer.meshing import ServedMeshingModel
|
|
50
56
|
from cortexgrid_infer.device import detect_device
|
|
51
|
-
from cortexgrid_infer.
|
|
57
|
+
from cortexgrid_infer.serve_apps import (
|
|
52
58
|
AnthropicText2Text,
|
|
59
|
+
GeminiText2Image,
|
|
60
|
+
GeminiText2Text,
|
|
53
61
|
HostedModel,
|
|
54
62
|
Image2Mesh,
|
|
55
63
|
LocalModel,
|
|
56
64
|
Text2Image,
|
|
57
65
|
Text2Text,
|
|
66
|
+
TextRewriter,
|
|
58
67
|
Weights,
|
|
59
68
|
)
|
|
60
69
|
from cortexgrid_infer.registry import Hosted, ModelEntry, split_model_id
|
|
@@ -68,6 +77,7 @@ __all__ = [
|
|
|
68
77
|
"GeneratedMesh",
|
|
69
78
|
"GeneratingModel",
|
|
70
79
|
"MeshingModel",
|
|
80
|
+
"RewritingModel",
|
|
71
81
|
"Message",
|
|
72
82
|
"Tool",
|
|
73
83
|
"ToolCall",
|
|
@@ -75,18 +85,23 @@ __all__ = [
|
|
|
75
85
|
"complete",
|
|
76
86
|
"generate",
|
|
77
87
|
"mesh",
|
|
88
|
+
"rewrite",
|
|
78
89
|
"ModelDeployFailed",
|
|
79
90
|
"detect_device",
|
|
80
91
|
"ServedCompletingModel",
|
|
81
92
|
"ServedGeneratingModel",
|
|
82
93
|
"ServedMeshingModel",
|
|
94
|
+
"ServedRewritingModel",
|
|
83
95
|
"LocalModel",
|
|
84
96
|
"HostedModel",
|
|
85
97
|
"Weights",
|
|
86
98
|
"Text2Text",
|
|
87
99
|
"Text2Image",
|
|
88
100
|
"Image2Mesh",
|
|
101
|
+
"TextRewriter",
|
|
89
102
|
"AnthropicText2Text",
|
|
103
|
+
"GeminiText2Text",
|
|
104
|
+
"GeminiText2Image",
|
|
90
105
|
"ModelEntry",
|
|
91
106
|
"Hosted",
|
|
92
107
|
"split_model_id",
|
|
@@ -109,6 +109,15 @@ class MeshingModel(DeployedModel):
|
|
|
109
109
|
async def mesh(self, image: bytes, **kwargs: Any) -> GeneratedMesh: ...
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
class RewritingModel(DeployedModel):
|
|
113
|
+
"""A deployed model that rewrites one text into another - corrects,
|
|
114
|
+
paraphrases, summarises or translates it. Like generation, a single
|
|
115
|
+
request/response."""
|
|
116
|
+
|
|
117
|
+
@abc.abstractmethod
|
|
118
|
+
async def rewrite(self, text: str, max_new_tokens: int = 512, **kwargs: Any) -> str: ...
|
|
119
|
+
|
|
120
|
+
|
|
112
121
|
async def complete(
|
|
113
122
|
deployed_model: CompletingModel,
|
|
114
123
|
messages: list[Message],
|
|
@@ -142,3 +151,13 @@ async def generate(
|
|
|
142
151
|
async def mesh(deployed_model: MeshingModel, image: bytes, **kwargs: Any) -> GeneratedMesh:
|
|
143
152
|
"""Single image-to-mesh request to the deployed model."""
|
|
144
153
|
return await deployed_model.mesh(image, **kwargs)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def rewrite(
|
|
157
|
+
deployed_model: RewritingModel,
|
|
158
|
+
text: str,
|
|
159
|
+
max_new_tokens: int = 512,
|
|
160
|
+
**kwargs: Any,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Single rewriting request to the deployed model."""
|
|
163
|
+
return await deployed_model.rewrite(text, max_new_tokens=max_new_tokens, **kwargs)
|
|
@@ -30,8 +30,8 @@ from typing import Self
|
|
|
30
30
|
import cortexgrid
|
|
31
31
|
|
|
32
32
|
from cortexgrid_infer.core import DeployedModel
|
|
33
|
-
from cortexgrid_infer.models.base import LocalModel, Weights
|
|
34
33
|
from cortexgrid_infer.registry import ModelEntry, split_model_id
|
|
34
|
+
from cortexgrid_infer.serve_apps.base import LocalModel, Weights
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class Importer(ModelEntry):
|
|
@@ -15,7 +15,7 @@ from huggingface_hub import HfApi, get_safetensors_metadata, snapshot_download
|
|
|
15
15
|
from huggingface_hub.errors import NotASafetensorsRepoError
|
|
16
16
|
|
|
17
17
|
from cortexgrid_infer.importers.base import Importer
|
|
18
|
-
from cortexgrid_infer.
|
|
18
|
+
from cortexgrid_infer.serve_apps.base import LocalModel, Weights
|
|
19
19
|
|
|
20
20
|
# Files that are weights rather than configs, tokenizers or docs. Used for the
|
|
21
21
|
# fallback estimate, where a repo has no top-level safetensors index.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The wire protocols between a serve app and its client, one per task, each with
|
|
2
|
+
the client that speaks it."""
|
|
3
|
+
|
|
4
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
5
|
+
from cortexgrid_infer.protocols.imaging import ServedGeneratingModel
|
|
6
|
+
from cortexgrid_infer.protocols.meshing import ServedMeshingModel
|
|
7
|
+
from cortexgrid_infer.protocols.rewriting import ServedRewritingModel
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ServedCompletingModel",
|
|
11
|
+
"ServedGeneratingModel",
|
|
12
|
+
"ServedMeshingModel",
|
|
13
|
+
"ServedRewritingModel",
|
|
14
|
+
]
|
|
@@ -7,7 +7,7 @@ as null, take the app's defaults. The reply carries the picture as a base64 PNG
|
|
|
7
7
|
under `image`, its `width` and `height`, the options the app resolved, and
|
|
8
8
|
`duration_s`.
|
|
9
9
|
|
|
10
|
-
`cortexgrid_infer.
|
|
10
|
+
`cortexgrid_infer.serve_apps.text2image.Text2Image` speaks it for any diffusers
|
|
11
11
|
pipeline, so one client, `ServedGeneratingModel`, serves every model of the
|
|
12
12
|
task whoever made it.
|
|
13
13
|
"""
|
|
@@ -7,9 +7,9 @@ the mesh as base64 little-endian arrays - `vertices` float32 x, y, z, `faces`
|
|
|
7
7
|
int32 indices, `colours` uint8 r, g, b per vertex - in the frame
|
|
8
8
|
`GeneratedMesh` documents, and `params`, what the model resolved.
|
|
9
9
|
|
|
10
|
-
Every such app speaks it through
|
|
11
|
-
so one client,
|
|
12
|
-
made it.
|
|
10
|
+
Every such app speaks it through
|
|
11
|
+
`cortexgrid_infer.serve_apps.image2mesh.Image2Mesh`, so one client,
|
|
12
|
+
`ServedMeshingModel`, serves every model of the task whoever made it.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""The wire protocol between a cortexgrid-served rewriting app and its client.
|
|
2
|
+
|
|
3
|
+
A rewriting serve app answers `POST /rewrite`: the request carries the text to
|
|
4
|
+
rewrite under `text`, plus whatever generation options the model takes
|
|
5
|
+
(`max_new_tokens`, `num_beams`, say) as further fields. The reply carries the
|
|
6
|
+
rewritten text under `text`. A request without `max_new_tokens` takes the app's
|
|
7
|
+
default: left to transformers, it would be 20 tokens, cutting all but the
|
|
8
|
+
shortest text.
|
|
9
|
+
|
|
10
|
+
Rewriting is one text in, one text out - correcting grammar, paraphrasing,
|
|
11
|
+
summarising, translating - with no conversation, tools or streaming, which is
|
|
12
|
+
what an encoder-decoder model does. One client, `ServedRewritingModel`, serves
|
|
13
|
+
every model of the task whoever made it.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
from cortexgrid_infer.core import RewritingModel
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ServedRewritingModel(RewritingModel):
|
|
28
|
+
"""The client of any rewriting serve app deployed at `url`."""
|
|
29
|
+
|
|
30
|
+
url: str
|
|
31
|
+
model_id: str
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self.model_id
|
|
36
|
+
|
|
37
|
+
async def rewrite(
|
|
38
|
+
self, text: str, max_new_tokens: int = 512, **generation_options: Any
|
|
39
|
+
) -> str:
|
|
40
|
+
body = {"text": text, "max_new_tokens": max_new_tokens, **generation_options}
|
|
41
|
+
async with httpx.AsyncClient(timeout=None) as client:
|
|
42
|
+
response = await client.post(f"{self.url}/rewrite", json=body)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
return response.json()["text"]
|
|
@@ -28,7 +28,7 @@ import abc
|
|
|
28
28
|
import cortexgrid
|
|
29
29
|
|
|
30
30
|
from cortexgrid_infer.core import DeployedModel
|
|
31
|
-
from cortexgrid_infer.
|
|
31
|
+
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def split_model_id(model_id: str) -> tuple[str, str]:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The serve apps cortexgrid runs, one per task, each working with a model of
|
|
2
|
+
that task whichever source it came from."""
|
|
3
|
+
|
|
4
|
+
from cortexgrid_infer.serve_apps.anthropic import AnthropicText2Text
|
|
5
|
+
from cortexgrid_infer.serve_apps.base import HostedModel, LocalModel, Weights
|
|
6
|
+
from cortexgrid_infer.serve_apps.gemini import GeminiText2Image, GeminiText2Text
|
|
7
|
+
from cortexgrid_infer.serve_apps.image2mesh import Image2Mesh
|
|
8
|
+
from cortexgrid_infer.serve_apps.text2image import Text2Image
|
|
9
|
+
from cortexgrid_infer.serve_apps.text2text import Text2Text
|
|
10
|
+
from cortexgrid_infer.serve_apps.text_rewriter import TextRewriter
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LocalModel",
|
|
14
|
+
"HostedModel",
|
|
15
|
+
"Weights",
|
|
16
|
+
"Text2Text",
|
|
17
|
+
"Text2Image",
|
|
18
|
+
"Image2Mesh",
|
|
19
|
+
"TextRewriter",
|
|
20
|
+
"AnthropicText2Text",
|
|
21
|
+
"GeminiText2Text",
|
|
22
|
+
"GeminiText2Image",
|
|
23
|
+
]
|
|
@@ -22,9 +22,9 @@ from cortexgrid import serve
|
|
|
22
22
|
from fastapi import FastAPI
|
|
23
23
|
from fastapi.responses import StreamingResponse
|
|
24
24
|
|
|
25
|
-
from cortexgrid_infer.completion import ServedCompletingModel, encode_tool_call
|
|
26
25
|
from cortexgrid_infer.core import Message, ToolSpec
|
|
27
|
-
from cortexgrid_infer.
|
|
26
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel, encode_tool_call
|
|
27
|
+
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
28
28
|
|
|
29
29
|
_app = FastAPI()
|
|
30
30
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The serve apps that forward to the Gemini API, one per task."""
|
|
2
|
+
|
|
3
|
+
from cortexgrid_infer.serve_apps.gemini.base import GeminiModel
|
|
4
|
+
from cortexgrid_infer.serve_apps.gemini.text2image import GeminiText2Image
|
|
5
|
+
from cortexgrid_infer.serve_apps.gemini.text2text import GeminiText2Text
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GeminiModel",
|
|
9
|
+
"GeminiText2Text",
|
|
10
|
+
"GeminiText2Image",
|
|
11
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""What every Gemini serve app shares: which model to call, and the key to call
|
|
2
|
+
it with.
|
|
3
|
+
|
|
4
|
+
A Gemini app loads no weights and needs no GPU, so there is nothing to import:
|
|
5
|
+
it is registered with `registry.Hosted`. What it needs instead comes from the
|
|
6
|
+
registry entry's `config`, set when the model was registered and editable on its
|
|
7
|
+
model card afterwards.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import cortexgrid
|
|
13
|
+
from google import genai
|
|
14
|
+
|
|
15
|
+
from cortexgrid_infer.serve_apps.base import HostedModel
|
|
16
|
+
|
|
17
|
+
# Keys the registry entry must carry for the deployment to reach the API.
|
|
18
|
+
MODEL_PARAM = "model"
|
|
19
|
+
API_KEY_SECRET_PARAM = "api_key_secret"
|
|
20
|
+
|
|
21
|
+
# The cortexgrid secret the deployment reads its API key from. It is the secret's
|
|
22
|
+
# name that is stored on the registry entry, never the key itself: entries are
|
|
23
|
+
# readable by anyone who can see the model.
|
|
24
|
+
DEFAULT_API_KEY_SECRET = "GEMINI_API_KEY"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GeminiModel(HostedModel):
|
|
28
|
+
"""Base of the serve apps that forward to the Gemini API, one per task."""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def config(
|
|
32
|
+
cls, model_id: str, api_key_secret: str = DEFAULT_API_KEY_SECRET
|
|
33
|
+
) -> dict[str, str]:
|
|
34
|
+
"""`model_id` is the name Gemini knows the model by, and is what the
|
|
35
|
+
deployment sends upstream; `api_key_secret` names the cortexgrid secret
|
|
36
|
+
holding the key to send it with."""
|
|
37
|
+
return {MODEL_PARAM: model_id, API_KEY_SECRET_PARAM: api_key_secret}
|
|
38
|
+
|
|
39
|
+
def __init__(self, family: str, suffix: str, run_name: str) -> None:
|
|
40
|
+
config = cortexgrid.model_config(family, suffix, run_name)
|
|
41
|
+
missing = {MODEL_PARAM, API_KEY_SECRET_PARAM} - config.keys()
|
|
42
|
+
if missing:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
f"{family}/{suffix}/{run_name} is missing {sorted(missing)} from "
|
|
45
|
+
"its config; set them on the model card"
|
|
46
|
+
)
|
|
47
|
+
self._model = config[MODEL_PARAM]
|
|
48
|
+
self._client = genai.Client(
|
|
49
|
+
api_key=cortexgrid.get_secret(config[API_KEY_SECRET_PARAM])
|
|
50
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""The text-to-image serve app that forwards to the Gemini API.
|
|
2
|
+
|
|
3
|
+
It answers the same `POST /generate` route as `Text2Image` (see
|
|
4
|
+
`cortexgrid_infer.protocols.imaging`), so the same client talks to it. It takes a prompt
|
|
5
|
+
only. Of the sampling options, `size` and `seed` have a Gemini counterpart;
|
|
6
|
+
`steps` and `guidance` are diffusion settings, and are reported back as unused.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import base64
|
|
12
|
+
import io
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from cortexgrid import serve
|
|
17
|
+
from fastapi import FastAPI, HTTPException
|
|
18
|
+
from google.genai import types
|
|
19
|
+
from PIL import Image
|
|
20
|
+
|
|
21
|
+
from cortexgrid_infer.protocols.imaging import ServedGeneratingModel
|
|
22
|
+
from cortexgrid_infer.serve_apps.gemini.base import GeminiModel
|
|
23
|
+
|
|
24
|
+
_app = FastAPI()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def image_size(size: int) -> str | None:
|
|
28
|
+
"""The Gemini size tier that covers `size` pixels a side.
|
|
29
|
+
|
|
30
|
+
None up to 1K, the model's default, so that it is left unsent: not every
|
|
31
|
+
Gemini image model takes a size tier."""
|
|
32
|
+
if size <= 1024:
|
|
33
|
+
return None
|
|
34
|
+
if size <= 2048:
|
|
35
|
+
return "2K"
|
|
36
|
+
return "4K"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def to_png(data: bytes) -> tuple[bytes, int, int]:
|
|
40
|
+
"""`data`, a picture in whatever format Gemini returned it, as PNG bytes
|
|
41
|
+
along with its width and height."""
|
|
42
|
+
image = Image.open(io.BytesIO(data))
|
|
43
|
+
if image.format == "PNG":
|
|
44
|
+
return data, image.width, image.height
|
|
45
|
+
buffer = io.BytesIO()
|
|
46
|
+
image.save(buffer, format="PNG")
|
|
47
|
+
return buffer.getvalue(), image.width, image.height
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def no_image_reason(response: types.GenerateContentResponse) -> str:
|
|
51
|
+
"""Why `response` carries no picture: what the model said instead, or why
|
|
52
|
+
it stopped."""
|
|
53
|
+
feedback = response.prompt_feedback
|
|
54
|
+
if feedback and feedback.block_reason:
|
|
55
|
+
return f"the prompt was blocked ({feedback.block_reason})"
|
|
56
|
+
text = " ".join(part.text for part in response.parts or [] if part.text)
|
|
57
|
+
if text:
|
|
58
|
+
return text
|
|
59
|
+
candidate = response.candidates[0] if response.candidates else None
|
|
60
|
+
return f"finish reason {candidate.finish_reason if candidate else None}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@serve.ingress(_app)
|
|
64
|
+
class GeminiText2Image(GeminiModel):
|
|
65
|
+
@classmethod
|
|
66
|
+
def client(cls, url: str, name: str) -> ServedGeneratingModel:
|
|
67
|
+
return ServedGeneratingModel(url=url, model_id=name)
|
|
68
|
+
|
|
69
|
+
@_app.post("/generate")
|
|
70
|
+
async def generate(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
if body.get("image"):
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=400,
|
|
74
|
+
detail=f"{type(self).__name__} takes a prompt only, not an image",
|
|
75
|
+
)
|
|
76
|
+
size = int(body.get("size") or 1024)
|
|
77
|
+
seed = body.get("seed")
|
|
78
|
+
|
|
79
|
+
# Square, like `Text2Image` without a reference picture.
|
|
80
|
+
image_config: dict[str, Any] = {"aspect_ratio": "1:1"}
|
|
81
|
+
tier = image_size(size)
|
|
82
|
+
if tier:
|
|
83
|
+
image_config["image_size"] = tier
|
|
84
|
+
config: dict[str, Any] = {
|
|
85
|
+
"response_modalities": ["IMAGE"],
|
|
86
|
+
"image_config": image_config,
|
|
87
|
+
}
|
|
88
|
+
if seed is not None:
|
|
89
|
+
config["seed"] = int(seed)
|
|
90
|
+
|
|
91
|
+
t0 = time.time()
|
|
92
|
+
response = await self._client.aio.models.generate_content(
|
|
93
|
+
model=self._model, contents=body["prompt"], config=config
|
|
94
|
+
)
|
|
95
|
+
duration = time.time() - t0
|
|
96
|
+
|
|
97
|
+
data = next(
|
|
98
|
+
(part.inline_data.data for part in response.parts or [] if part.inline_data),
|
|
99
|
+
None,
|
|
100
|
+
)
|
|
101
|
+
if data is None:
|
|
102
|
+
raise HTTPException(
|
|
103
|
+
status_code=502,
|
|
104
|
+
detail=f"{self._model} returned no image: {no_image_reason(response)}",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
png, width, height = to_png(data)
|
|
108
|
+
return {
|
|
109
|
+
"image": base64.b64encode(png).decode("ascii"),
|
|
110
|
+
"width": width,
|
|
111
|
+
"height": height,
|
|
112
|
+
"steps": None,
|
|
113
|
+
"guidance": None,
|
|
114
|
+
"seed": seed,
|
|
115
|
+
"duration_s": round(duration, 2),
|
|
116
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""The text-to-text serve app that forwards to the Gemini API.
|
|
2
|
+
|
|
3
|
+
It speaks the same `/complete` protocol as `Text2Text`, so the same client talks
|
|
4
|
+
to it. Gemini reports tool calls as structured parts rather than as generated
|
|
5
|
+
text, so they are re-encoded on the way out.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
from collections.abc import AsyncIterator
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from cortexgrid import serve
|
|
15
|
+
from fastapi import FastAPI
|
|
16
|
+
from fastapi.responses import StreamingResponse
|
|
17
|
+
|
|
18
|
+
from cortexgrid_infer.core import Message, ToolSpec
|
|
19
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel, encode_tool_call
|
|
20
|
+
from cortexgrid_infer.serve_apps.gemini.base import GeminiModel
|
|
21
|
+
|
|
22
|
+
_app = FastAPI()
|
|
23
|
+
|
|
24
|
+
# Gemini 3 rejects a function call replayed without the thought signature it was
|
|
25
|
+
# returned with, and signatures do not survive the trip through the client as
|
|
26
|
+
# text. Google documents this value as the one that skips the check. The SDK
|
|
27
|
+
# base64-encodes the bytes it is given, so they are decoded here for the value
|
|
28
|
+
# to go out verbatim.
|
|
29
|
+
SKIP_THOUGHT_SIGNATURE = base64.urlsafe_b64decode("skip_thought_signature_validator")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_gemini_contents(
|
|
33
|
+
messages: list[Message],
|
|
34
|
+
) -> tuple[str | None, list[dict[str, Any]]]:
|
|
35
|
+
"""Convert OpenAI-style messages to Gemini contents."""
|
|
36
|
+
system_prompt: str | None = None
|
|
37
|
+
contents: list[dict[str, Any]] = []
|
|
38
|
+
# Gemini answers a function call by name, which a tool message does not
|
|
39
|
+
# carry, so it is looked up from the call the message answers.
|
|
40
|
+
tool_names: dict[str, str] = {}
|
|
41
|
+
|
|
42
|
+
for msg in messages:
|
|
43
|
+
role = msg["role"]
|
|
44
|
+
|
|
45
|
+
if role == "system":
|
|
46
|
+
system_prompt = msg["content"]
|
|
47
|
+
|
|
48
|
+
elif role == "user":
|
|
49
|
+
contents.append({"role": "user", "parts": [{"text": msg["content"]}]})
|
|
50
|
+
|
|
51
|
+
elif role == "assistant":
|
|
52
|
+
parts: list[dict[str, Any]] = []
|
|
53
|
+
if msg.get("content"):
|
|
54
|
+
parts.append({"text": msg["content"]})
|
|
55
|
+
for i, tc in enumerate(msg.get("tool_calls", [])):
|
|
56
|
+
func = tc["function"]
|
|
57
|
+
tool_names[tc["id"]] = func["name"]
|
|
58
|
+
call_part: dict[str, Any] = {
|
|
59
|
+
"function_call": {
|
|
60
|
+
"id": tc["id"],
|
|
61
|
+
"name": func["name"],
|
|
62
|
+
"args": func["arguments"],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
# Gemini signs only the first call of a parallel batch.
|
|
66
|
+
if i == 0:
|
|
67
|
+
call_part["thought_signature"] = SKIP_THOUGHT_SIGNATURE
|
|
68
|
+
parts.append(call_part)
|
|
69
|
+
contents.append({"role": "model", "parts": parts})
|
|
70
|
+
|
|
71
|
+
elif role == "tool":
|
|
72
|
+
response_part = {
|
|
73
|
+
"function_response": {
|
|
74
|
+
"id": msg["tool_call_id"],
|
|
75
|
+
"name": tool_names[msg["tool_call_id"]],
|
|
76
|
+
"response": {"output": msg["content"]},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
contents
|
|
81
|
+
and contents[-1]["role"] == "user"
|
|
82
|
+
and "function_response" in contents[-1]["parts"][0]
|
|
83
|
+
):
|
|
84
|
+
contents[-1]["parts"].append(response_part)
|
|
85
|
+
else:
|
|
86
|
+
contents.append({"role": "user", "parts": [response_part]})
|
|
87
|
+
|
|
88
|
+
return system_prompt, contents
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def to_gemini_tools(
|
|
92
|
+
tool_specs: list[ToolSpec] | None,
|
|
93
|
+
) -> list[dict[str, Any]] | None:
|
|
94
|
+
"""Convert OpenAI-style tool specs to one Gemini tool of function
|
|
95
|
+
declarations."""
|
|
96
|
+
if not tool_specs:
|
|
97
|
+
return None
|
|
98
|
+
declarations = []
|
|
99
|
+
for spec in tool_specs:
|
|
100
|
+
func = spec["function"]
|
|
101
|
+
declaration = {"name": func["name"], "description": func.get("description", "")}
|
|
102
|
+
# Plain JSON Schema, which `parameters` (an OpenAPI subset) is not. A
|
|
103
|
+
# function that takes nothing declares no schema at all.
|
|
104
|
+
if "parameters" in func:
|
|
105
|
+
declaration["parameters_json_schema"] = func["parameters"]
|
|
106
|
+
declarations.append(declaration)
|
|
107
|
+
return [{"function_declarations": declarations}]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@serve.ingress(_app)
|
|
111
|
+
class GeminiText2Text(GeminiModel):
|
|
112
|
+
@classmethod
|
|
113
|
+
def client(cls, url: str, name: str) -> ServedCompletingModel:
|
|
114
|
+
return ServedCompletingModel(url=url, model_id=name)
|
|
115
|
+
|
|
116
|
+
@_app.post("/complete")
|
|
117
|
+
async def complete(self, body: dict[str, Any]) -> StreamingResponse:
|
|
118
|
+
system_prompt, contents = to_gemini_contents(body["messages"])
|
|
119
|
+
tools = to_gemini_tools(body.get("tools"))
|
|
120
|
+
|
|
121
|
+
generate_config: dict[str, Any] = {
|
|
122
|
+
"max_output_tokens": body.get("max_new_tokens", 16 * 1024),
|
|
123
|
+
"temperature": body.get("temperature", 0.7),
|
|
124
|
+
}
|
|
125
|
+
if system_prompt:
|
|
126
|
+
generate_config["system_instruction"] = system_prompt
|
|
127
|
+
if tools:
|
|
128
|
+
generate_config["tools"] = tools
|
|
129
|
+
|
|
130
|
+
async def stream() -> AsyncIterator[bytes]:
|
|
131
|
+
chunks = await self._client.aio.models.generate_content_stream(
|
|
132
|
+
model=self._model, contents=contents, config=generate_config
|
|
133
|
+
)
|
|
134
|
+
async for chunk in chunks:
|
|
135
|
+
if not chunk.candidates or not chunk.candidates[0].content:
|
|
136
|
+
continue
|
|
137
|
+
for part in chunk.candidates[0].content.parts or []:
|
|
138
|
+
if part.function_call:
|
|
139
|
+
yield encode_tool_call(
|
|
140
|
+
part.function_call.name, dict(part.function_call.args or {})
|
|
141
|
+
).encode("utf-8")
|
|
142
|
+
|
|
143
|
+
elif part.text and not part.thought:
|
|
144
|
+
yield part.text.encode("utf-8")
|
|
145
|
+
|
|
146
|
+
return StreamingResponse(stream(), media_type="text/plain")
|
|
@@ -6,8 +6,8 @@ There is no one loader for image-to-mesh models the way `AutoModelForCausalLM`
|
|
|
6
6
|
or a diffusers `model_index.json` serves a whole family of repos: each model
|
|
7
7
|
ships its own code. So a model's serve app subclasses `Image2Mesh` and supplies
|
|
8
8
|
`load` and `make_mesh`; the subclass inherits the route, whose protocol is
|
|
9
|
-
`cortexgrid_infer.meshing`'s, and the client that speaks it.
|
|
10
|
-
the subclass's own file, so the model's code travels with it.
|
|
9
|
+
`cortexgrid_infer.protocols.meshing`'s, and the client that speaks it.
|
|
10
|
+
cortexgrid bundles the subclass's own file, so the model's code travels with it.
|
|
11
11
|
|
|
12
12
|
class MyMesh(Image2Mesh):
|
|
13
13
|
min_vram_gb = 6.0
|
|
@@ -31,11 +31,11 @@ from fastapi import FastAPI
|
|
|
31
31
|
from PIL import Image, ImageOps
|
|
32
32
|
import torch
|
|
33
33
|
|
|
34
|
-
from cortexgrid_infer import meshing
|
|
35
34
|
from cortexgrid_infer.core import GeneratedMesh
|
|
36
35
|
from cortexgrid_infer.device import detect_device
|
|
37
|
-
from cortexgrid_infer.
|
|
38
|
-
from cortexgrid_infer.
|
|
36
|
+
from cortexgrid_infer.protocols import meshing
|
|
37
|
+
from cortexgrid_infer.protocols.meshing import ServedMeshingModel
|
|
38
|
+
from cortexgrid_infer.serve_apps.base import LocalModel
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
_app = FastAPI()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""The text-to-image serve app: a diffusers pipeline loaded from the cortexgrid
|
|
2
|
-
registry, answering `POST /generate` (see `cortexgrid_infer.imaging`).
|
|
2
|
+
registry, answering `POST /generate` (see `cortexgrid_infer.protocols.imaging`).
|
|
3
3
|
|
|
4
4
|
The pipeline class is not hardcoded: it is read from the model's
|
|
5
5
|
``model_index.json`` (``_class_name``) and resolved against ``diffusers``, so
|
|
@@ -28,8 +28,8 @@ import torch
|
|
|
28
28
|
|
|
29
29
|
from cortexgrid_infer import compiling
|
|
30
30
|
from cortexgrid_infer.device import detect_device
|
|
31
|
-
from cortexgrid_infer.imaging import ServedGeneratingModel
|
|
32
|
-
from cortexgrid_infer.
|
|
31
|
+
from cortexgrid_infer.protocols.imaging import ServedGeneratingModel
|
|
32
|
+
from cortexgrid_infer.serve_apps.base import LocalModel
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
_app = FastAPI()
|
|
@@ -28,9 +28,9 @@ from transformers import (
|
|
|
28
28
|
import torch
|
|
29
29
|
|
|
30
30
|
from cortexgrid_infer import compiling
|
|
31
|
-
from cortexgrid_infer.completion import ServedCompletingModel
|
|
32
31
|
from cortexgrid_infer.device import detect_device
|
|
33
|
-
from cortexgrid_infer.
|
|
32
|
+
from cortexgrid_infer.protocols.completion import ServedCompletingModel
|
|
33
|
+
from cortexgrid_infer.serve_apps.base import LocalModel
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
log = logging.getLogger(__name__)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""The text-rewriting serve app: an encoder-decoder LM loaded from the cortexgrid
|
|
2
|
+
registry, answering `POST /rewrite` (see `cortexgrid_infer.protocols.rewriting`).
|
|
3
|
+
|
|
4
|
+
Loads with `AutoModelForSeq2SeqLM`, so it runs any encoder-decoder model (T5,
|
|
5
|
+
BART, Marian, ...) whose weights are in the transformers layout, whichever
|
|
6
|
+
importer staged them. Any task prefix the model expects (`gec: ` for grammar
|
|
7
|
+
correction, say) is the caller's to put in the text.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import cortexgrid
|
|
15
|
+
from cortexgrid import serve
|
|
16
|
+
from fastapi import FastAPI
|
|
17
|
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
from cortexgrid_infer.device import detect_device
|
|
21
|
+
from cortexgrid_infer.protocols.rewriting import ServedRewritingModel
|
|
22
|
+
from cortexgrid_infer.serve_apps.base import LocalModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_app = FastAPI()
|
|
26
|
+
|
|
27
|
+
# Output budget for a request that names none: T5's training context, and
|
|
28
|
+
# generation stops at the end-of-sequence token well before it for most text.
|
|
29
|
+
DEFAULT_MAX_NEW_TOKENS = 512
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@serve.ingress(_app)
|
|
33
|
+
class TextRewriter(LocalModel):
|
|
34
|
+
@classmethod
|
|
35
|
+
def client(cls, url: str, name: str) -> ServedRewritingModel:
|
|
36
|
+
return ServedRewritingModel(url=url, model_id=name)
|
|
37
|
+
|
|
38
|
+
def __init__(self, family: str, suffix: str, run_name: str) -> None:
|
|
39
|
+
path = cortexgrid.load_model(family, suffix, run_name)
|
|
40
|
+
self._device = detect_device()
|
|
41
|
+
self._tokenizer = AutoTokenizer.from_pretrained(path)
|
|
42
|
+
self._model = AutoModelForSeq2SeqLM.from_pretrained(
|
|
43
|
+
str(path), torch_dtype=torch.bfloat16
|
|
44
|
+
)
|
|
45
|
+
self._model.to(self._device)
|
|
46
|
+
|
|
47
|
+
@_app.post("/rewrite")
|
|
48
|
+
async def rewrite(self, body: dict[str, Any]) -> dict[str, str]:
|
|
49
|
+
generation_options = {"max_new_tokens": DEFAULT_MAX_NEW_TOKENS} | {
|
|
50
|
+
key: value for key, value in body.items() if key != "text"
|
|
51
|
+
}
|
|
52
|
+
inputs = self._tokenizer(body["text"], return_tensors="pt").to(self._device)
|
|
53
|
+
output = self._model.generate(**inputs, **generation_options)
|
|
54
|
+
return {"text": self._tokenizer.decode(output[0], skip_special_tokens=True)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cortexgrid-infer"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.3"
|
|
4
4
|
description = "Serve apps per task (text-to-text, text-to-image, image-to-mesh) and importers per source (HuggingFace) for running models on cortexgrid"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -22,12 +22,15 @@ dependencies = [
|
|
|
22
22
|
"transformers>=4.47.0",
|
|
23
23
|
"huggingface_hub>=0.20.0",
|
|
24
24
|
"anthropic>=0.40.0",
|
|
25
|
+
# serve_apps.gemini. The unified Google Gen AI SDK, not the deprecated
|
|
26
|
+
# google-generativeai.
|
|
27
|
+
"google-genai>=1.0",
|
|
25
28
|
"python-dotenv>=1.0.0",
|
|
26
29
|
"httpx>=0.27.0",
|
|
27
30
|
# The serve-apps run on the cluster's FastAPI, which Ray 2.58's Serve
|
|
28
31
|
# requires to be >=0.133.
|
|
29
32
|
"fastapi>=0.133",
|
|
30
|
-
# Text-to-image serving (
|
|
33
|
+
# Text-to-image serving (serve_apps.text2image). FLUX.2 pipelines land in
|
|
31
34
|
# diffusers 0.38; the Qwen3 text encoder needs sentencepiece + protobuf.
|
|
32
35
|
"diffusers>=0.38",
|
|
33
36
|
"pillow>=10.0",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"""The serve apps cortexgrid runs, one per task, each working with a model of
|
|
2
|
-
that task whichever source it came from."""
|
|
3
|
-
|
|
4
|
-
from cortexgrid_infer.models.anthropic import AnthropicText2Text
|
|
5
|
-
from cortexgrid_infer.models.base import HostedModel, LocalModel, Weights
|
|
6
|
-
from cortexgrid_infer.models.image2mesh import Image2Mesh
|
|
7
|
-
from cortexgrid_infer.models.text2image import Text2Image
|
|
8
|
-
from cortexgrid_infer.models.text2text import Text2Text
|
|
9
|
-
|
|
10
|
-
__all__ = [
|
|
11
|
-
"LocalModel",
|
|
12
|
-
"HostedModel",
|
|
13
|
-
"Weights",
|
|
14
|
-
"Text2Text",
|
|
15
|
-
"Text2Image",
|
|
16
|
-
"Image2Mesh",
|
|
17
|
-
"AnthropicText2Text",
|
|
18
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|