cortexgrid-infer 0.3.8__tar.gz → 0.3.9__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.
Files changed (32) hide show
  1. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/PKG-INFO +40 -39
  2. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/README.md +37 -37
  3. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/__init__.py +6 -14
  4. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/core.py +72 -55
  5. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/anthropic.py +41 -53
  6. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/base.py +3 -13
  7. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/gemini/text2image.py +32 -29
  8. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/gemini/text2text.py +45 -58
  9. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/image2mesh.py +25 -29
  10. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/streamed_completion.py +27 -40
  11. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/text2image.py +35 -36
  12. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/text2text.py +47 -73
  13. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/text_rewriter.py +19 -20
  14. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/utils.py +13 -24
  15. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/pyproject.toml +3 -2
  16. cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/__init__.py +0 -14
  17. cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/completion.py +0 -83
  18. cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/imaging.py +0 -64
  19. cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/meshing.py +0 -62
  20. cortexgrid_infer-0.3.8/cortexgrid_infer/protocols/rewriting.py +0 -35
  21. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/.gitignore +0 -0
  22. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/LICENSE +0 -0
  23. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/compiling.py +0 -0
  24. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/device.py +0 -0
  25. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/importers/__init__.py +0 -0
  26. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/importers/base.py +0 -0
  27. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/importers/huggingface.py +0 -0
  28. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/py.typed +0 -0
  29. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/registry.py +0 -0
  30. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/__init__.py +0 -0
  31. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/gemini/__init__.py +0 -0
  32. {cortexgrid_infer-0.3.8 → cortexgrid_infer-0.3.9}/cortexgrid_infer/serve_apps/gemini/base.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortexgrid-infer
3
- Version: 0.3.8
3
+ Version: 0.3.9
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
@@ -9,7 +9,7 @@ License-File: LICENSE
9
9
  Requires-Python: <3.12,>=3.11
10
10
  Requires-Dist: accelerate>=0.33
11
11
  Requires-Dist: anthropic>=0.40.0
12
- Requires-Dist: cortexgrid>=0.3.16
12
+ Requires-Dist: cortexgrid>=0.3.18
13
13
  Requires-Dist: diffusers>=0.38
14
14
  Requires-Dist: fastapi>=0.133
15
15
  Requires-Dist: google-genai>=1.0
@@ -23,6 +23,7 @@ Requires-Dist: sentencepiece>=0.2
23
23
  Requires-Dist: torch>=2.0.0
24
24
  Requires-Dist: transformers>=4.47.0
25
25
  Provides-Extra: dev
26
+ Requires-Dist: parameterized>=0.9.0; extra == 'dev'
26
27
  Requires-Dist: pytest>=8.0; extra == 'dev'
27
28
  Description-Content-Type: text/markdown
28
29
 
@@ -50,23 +51,24 @@ Upgrading from 0.2.x? See [MIGRATION.md](https://github.com/robodatalab/cortexgr
50
51
  A model is two things, and the library keeps them apart:
51
52
 
52
53
  - **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.
54
+ it runs, not for where the model came from. It loads the weights and serves the
55
+ task's endpoints, declared by the task's type (`CompletingModel`, say); cortexgrid
56
+ generates the client that calls them. One serve app runs every model of its task,
57
+ whichever source staged it.
56
58
  - **Where its weights come from** — an importer in `cortexgrid_infer.importers`, one
57
59
  per source. It names the model in the registry, downloads the weights, and sizes
58
60
  them from the source's metadata. It is handed the serve app, so one importer takes
59
61
  any model its source holds.
60
62
 
61
- | Serve app | Task | Runs | Client |
63
+ | Serve app | Task | Runs | Task type |
62
64
  |---|---|---|---|
63
- | `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
64
- | `Text2Image` | text (and image) to image | any diffusers pipeline | `ServedGeneratingModel` (`generate`) |
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) |
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`) |
65
+ | `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `CompletingModel` (`complete`), plus its own `loglikelihoods` and `last_hidden_states` |
66
+ | `Text2Image` | text (and image) to image | any diffusers pipeline | `GeneratingModel` (`generate`) |
67
+ | `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 `mesh` endpoint | `MeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
68
+ | `TextRewriter` | text to text, rewritten | any encoder-decoder LM (T5, BART, Marian, ...) in the transformers layout, via `AutoModelForSeq2SeqLM` | `RewritingModel` (`rewrite`: one text in, one text out) |
69
+ | `AnthropicText2Text` | text to text | the Anthropic API, forwarded from the cluster | `CompletingModel` (`complete`) |
70
+ | `GeminiText2Text` | text to text | the Gemini API, forwarded from the cluster | `CompletingModel` (`complete`) |
71
+ | `GeminiText2Image` | text to image | the Gemini API, forwarded from the cluster | `GeneratingModel` (`generate`) |
70
72
 
71
73
  | Importer | Source |
72
74
  |---|---|
@@ -89,8 +91,9 @@ need:
89
91
 
90
92
  One with weights goes through `cortexgrid.import_model`, one without through
91
93
  `cortexgrid.register_model`. Once deployed, the `cortexgrid.Deployment` hands back
92
- the client that speaks the app's routes: `deployment.client()` once the app serves,
93
- or `deployment.client_async()` at once, with `await model.is_ready()` to ask.
94
+ the client that calls the app's endpoints, typed as the serve app and so as its task
95
+ type: `deployment.client()` once the app serves, or `deployment.client_async()` at
96
+ once, with `await model.is_ready()` to ask.
94
97
 
95
98
  ## Quick start
96
99
 
@@ -122,7 +125,7 @@ cortexgrid.remote(
122
125
  deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED, timeout=1800)
123
126
 
124
127
  # 3. Inference — the client blocks until the Ray Serve app is running.
125
- model = deployment.client()
128
+ model: mg.GeneratingModel = deployment.client()
126
129
 
127
130
  async def run():
128
131
  result = await mg.generate(model, "a red bicycle on a beach at sunrise")
@@ -277,10 +280,13 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
277
280
 
278
281
  ## Inference
279
282
 
280
- `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
283
+ `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`).
284
+ `tools` are Python functions or tool specs; a function goes to the model as its spec,
285
+ and each `ToolCall` comes back as data (`.id`, `.name`, `.arguments`) for the caller
286
+ to run:
281
287
 
282
288
  ```python
283
- model = deployment.client()
289
+ model: mg.CompletingModel = deployment.client()
284
290
  messages = [{"role": "user", "content": "Explain RAG in one sentence."}]
285
291
 
286
292
  async for chunk in mg.complete(model, messages, max_new_tokens=512, temperature=0.7):
@@ -321,9 +327,9 @@ a re-deploy rather than a re-registration.
321
327
 
322
328
  A replica asks for no hardware at all, so it is placed on any node, CPU-only
323
329
  included. From there it deploys and streams exactly like a cluster-served model:
324
- `deployment.client()` returns the same `ServedCompletingModel`. Anthropic
330
+ `deployment.client()` is a `CompletingModel` like `Text2Text`'s. Anthropic
325
331
  reports tool calls as structured blocks rather than as generated text, so the
326
- serve app re-encodes them into the text form the client parses.
332
+ serve app turns them into `ToolCall`s as they come.
327
333
 
328
334
  ### Gemini
329
335
 
@@ -345,16 +351,14 @@ cortexgrid.register_model(
345
351
  Everything said above for Anthropic holds: `config` carries the model name and
346
352
  the name of the secret (`api_key_secret="..."` to use another), both editable on
347
353
  the model card; a replica needs no hardware; and `deployment.client()`
348
- is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
349
- text form the client parses.
354
+ is a `CompletingModel`, with Gemini's function calls turned into `ToolCall`s.
350
355
 
351
- `GeminiText2Image` answers the same `/generate` route as `Text2Image`, so its
352
- client is a `ServedGeneratingModel`:
356
+ `GeminiText2Image` is a `GeneratingModel` like `Text2Image`:
353
357
 
354
358
  ```python
355
359
  entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
356
360
  ...
357
- model = deployment.client()
361
+ model: mg.GeneratingModel = deployment.client()
358
362
  picture = await mg.generate(model, "a fox in the snow", size=2048)
359
363
  ```
360
364
 
@@ -373,14 +377,11 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
373
377
  | `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
374
378
  | `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
375
379
  | `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
376
- | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them), `client(deployment)`. |
377
- | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(deployment)`. |
380
+ | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them). |
381
+ | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`. |
378
382
  | `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
379
- | `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
380
- | `generate(model, prompt, *, image=None, **kw) -> GeneratedImage` | One image from a `GeneratingModel`. |
381
- | `ServedCompletingModel(key, url)` | Client for any deployed completion app. |
382
- | `ServedGeneratingModel(key, url)` | Client for any deployed text-to-image app. |
383
- | `ServedMeshingModel(key, url)` | Client for any deployed image-to-mesh app. |
383
+ | `complete(model, messages, tools=None, max_new_tokens=None, temperature=0.7)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
384
+ | `generate(model, prompt, *, image=None, steps=None, guidance=None, size=1024, seed=None) -> GeneratedImage` | One image from a `GeneratingModel`. |
384
385
  | `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
385
386
  | `detect_device()` | The torch device a serve app should load onto. |
386
387
  | `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
@@ -388,19 +389,19 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
388
389
  | `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
389
390
 
390
391
  Types: `CompletingModel`, `GeneratingModel`, `MeshingModel`, `RewritingModel` (each
391
- a `cortexgrid.DeploymentClient`), `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
392
- `ToolSpec`.
392
+ the abstract base of its task's serve apps, declaring the task's endpoints),
393
+ `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
394
+ `ToolSpec`, `Tensor`.
393
395
 
394
396
  Extending it:
395
397
 
396
398
  - **A new source** is one importer: subclass `Importer` with `download(local_dir)`
397
399
  and `weights()`, and every serve app whose weight format it delivers runs its
398
400
  models unchanged.
399
- - **A new hosted API** is one serve app: subclass `HostedModel`, and register it
400
- with `Hosted`. A text-to-text one only has to stream text from `POST /complete`,
401
- inlining tool calls as `<tool_call>{"name": ..., "arguments": {...}}</tool_call>`
402
- (there is an `encode_tool_call` for upstreams that report them structurally) —
403
- then `ServedCompletingModel` is its client, unchanged.
401
+ - **A new hosted API** is one serve app: subclass `HostedModel` and the task's type,
402
+ and register it with `Hosted`. A text-to-text one subclasses `CompletingModel` and
403
+ streams `CompletionChunk`s from its `complete` endpoint, with
404
+ `StreamedCompletionWriter` turning the upstream's text and tool calls into them.
404
405
  - **A model no task's serve app can load** gets its own serve app — an `Image2Mesh`
405
406
  subclass, say, with `load` and `make_mesh`, and `compile` if compiling it pays —
406
407
  and, if its source is unusual too, its own importer.
@@ -22,23 +22,24 @@ Upgrading from 0.2.x? See [MIGRATION.md](https://github.com/robodatalab/cortexgr
22
22
  A model is two things, and the library keeps them apart:
23
23
 
24
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.
25
+ it runs, not for where the model came from. It loads the weights and serves the
26
+ task's endpoints, declared by the task's type (`CompletingModel`, say); cortexgrid
27
+ generates the client that calls them. One serve app runs every model of its task,
28
+ whichever source staged it.
28
29
  - **Where its weights come from** — an importer in `cortexgrid_infer.importers`, one
29
30
  per source. It names the model in the registry, downloads the weights, and sizes
30
31
  them from the source's metadata. It is handed the serve app, so one importer takes
31
32
  any model its source holds.
32
33
 
33
- | Serve app | Task | Runs | Client |
34
+ | Serve app | Task | Runs | Task type |
34
35
  |---|---|---|---|
35
- | `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `ServedCompletingModel` (`complete`) |
36
- | `Text2Image` | text (and image) to image | any diffusers pipeline | `ServedGeneratingModel` (`generate`) |
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) |
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`) |
36
+ | `Text2Text` | text to text | any causal LM in the transformers layout, via `AutoModelForCausalLM` | `CompletingModel` (`complete`), plus its own `loglikelihoods` and `last_hidden_states` |
37
+ | `Text2Image` | text (and image) to image | any diffusers pipeline | `GeneratingModel` (`generate`) |
38
+ | `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 `mesh` endpoint | `MeshingModel` (`mesh`: one picture in, a `GeneratedMesh` out) |
39
+ | `TextRewriter` | text to text, rewritten | any encoder-decoder LM (T5, BART, Marian, ...) in the transformers layout, via `AutoModelForSeq2SeqLM` | `RewritingModel` (`rewrite`: one text in, one text out) |
40
+ | `AnthropicText2Text` | text to text | the Anthropic API, forwarded from the cluster | `CompletingModel` (`complete`) |
41
+ | `GeminiText2Text` | text to text | the Gemini API, forwarded from the cluster | `CompletingModel` (`complete`) |
42
+ | `GeminiText2Image` | text to image | the Gemini API, forwarded from the cluster | `GeneratingModel` (`generate`) |
42
43
 
43
44
  | Importer | Source |
44
45
  |---|---|
@@ -61,8 +62,9 @@ need:
61
62
 
62
63
  One with weights goes through `cortexgrid.import_model`, one without through
63
64
  `cortexgrid.register_model`. Once deployed, the `cortexgrid.Deployment` hands back
64
- the client that speaks the app's routes: `deployment.client()` once the app serves,
65
- or `deployment.client_async()` at once, with `await model.is_ready()` to ask.
65
+ the client that calls the app's endpoints, typed as the serve app and so as its task
66
+ type: `deployment.client()` once the app serves, or `deployment.client_async()` at
67
+ once, with `await model.is_ready()` to ask.
66
68
 
67
69
  ## Quick start
68
70
 
@@ -94,7 +96,7 @@ cortexgrid.remote(
94
96
  deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED, timeout=1800)
95
97
 
96
98
  # 3. Inference — the client blocks until the Ray Serve app is running.
97
- model = deployment.client()
99
+ model: mg.GeneratingModel = deployment.client()
98
100
 
99
101
  async def run():
100
102
  result = await mg.generate(model, "a red bicycle on a beach at sunrise")
@@ -249,10 +251,13 @@ it is the floor; only quantizing the weights or running fewer forwards moves it.
249
251
 
250
252
  ## Inference
251
253
 
252
- `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`):
254
+ `complete` streams `CompletionChunk`s (`.content`, `.thinking`, `.tool_calls`, `.finish_reason`).
255
+ `tools` are Python functions or tool specs; a function goes to the model as its spec,
256
+ and each `ToolCall` comes back as data (`.id`, `.name`, `.arguments`) for the caller
257
+ to run:
253
258
 
254
259
  ```python
255
- model = deployment.client()
260
+ model: mg.CompletingModel = deployment.client()
256
261
  messages = [{"role": "user", "content": "Explain RAG in one sentence."}]
257
262
 
258
263
  async for chunk in mg.complete(model, messages, max_new_tokens=512, temperature=0.7):
@@ -293,9 +298,9 @@ a re-deploy rather than a re-registration.
293
298
 
294
299
  A replica asks for no hardware at all, so it is placed on any node, CPU-only
295
300
  included. From there it deploys and streams exactly like a cluster-served model:
296
- `deployment.client()` returns the same `ServedCompletingModel`. Anthropic
301
+ `deployment.client()` is a `CompletingModel` like `Text2Text`'s. Anthropic
297
302
  reports tool calls as structured blocks rather than as generated text, so the
298
- serve app re-encodes them into the text form the client parses.
303
+ serve app turns them into `ToolCall`s as they come.
299
304
 
300
305
  ### Gemini
301
306
 
@@ -317,16 +322,14 @@ cortexgrid.register_model(
317
322
  Everything said above for Anthropic holds: `config` carries the model name and
318
323
  the name of the secret (`api_key_secret="..."` to use another), both editable on
319
324
  the model card; a replica needs no hardware; and `deployment.client()`
320
- is a `ServedCompletingModel`, with Gemini's function calls re-encoded into the
321
- text form the client parses.
325
+ is a `CompletingModel`, with Gemini's function calls turned into `ToolCall`s.
322
326
 
323
- `GeminiText2Image` answers the same `/generate` route as `Text2Image`, so its
324
- client is a `ServedGeneratingModel`:
327
+ `GeminiText2Image` is a `GeneratingModel` like `Text2Image`:
325
328
 
326
329
  ```python
327
330
  entry = mg.Hosted("gemini-2.5-flash-image", mg.GeminiText2Image)
328
331
  ...
329
- model = deployment.client()
332
+ model: mg.GeneratingModel = deployment.client()
330
333
  picture = await mg.generate(model, "a fox in the snow", size=2048)
331
334
  ```
332
335
 
@@ -345,14 +348,11 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
345
348
  | `Text2Text`, `Text2Image`, `TextRewriter`, `Image2Mesh` | Serve apps that run weights (`LocalModel`s). |
346
349
  | `AnthropicText2Text` | Serve app that forwards to the Anthropic API (a `HostedModel`). |
347
350
  | `GeminiText2Text`, `GeminiText2Image` | Serve apps that forward to the Gemini API (`HostedModel`s sharing the `GeminiModel` base). |
348
- | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them), `client(deployment)`. |
349
- | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`, `client(deployment)`. |
351
+ | `LocalModel` | Base of the serve apps that run weights: `ignore_patterns()`, `bytes_per_param`, `min_vram_gb`, `requirements(weights)`, `config()` (the model card's defaults, `compile: "false"` among them). |
352
+ | `HostedModel` | Base of the serve apps that forward: `config(model_id, **settings)`, `requirements()`. |
350
353
  | `Weights(params=None, file_bytes=0)` | What an importer reads about the weights, handed to the serve app to size a replica. |
351
- | `complete(model, messages, tools=None, max_new_tokens=2048, temperature=0.7, **kw)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
352
- | `generate(model, prompt, *, image=None, **kw) -> GeneratedImage` | One image from a `GeneratingModel`. |
353
- | `ServedCompletingModel(key, url)` | Client for any deployed completion app. |
354
- | `ServedGeneratingModel(key, url)` | Client for any deployed text-to-image app. |
355
- | `ServedMeshingModel(key, url)` | Client for any deployed image-to-mesh app. |
354
+ | `complete(model, messages, tools=None, max_new_tokens=None, temperature=0.7)` | Async stream of `CompletionChunk` for a `CompletingModel`. |
355
+ | `generate(model, prompt, *, image=None, steps=None, guidance=None, size=1024, seed=None) -> GeneratedImage` | One image from a `GeneratingModel`. |
356
356
  | `split_model_id(model_id) -> (family, suffix)` | The registry identity an importer derives. |
357
357
  | `detect_device()` | The torch device a serve app should load onto. |
358
358
  | `compiling.Mode.GRAPHED` / `compiling.Mode.FUSED` | The two modes a serve app chooses between. See [Compilation](#compilation). |
@@ -360,19 +360,19 @@ diffusion settings with no Gemini counterpart, and come back as `None`.
360
360
  | `ModelDeployFailed` | Re-exported from cortexgrid; subclasses `RuntimeError`. |
361
361
 
362
362
  Types: `CompletingModel`, `GeneratingModel`, `MeshingModel`, `RewritingModel` (each
363
- a `cortexgrid.DeploymentClient`), `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
364
- `ToolSpec`.
363
+ the abstract base of its task's serve apps, declaring the task's endpoints),
364
+ `CompletionChunk`, `GeneratedImage`, `GeneratedMesh`, `ToolCall`, `Message`, `Tool`,
365
+ `ToolSpec`, `Tensor`.
365
366
 
366
367
  Extending it:
367
368
 
368
369
  - **A new source** is one importer: subclass `Importer` with `download(local_dir)`
369
370
  and `weights()`, and every serve app whose weight format it delivers runs its
370
371
  models unchanged.
371
- - **A new hosted API** is one serve app: subclass `HostedModel`, and register it
372
- with `Hosted`. A text-to-text one only has to stream text from `POST /complete`,
373
- inlining tool calls as `<tool_call>{"name": ..., "arguments": {...}}</tool_call>`
374
- (there is an `encode_tool_call` for upstreams that report them structurally) —
375
- then `ServedCompletingModel` is its client, unchanged.
372
+ - **A new hosted API** is one serve app: subclass `HostedModel` and the task's type,
373
+ and register it with `Hosted`. A text-to-text one subclasses `CompletingModel` and
374
+ streams `CompletionChunk`s from its `complete` endpoint, with
375
+ `StreamedCompletionWriter` turning the upstream's text and tool calls into them.
376
376
  - **A model no task's serve app can load** gets its own serve app — an `Image2Mesh`
377
377
  subclass, say, with `load` and `make_mesh`, and `compile` if compiling it pays —
378
378
  and, if its source is unusual too, its own importer.
@@ -7,9 +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 (`serve_apps`), which loads the weights,
11
- answers the task's routes (`protocols`), and names the client that speaks
12
- them.
10
+ - what runs it - a serve app per task (`serve_apps`), which loads the weights
11
+ and serves the task's endpoints, declared by the task's type in `core`
12
+ (`CompletingModel`, say); cortexgrid generates the client that calls them.
13
13
 
14
14
  imp = cortexgrid_infer.HuggingFaceImporter("Qwen/Qwen2.5-0.5B-Instruct", cortexgrid_infer.Text2Text)
15
15
  with imp:
@@ -19,7 +19,7 @@ nothing else, split along the two things a model is:
19
19
  requirements=imp.requirements(),
20
20
  )
21
21
  deployment = cortexgrid.deploy_model(imp.family, imp.suffix, cortexgrid.IMPORTED)
22
- model = deployment.client()
22
+ model: cortexgrid_infer.CompletingModel = deployment.client()
23
23
 
24
24
  A model hosted elsewhere has no weights and so no importer: its serve app
25
25
  forwards to it, and a `Hosted` entry registers it.
@@ -38,6 +38,7 @@ from cortexgrid_infer.core import (
38
38
  MeshingModel,
39
39
  Message,
40
40
  RewritingModel,
41
+ Tensor,
41
42
  Tool,
42
43
  ToolCall,
43
44
  ToolSpec,
@@ -46,12 +47,6 @@ from cortexgrid_infer.core import (
46
47
  mesh,
47
48
  rewrite,
48
49
  )
49
- from cortexgrid_infer.protocols import (
50
- ServedCompletingModel,
51
- ServedGeneratingModel,
52
- ServedMeshingModel,
53
- ServedRewritingModel,
54
- )
55
50
  from cortexgrid_infer.device import detect_device
56
51
  from cortexgrid_infer.serve_apps import (
57
52
  AnthropicText2Text,
@@ -77,6 +72,7 @@ __all__ = [
77
72
  "MeshingModel",
78
73
  "RewritingModel",
79
74
  "Message",
75
+ "Tensor",
80
76
  "Tool",
81
77
  "ToolCall",
82
78
  "ToolSpec",
@@ -86,10 +82,6 @@ __all__ = [
86
82
  "rewrite",
87
83
  "ModelDeployFailed",
88
84
  "detect_device",
89
- "ServedCompletingModel",
90
- "ServedGeneratingModel",
91
- "ServedMeshingModel",
92
- "ServedRewritingModel",
93
85
  "LocalModel",
94
86
  "HostedModel",
95
87
  "Weights",
@@ -3,13 +3,24 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
+ import base64
6
7
  from dataclasses import dataclass, field
7
- from functools import partial
8
8
  from collections.abc import AsyncIterator
9
- from typing import Any, Callable, Sequence
9
+ from functools import partial
10
+ from typing import Annotated, Any
10
11
 
11
- import cortexgrid
12
12
  import numpy as np
13
+ import torch
14
+ from pydantic import Base64Bytes, PlainSerializer, PlainValidator
15
+
16
+ from cortexgrid_infer.utils import Tool, ToolSpec
17
+
18
+
19
+ Tensor = Annotated[
20
+ torch.Tensor,
21
+ PlainValidator(torch.tensor),
22
+ PlainSerializer(torch.Tensor.tolist),
23
+ ]
13
24
 
14
25
 
15
26
  @dataclass
@@ -17,10 +28,6 @@ class ToolCall:
17
28
  id: str
18
29
  name: str
19
30
  arguments: dict[str, Any]
20
- _func: partial[Any] = field(repr=False)
21
-
22
- def __call__(self) -> Any:
23
- return self._func(**self.arguments)
24
31
 
25
32
 
26
33
  @dataclass
@@ -38,42 +45,18 @@ class CompletionChunk:
38
45
  def has_tool_calls(self) -> bool:
39
46
  return bool(self.tool_calls)
40
47
 
41
- @classmethod
42
- def from_wire(
43
- cls, answered: dict[str, Any], tools_by_name: dict[str, Callable[..., Any]]
44
- ) -> CompletionChunk:
45
- tool_calls = []
46
- for called in answered["tool_calls"]:
47
- tool = tools_by_name.get(called["name"])
48
- if tool is None:
49
- continue
50
- bound = partial(tool, **called["arguments"])
51
- tool_call = ToolCall(
52
- id=called["id"], name=called["name"], arguments=called["arguments"], _func=bound
53
- )
54
- tool_calls.append(tool_call)
55
- return cls(
56
- content=answered["content"],
57
- thinking=answered["thinking"],
58
- tool_calls=tool_calls,
59
- finish_reason=answered["finish_reason"],
60
- )
61
-
62
48
 
63
49
  Message = dict[str, Any]
64
- ToolSpec = dict[str, Any]
65
- Tool = Callable[..., Any] | ToolSpec
66
50
 
67
51
 
68
- class CompletingModel(cortexgrid.DeploymentClient, abc.ABC):
52
+ class CompletingModel(abc.ABC):
69
53
  @abc.abstractmethod
70
54
  def complete(
71
55
  self,
72
56
  messages: list[Message],
73
- tools: Sequence[Tool] | None = None,
74
- max_new_tokens: int = 2048,
57
+ tools: list[Tool] | None = None,
58
+ max_new_tokens: int | None = None,
75
59
  temperature: float = 0.7,
76
- **kwargs: Any,
77
60
  ) -> AsyncIterator[CompletionChunk]: ...
78
61
 
79
62
 
@@ -82,13 +65,13 @@ class GeneratedImage:
82
65
  """One generated image: the encoded file bytes (PNG) plus the resolved
83
66
  parameters the deployment used to produce it."""
84
67
 
85
- image: bytes
68
+ image: Base64Bytes
86
69
  width: int
87
70
  height: int
88
71
  params: dict[str, Any] = field(default_factory=dict)
89
72
 
90
73
 
91
- class GeneratingModel(cortexgrid.DeploymentClient, abc.ABC):
74
+ class GeneratingModel(abc.ABC):
92
75
  """A deployed image model. Unlike completion, generation is a single
93
76
  request/response (no token streaming), so `generate` returns one result."""
94
77
 
@@ -97,15 +80,43 @@ class GeneratingModel(cortexgrid.DeploymentClient, abc.ABC):
97
80
  self,
98
81
  prompt: str,
99
82
  *,
100
- image: bytes | None = None,
83
+ image: Base64Bytes | None = None,
101
84
  steps: int | None = None,
102
85
  guidance: float | None = None,
103
86
  size: int = 1024,
104
87
  seed: int | None = None,
105
- **kwargs: Any,
106
88
  ) -> GeneratedImage: ...
107
89
 
108
90
 
91
+ def _encoded_array(array: np.ndarray, dtype: str) -> str:
92
+ contiguous = np.ascontiguousarray(array, dtype=dtype)
93
+ encoded = base64.b64encode(contiguous.tobytes())
94
+ return encoded.decode("ascii")
95
+
96
+
97
+ def _decoded_array(data: str, dtype: str, columns: int) -> np.ndarray:
98
+ raw = base64.b64decode(data)
99
+ flat = np.frombuffer(raw, dtype=dtype)
100
+ return flat.reshape(-1, columns)
101
+
102
+
103
+ Vertices = Annotated[
104
+ np.ndarray,
105
+ PlainValidator(partial(_decoded_array, dtype="<f4", columns=3)),
106
+ PlainSerializer(partial(_encoded_array, dtype="<f4")),
107
+ ]
108
+ Faces = Annotated[
109
+ np.ndarray,
110
+ PlainValidator(partial(_decoded_array, dtype="<i4", columns=3)),
111
+ PlainSerializer(partial(_encoded_array, dtype="<i4")),
112
+ ]
113
+ Colours = Annotated[
114
+ np.ndarray,
115
+ PlainValidator(partial(_decoded_array, dtype="u1", columns=3)),
116
+ PlainSerializer(partial(_encoded_array, dtype="u1")),
117
+ ]
118
+
119
+
109
120
  @dataclass
110
121
  class GeneratedMesh:
111
122
  """One mesh: `vertices` (N, 3) float32, x to the right of the picture it was
@@ -113,35 +124,36 @@ class GeneratedMesh:
113
124
  into them, counter-clockwise seen from outside; `colours` (N, 3) uint8, each
114
125
  vertex's colour. Plus the resolved parameters the deployment used."""
115
126
 
116
- vertices: np.ndarray
117
- faces: np.ndarray
118
- colours: np.ndarray
127
+ vertices: Vertices
128
+ faces: Faces
129
+ colours: Colours
119
130
  params: dict[str, Any] = field(default_factory=dict)
120
131
 
121
132
 
122
- class MeshingModel(cortexgrid.DeploymentClient, abc.ABC):
133
+ class MeshingModel(abc.ABC):
123
134
  """A deployed model that makes a mesh of the object in one picture."""
124
135
 
125
136
  @abc.abstractmethod
126
- async def mesh(self, image: bytes, **kwargs: Any) -> GeneratedMesh: ...
137
+ async def mesh(
138
+ self, image: Base64Bytes, options: dict[str, Any] | None = None
139
+ ) -> GeneratedMesh: ...
127
140
 
128
141
 
129
- class RewritingModel(cortexgrid.DeploymentClient, abc.ABC):
142
+ class RewritingModel(abc.ABC):
130
143
  """A deployed model that rewrites one text into another - corrects,
131
144
  paraphrases, summarises or translates it. Like generation, a single
132
145
  request/response."""
133
146
 
134
147
  @abc.abstractmethod
135
- async def rewrite(self, text: str, max_new_tokens: int = 512, **kwargs: Any) -> str: ...
148
+ async def rewrite(self, text: str, max_new_tokens: int = 512) -> str: ...
136
149
 
137
150
 
138
151
  async def complete(
139
152
  deployed_model: CompletingModel,
140
153
  messages: list[Message],
141
- tools: Sequence[Tool] | None = None,
142
- max_new_tokens: int = 2048,
154
+ tools: list[Tool] | None = None,
155
+ max_new_tokens: int | None = None,
143
156
  temperature: float = 0.7,
144
- **kwargs: Any,
145
157
  ) -> AsyncIterator[CompletionChunk]:
146
158
  """Async completion request to the deployed model."""
147
159
  async for chunk in deployed_model.complete(
@@ -149,7 +161,6 @@ async def complete(
149
161
  tools,
150
162
  max_new_tokens=max_new_tokens,
151
163
  temperature=temperature,
152
- **kwargs,
153
164
  ):
154
165
  yield chunk
155
166
 
@@ -159,22 +170,28 @@ async def generate(
159
170
  prompt: str,
160
171
  *,
161
172
  image: bytes | None = None,
162
- **kwargs: Any,
173
+ steps: int | None = None,
174
+ guidance: float | None = None,
175
+ size: int = 1024,
176
+ seed: int | None = None,
163
177
  ) -> GeneratedImage:
164
178
  """Single image-generation request to the deployed model."""
165
- return await deployed_model.generate(prompt, image=image, **kwargs)
179
+ return await deployed_model.generate(
180
+ prompt, image=image, steps=steps, guidance=guidance, size=size, seed=seed
181
+ )
166
182
 
167
183
 
168
- async def mesh(deployed_model: MeshingModel, image: bytes, **kwargs: Any) -> GeneratedMesh:
184
+ async def mesh(
185
+ deployed_model: MeshingModel, image: bytes, options: dict[str, Any] | None = None
186
+ ) -> GeneratedMesh:
169
187
  """Single image-to-mesh request to the deployed model."""
170
- return await deployed_model.mesh(image, **kwargs)
188
+ return await deployed_model.mesh(image, options)
171
189
 
172
190
 
173
191
  async def rewrite(
174
192
  deployed_model: RewritingModel,
175
193
  text: str,
176
194
  max_new_tokens: int = 512,
177
- **kwargs: Any,
178
195
  ) -> str:
179
196
  """Single rewriting request to the deployed model."""
180
- return await deployed_model.rewrite(text, max_new_tokens=max_new_tokens, **kwargs)
197
+ return await deployed_model.rewrite(text, max_new_tokens=max_new_tokens)