spawnllm 0.1.3__tar.gz → 0.3.0__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.
- {spawnllm-0.1.3 → spawnllm-0.3.0}/PKG-INFO +75 -14
- spawnllm-0.3.0/README.md +123 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/pyproject.toml +6 -1
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/__init__.py +20 -14
- spawnllm-0.3.0/spawnllm/backends/__init__.py +33 -0
- spawnllm-0.3.0/spawnllm/backends/base.py +184 -0
- spawnllm-0.3.0/spawnllm/backends/claude.py +194 -0
- spawnllm-0.3.0/spawnllm/backends/codex.py +137 -0
- spawnllm-0.3.0/spawnllm/backends/gemini.py +226 -0
- spawnllm-0.3.0/spawnllm/backends/registry.py +80 -0
- spawnllm-0.3.0/spawnllm/call.py +54 -0
- spawnllm-0.3.0/spawnllm/cli.py +66 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/__init__.py +2 -2
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/codec.py +26 -6
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/engine.py +49 -5
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/fuse.py +23 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/patches.py +11 -1
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/proc.py +53 -1
- spawnllm-0.3.0/spawnllm/structured.py +120 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/types.py +3 -0
- spawnllm-0.1.3/README.md +0 -64
- spawnllm-0.1.3/spawnllm/backends/__init__.py +0 -27
- spawnllm-0.1.3/spawnllm/backends/base.py +0 -53
- spawnllm-0.1.3/spawnllm/backends/claude.py +0 -126
- spawnllm-0.1.3/spawnllm/backends/codex.py +0 -41
- spawnllm-0.1.3/spawnllm/backends/registry.py +0 -27
- spawnllm-0.1.3/spawnllm/call.py +0 -42
- spawnllm-0.1.3/spawnllm/cli.py +0 -40
- spawnllm-0.1.3/spawnllm/structured.py +0 -68
- {spawnllm-0.1.3 → spawnllm-0.3.0}/LICENSE +0 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/__main__.py +0 -0
- {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spawnllm
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
|
|
5
5
|
Keywords:
|
|
6
6
|
Author: Yasyf Mohamedali
|
|
@@ -16,6 +16,8 @@ Classifier: Typing :: Typed
|
|
|
16
16
|
Requires-Dist: click>=8
|
|
17
17
|
Requires-Dist: loguru>=0.7
|
|
18
18
|
Requires-Dist: pydantic>=2
|
|
19
|
+
Requires-Dist: openai>=2.43
|
|
20
|
+
Requires-Dist: anthropic>=0.111
|
|
19
21
|
Requires-Dist: zstandard>=0.25.0 ; extra == 'adapter'
|
|
20
22
|
Requires-Dist: numpy>=1.26 ; extra == 'adapter'
|
|
21
23
|
Requires-Dist: orjson>=3.10 ; extra == 'adapter'
|
|
@@ -45,6 +47,8 @@ Description-Content-Type: text/markdown
|
|
|
45
47
|
|
|
46
48
|
# spawnllm
|
|
47
49
|
|
|
50
|
+

|
|
51
|
+
|
|
48
52
|
[](https://pypi.org/project/spawnllm/)
|
|
49
53
|
[](https://pypi.org/project/spawnllm/)
|
|
50
54
|
[](https://yasyf.github.io/spawnllm/)
|
|
@@ -81,28 +85,85 @@ uv add "spawnllm[mlx]"
|
|
|
81
85
|
|
|
82
86
|
## Quickstart
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
See which backends are installed and authenticated, and which one auto-selection picks:
|
|
85
89
|
|
|
86
90
|
```bash
|
|
87
|
-
uvx spawnllm
|
|
91
|
+
uvx spawnllm status
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
claude: ready
|
|
96
|
+
codex: ready
|
|
97
|
+
selected: claude
|
|
88
98
|
```
|
|
89
99
|
|
|
100
|
+
Make a request by passing a prompt as the argument, or piping it over stdin:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uvx spawnllm call --backend claude "What is 2+2? Reply with just the number."
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
4
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`--model small|medium|large` swaps the tier, which each backend maps to a concrete model.
|
|
111
|
+
The `claude` backend resolves `small` to Haiku, `medium` to Sonnet, and `large` to Opus. Add
|
|
112
|
+
`--agent` to let the call use tools.
|
|
113
|
+
|
|
114
|
+
### From Python
|
|
115
|
+
|
|
116
|
+
`call` runs one request and returns the response. With no `backend`, it auto-selects the
|
|
117
|
+
first installed, authenticated CLI:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from spawnllm import call
|
|
121
|
+
|
|
122
|
+
print(call("Reply with just the word: pong"))
|
|
123
|
+
# pong
|
|
90
124
|
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
|
|
126
|
+
Pin a backend and tier explicitly, or pass a Pydantic model to get a validated object back
|
|
127
|
+
instead of text:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pydantic import BaseModel
|
|
131
|
+
|
|
132
|
+
from spawnllm import call, ClaudeCliBackend
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Capital(BaseModel):
|
|
136
|
+
country: str
|
|
137
|
+
capital: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
result = call(
|
|
141
|
+
"What is the capital of France?",
|
|
142
|
+
backend=ClaudeCliBackend(),
|
|
143
|
+
model="large",
|
|
144
|
+
response_model=Capital,
|
|
145
|
+
)
|
|
146
|
+
print(result.capital) # Paris
|
|
94
147
|
```
|
|
95
148
|
|
|
149
|
+
When you don't pin a backend, set `specialty=` to scope auto-selection by task. The
|
|
150
|
+
`debugging` and `review` specialties route to Codex, and `general` routes to Claude.
|
|
151
|
+
|
|
96
152
|
## What problems does this solve?
|
|
97
153
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
154
|
+
Every tool that shells out to `claude` or `codex` rebuilds the same plumbing: argv
|
|
155
|
+
construction, stdin/stdout piping, stderr teeing, and turning non-zero exits into useful
|
|
156
|
+
errors. spawnllm holds it once.
|
|
157
|
+
|
|
158
|
+
Structured output is boilerplate too. A Pydantic model becomes a JSON-schema constraint
|
|
159
|
+
and a parsed, validated result, identically for both CLI backends.
|
|
160
|
+
|
|
161
|
+
Local MLX is fiddly. Adapter fusion, prompt-cache reuse, worker-thread lifecycle, and
|
|
162
|
+
batched single-token generation live behind one engine instead of in every consumer.
|
|
163
|
+
|
|
164
|
+
Behavior drift goes away with the duplication: two tools that call the same models stay
|
|
165
|
+
byte-for-byte consistent because they share the backend layer, not a pair of diverging
|
|
166
|
+
copies.
|
|
106
167
|
|
|
107
168
|
## Docs
|
|
108
169
|
|
spawnllm-0.3.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# spawnllm
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/spawnllm/)
|
|
6
|
+
[](https://pypi.org/project/spawnllm/)
|
|
7
|
+
[](https://yasyf.github.io/spawnllm/)
|
|
8
|
+
[](https://github.com/yasyf/spawnllm/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
|
|
11
|
+
|
|
12
|
+
spawnllm centralizes the LLM-calling plumbing that small tools keep re-inventing: driving the
|
|
13
|
+
`claude` and `codex` CLIs as subshells — with structured Pydantic output, model tiers, and
|
|
14
|
+
faithful error capture — and running local Apple-Silicon MLX models with adapter fusion,
|
|
15
|
+
prompt-cache reuse, and batched generation. Depend on it once and each tool keeps only its
|
|
16
|
+
domain logic instead of its own copy of the backends.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uvx spawnllm --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`uvx` fetches spawnllm into a throwaway environment and runs it. To add it
|
|
27
|
+
to a project instead:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add spawnllm
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For the local MLX engine (Apple Silicon only), pull the extra:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv add "spawnllm[mlx]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
See which backends are installed and authenticated, and which one auto-selection picks:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uvx spawnllm status
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
claude: ready
|
|
49
|
+
codex: ready
|
|
50
|
+
selected: claude
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Make a request by passing a prompt as the argument, or piping it over stdin:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uvx spawnllm call --backend claude "What is 2+2? Reply with just the number."
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
4
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`--model small|medium|large` swaps the tier, which each backend maps to a concrete model.
|
|
64
|
+
The `claude` backend resolves `small` to Haiku, `medium` to Sonnet, and `large` to Opus. Add
|
|
65
|
+
`--agent` to let the call use tools.
|
|
66
|
+
|
|
67
|
+
### From Python
|
|
68
|
+
|
|
69
|
+
`call` runs one request and returns the response. With no `backend`, it auto-selects the
|
|
70
|
+
first installed, authenticated CLI:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from spawnllm import call
|
|
74
|
+
|
|
75
|
+
print(call("Reply with just the word: pong"))
|
|
76
|
+
# pong
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Pin a backend and tier explicitly, or pass a Pydantic model to get a validated object back
|
|
80
|
+
instead of text:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pydantic import BaseModel
|
|
84
|
+
|
|
85
|
+
from spawnllm import call, ClaudeCliBackend
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Capital(BaseModel):
|
|
89
|
+
country: str
|
|
90
|
+
capital: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
result = call(
|
|
94
|
+
"What is the capital of France?",
|
|
95
|
+
backend=ClaudeCliBackend(),
|
|
96
|
+
model="large",
|
|
97
|
+
response_model=Capital,
|
|
98
|
+
)
|
|
99
|
+
print(result.capital) # Paris
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When you don't pin a backend, set `specialty=` to scope auto-selection by task. The
|
|
103
|
+
`debugging` and `review` specialties route to Codex, and `general` routes to Claude.
|
|
104
|
+
|
|
105
|
+
## What problems does this solve?
|
|
106
|
+
|
|
107
|
+
Every tool that shells out to `claude` or `codex` rebuilds the same plumbing: argv
|
|
108
|
+
construction, stdin/stdout piping, stderr teeing, and turning non-zero exits into useful
|
|
109
|
+
errors. spawnllm holds it once.
|
|
110
|
+
|
|
111
|
+
Structured output is boilerplate too. A Pydantic model becomes a JSON-schema constraint
|
|
112
|
+
and a parsed, validated result, identically for both CLI backends.
|
|
113
|
+
|
|
114
|
+
Local MLX is fiddly. Adapter fusion, prompt-cache reuse, worker-thread lifecycle, and
|
|
115
|
+
batched single-token generation live behind one engine instead of in every consumer.
|
|
116
|
+
|
|
117
|
+
Behavior drift goes away with the duplication: two tools that call the same models stay
|
|
118
|
+
byte-for-byte consistent because they share the backend layer, not a pair of diverging
|
|
119
|
+
copies.
|
|
120
|
+
|
|
121
|
+
## Docs
|
|
122
|
+
|
|
123
|
+
[Read the docs](https://yasyf.github.io/spawnllm/) for the full guide and API reference.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spawnllm"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -20,6 +20,11 @@ dependencies = [
|
|
|
20
20
|
"click>=8",
|
|
21
21
|
"loguru>=0.7",
|
|
22
22
|
"pydantic>=2",
|
|
23
|
+
# Per-backend strict-schema transforms: openai.lib._pydantic.to_strict_json_schema
|
|
24
|
+
# and anthropic.lib._parse._transform.transform_schema. Both import paths are
|
|
25
|
+
# SDK-private, so the floors pin verified-working versions.
|
|
26
|
+
"openai>=2.43",
|
|
27
|
+
"anthropic>=0.111",
|
|
23
28
|
]
|
|
24
29
|
|
|
25
30
|
[project.optional-dependencies]
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
"""Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
|
|
2
2
|
|
|
3
3
|
The top-level namespace exposes the CLI backends, subprocess transport, and
|
|
4
|
-
structured-output helpers. The MLX engine lives under
|
|
5
|
-
|
|
4
|
+
structured-output helpers. The MLX engine lives under `spawnllm.mlx`, whose
|
|
5
|
+
imports are lazy so that `import spawnllm` never pulls `mlx_lm`/`zstandard`.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from spawnllm.backends import (
|
|
11
|
+
AntigravityCliBackend,
|
|
12
|
+
BackendNotAuthenticated,
|
|
13
|
+
BackendNotInstalled,
|
|
14
|
+
BackendReady,
|
|
15
|
+
BackendStatus,
|
|
16
|
+
BackendUnavailable,
|
|
11
17
|
ClaudeCliBackend,
|
|
12
|
-
ClaudeNotAuthenticated,
|
|
13
|
-
ClaudeNotInstalled,
|
|
14
|
-
ClaudeReady,
|
|
15
|
-
ClaudeStatus,
|
|
16
18
|
CodexCliBackend,
|
|
19
|
+
GeminiCliBackend,
|
|
20
|
+
Invocation,
|
|
17
21
|
LlmBackend,
|
|
18
22
|
LlmBackends,
|
|
19
|
-
|
|
23
|
+
select_backend,
|
|
20
24
|
)
|
|
21
25
|
from spawnllm.call import call
|
|
22
26
|
from spawnllm.proc import arun_cli, collect_process, map_concurrent, run_cli
|
|
@@ -25,24 +29,26 @@ from spawnllm.structured import (
|
|
|
25
29
|
parse_result_envelope,
|
|
26
30
|
parse_structured_output,
|
|
27
31
|
resolve_schema_path,
|
|
28
|
-
schema_for,
|
|
29
32
|
)
|
|
30
33
|
from spawnllm.types import TModel, TSpecialty
|
|
31
34
|
|
|
32
35
|
__all__ = [
|
|
36
|
+
"AntigravityCliBackend",
|
|
37
|
+
"BackendNotAuthenticated",
|
|
38
|
+
"BackendNotInstalled",
|
|
39
|
+
"BackendReady",
|
|
40
|
+
"BackendStatus",
|
|
41
|
+
"BackendUnavailable",
|
|
33
42
|
"ClaudeCliBackend",
|
|
34
|
-
"ClaudeNotAuthenticated",
|
|
35
|
-
"ClaudeNotInstalled",
|
|
36
|
-
"ClaudeReady",
|
|
37
|
-
"ClaudeStatus",
|
|
38
43
|
"CodexCliBackend",
|
|
44
|
+
"GeminiCliBackend",
|
|
45
|
+
"Invocation",
|
|
39
46
|
"LlmBackend",
|
|
40
47
|
"LlmBackends",
|
|
41
48
|
"TModel",
|
|
42
49
|
"TSpecialty",
|
|
43
50
|
"arun_cli",
|
|
44
51
|
"call",
|
|
45
|
-
"check_status",
|
|
46
52
|
"collect_process",
|
|
47
53
|
"extract_structured",
|
|
48
54
|
"map_concurrent",
|
|
@@ -50,5 +56,5 @@ __all__ = [
|
|
|
50
56
|
"parse_structured_output",
|
|
51
57
|
"resolve_schema_path",
|
|
52
58
|
"run_cli",
|
|
53
|
-
"
|
|
59
|
+
"select_backend",
|
|
54
60
|
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""LLM CLI backends (Claude/Codex/Gemini family) and the specialty registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from spawnllm.backends.base import (
|
|
6
|
+
BackendNotAuthenticated,
|
|
7
|
+
BackendNotInstalled,
|
|
8
|
+
BackendReady,
|
|
9
|
+
BackendStatus,
|
|
10
|
+
BackendUnavailable,
|
|
11
|
+
Invocation,
|
|
12
|
+
LlmBackend,
|
|
13
|
+
)
|
|
14
|
+
from spawnllm.backends.claude import ClaudeCliBackend
|
|
15
|
+
from spawnllm.backends.codex import CodexCliBackend
|
|
16
|
+
from spawnllm.backends.gemini import AntigravityCliBackend, GeminiCliBackend
|
|
17
|
+
from spawnllm.backends.registry import LlmBackends, select_backend
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"AntigravityCliBackend",
|
|
21
|
+
"BackendNotAuthenticated",
|
|
22
|
+
"BackendNotInstalled",
|
|
23
|
+
"BackendReady",
|
|
24
|
+
"BackendStatus",
|
|
25
|
+
"BackendUnavailable",
|
|
26
|
+
"ClaudeCliBackend",
|
|
27
|
+
"CodexCliBackend",
|
|
28
|
+
"GeminiCliBackend",
|
|
29
|
+
"Invocation",
|
|
30
|
+
"LlmBackend",
|
|
31
|
+
"LlmBackends",
|
|
32
|
+
"select_backend",
|
|
33
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Abstract interface for an LLM CLI backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from spawnllm.types import TModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class BackendReady:
|
|
19
|
+
"""A backend whose CLI is installed and authenticated.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
binary: Name of the backend's CLI executable on PATH.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
binary: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class BackendNotInstalled:
|
|
30
|
+
"""A backend whose CLI is not on PATH.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
binary: Name of the backend's CLI executable.
|
|
34
|
+
install_hint: Suggested shell command to install the CLI.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
binary: str
|
|
38
|
+
install_hint: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class BackendNotAuthenticated:
|
|
43
|
+
"""A backend whose CLI is installed but not authenticated.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
binary: Name of the backend's CLI executable on PATH.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
binary: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
BackendStatus = BackendReady | BackendNotInstalled | BackendNotAuthenticated
|
|
53
|
+
"""Result of `LlmBackend.check_status`: `BackendReady`, `BackendNotInstalled`, or `BackendNotAuthenticated`."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BackendUnavailable(RuntimeError):
|
|
57
|
+
"""Raised when no backend is ready (installed and authenticated)."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Invocation:
|
|
62
|
+
"""A built CLI invocation: argv, optional stdin, and where to read the result.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
argv: The argv list to execute.
|
|
66
|
+
stdin: Prompt text delivered over stdin, or `None` when delivered inline.
|
|
67
|
+
result_path: File the backend writes its final message to; when set, the
|
|
68
|
+
result is read from this file instead of stdout.
|
|
69
|
+
cleanup_paths: Temp files to remove once the invocation completes.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
argv: list[str]
|
|
73
|
+
stdin: str | None = None
|
|
74
|
+
result_path: str | None = None
|
|
75
|
+
cleanup_paths: tuple[str, ...] = ()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LlmBackend(ABC):
|
|
79
|
+
"""Abstract interface for an LLM CLI backend.
|
|
80
|
+
|
|
81
|
+
Concrete backends map abstract model sizes to provider-specific model names
|
|
82
|
+
and encapsulate how to invoke the provider's CLI and parse the raw response.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
models: Mapping from abstract model size to the provider's model name.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
models: ClassVar[dict[TModel, str]]
|
|
89
|
+
binary: ClassVar[str]
|
|
90
|
+
install_hint: ClassVar[str]
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def build_command(self, model: str, schema_path: str | None, agent: bool) -> list[str]:
|
|
94
|
+
"""Build the CLI argv for a single invocation (prompt delivered via stdin).
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
model: Provider-specific model name.
|
|
98
|
+
schema_path: Schema argument for structured output, or `None`.
|
|
99
|
+
agent: Whether the invocation may use tools / agent capabilities.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
The argv list to execute.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def parse_response(self, raw: str, response_model: type[BaseModel] | None) -> str | BaseModel:
|
|
107
|
+
"""Parse raw CLI stdout into text or a validated model.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
raw: Raw stdout from the backend CLI.
|
|
111
|
+
response_model: Model to validate against, or `None` for raw text.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
`raw` when `response_model` is `None`, else a validated instance.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def env(self) -> dict[str, str]:
|
|
119
|
+
"""Return extra environment variables for the CLI invocation, merged over the inherited environment."""
|
|
120
|
+
|
|
121
|
+
def check_status(self, *, timeout: int = 10) -> BackendStatus:
|
|
122
|
+
"""Check whether this backend's CLI is installed and authenticated.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
timeout: Seconds to wait for the authentication probe.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
`BackendReady` when authenticated, `BackendNotInstalled` when the CLI
|
|
129
|
+
is not on PATH, else `BackendNotAuthenticated`.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
subprocess.TimeoutExpired: If `is_authenticated` exceeds `timeout`.
|
|
133
|
+
"""
|
|
134
|
+
if not shutil.which(self.binary):
|
|
135
|
+
return BackendNotInstalled(binary=self.binary, install_hint=self.install_hint)
|
|
136
|
+
if self.is_authenticated(timeout=timeout):
|
|
137
|
+
return BackendReady(binary=self.binary)
|
|
138
|
+
return BackendNotAuthenticated(binary=self.binary)
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def is_authenticated(self, *, timeout: int) -> bool:
|
|
142
|
+
"""Probe whether the CLI holds valid credentials for its provider.
|
|
143
|
+
|
|
144
|
+
"Authenticated" means the CLI reports an active login session for the
|
|
145
|
+
provider, not merely that the executable is present on PATH.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
timeout: Seconds to wait for the credential probe.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
`True` when the CLI reports an authenticated session.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def schema_for(self, model: type[BaseModel]) -> str:
|
|
155
|
+
"""Serialize a Pydantic model into the JSON-schema string this backend's CLI expects.
|
|
156
|
+
|
|
157
|
+
The default emits the model's plain JSON schema; provider backends
|
|
158
|
+
override to apply their SDK's strict-schema transform.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
model: The Pydantic model describing the structured output.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A JSON-schema string suitable for this backend's structured-output argument.
|
|
165
|
+
"""
|
|
166
|
+
return json.dumps(model.model_json_schema())
|
|
167
|
+
|
|
168
|
+
def invocation(self, prompt: str, *, model: str, schema_path: str | None, agent: bool) -> Invocation:
|
|
169
|
+
"""Build the argv, stdin, and result source for a single invocation.
|
|
170
|
+
|
|
171
|
+
The default delivers the prompt over stdin and reads the result from
|
|
172
|
+
stdout; subclasses override to deliver the prompt inline or to read the
|
|
173
|
+
result from a file.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
prompt: The prompt text to deliver to the CLI.
|
|
177
|
+
model: Provider-specific model name.
|
|
178
|
+
schema_path: Schema argument for structured output, or `None`.
|
|
179
|
+
agent: Whether the invocation may use tools / agent capabilities.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
An `Invocation` carrying the argv, stdin text, and result source.
|
|
183
|
+
"""
|
|
184
|
+
return Invocation(self.build_command(model, schema_path, agent), prompt)
|