spawnllm 0.13.3__tar.gz → 0.14.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.13.3 → spawnllm-0.14.0}/PKG-INFO +1 -1
- {spawnllm-0.13.3 → spawnllm-0.14.0}/pyproject.toml +1 -1
- {spawnllm-0.13.3 → spawnllm-0.14.0}/pyproject.toml.orig +1 -1
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/__init__.py +2 -1
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/_core/spawnllm_core.wasm +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/base.py +27 -8
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/claude.py +24 -19
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/openai_endpoint.py +18 -3
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/types.py +4 -1
- {spawnllm-0.13.3 → spawnllm-0.14.0}/LICENSE +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/README.md +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/__main__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/_core/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/apple.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/claude_sdk.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/codex.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/gemini.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/mlx.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/registry.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/call.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/cli.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/extract.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/codec.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/engine.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/fuse.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/patches.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/proc.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/py.typed +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/response.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/run.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/spec.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/structured.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spawnllm"
|
|
3
3
|
# Inert sentinel: the real version is set from the release tag (uv version --frozen).
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.14.0"
|
|
5
5
|
description = "Delete your subprocess wrappers around claude, codex, and gemini."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
@@ -34,7 +34,7 @@ from spawnllm.extract import extract, extract_sync
|
|
|
34
34
|
from spawnllm.response import DiscardedAttempt, Error, Output, Response, Result
|
|
35
35
|
from spawnllm.run import run, run_sync
|
|
36
36
|
from spawnllm.spec import AppleConfig, ClaudeConfig, CodexConfig, GeminiConfig, RunSpec
|
|
37
|
-
from spawnllm.types import ProviderName, TModel, TSpecialty
|
|
37
|
+
from spawnllm.types import ProviderName, TModel, TReasoningEffort, TSpecialty
|
|
38
38
|
|
|
39
39
|
__all__ = [
|
|
40
40
|
"AntigravityCliBackend",
|
|
@@ -66,6 +66,7 @@ __all__ = [
|
|
|
66
66
|
"Result",
|
|
67
67
|
"RunSpec",
|
|
68
68
|
"TModel",
|
|
69
|
+
"TReasoningEffort",
|
|
69
70
|
"TSpecialty",
|
|
70
71
|
"call",
|
|
71
72
|
"call_sync",
|
|
Binary file
|
|
@@ -83,6 +83,20 @@ class BackendCallError(RuntimeError):
|
|
|
83
83
|
"""
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ClaudeIsolation:
|
|
88
|
+
"""A seeded throwaway config home and the env a claude run adds beside it.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
config_dir: Path substituted for `${isolated_config_dir}` in the plan.
|
|
92
|
+
env: Variables the run sets on the child, such as `CLAUDE_CODE_OAUTH_TOKEN`
|
|
93
|
+
carrying the claude.ai access token; never written to disk.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
config_dir: str
|
|
97
|
+
env: dict[str, str]
|
|
98
|
+
|
|
99
|
+
|
|
86
100
|
@dataclass(frozen=True)
|
|
87
101
|
class Invocation:
|
|
88
102
|
"""A built CLI invocation: argv, optional stdin, and where to read the result.
|
|
@@ -382,8 +396,8 @@ class CliBackend(LlmBackend):
|
|
|
382
396
|
"""
|
|
383
397
|
return self.binary
|
|
384
398
|
|
|
385
|
-
def claude_isolation(self) ->
|
|
386
|
-
"""Return the
|
|
399
|
+
def claude_isolation(self) -> ClaudeIsolation:
|
|
400
|
+
"""Return the config home a claude run substitutes into `${isolated_config_dir}` and the env it adds."""
|
|
387
401
|
raise NotImplementedError
|
|
388
402
|
|
|
389
403
|
def build_command(self, spec: RunSpec) -> list[str]:
|
|
@@ -422,9 +436,11 @@ class CliBackend(LlmBackend):
|
|
|
422
436
|
argv = [tokens.get(arg, arg) for arg in plan["argv"]]
|
|
423
437
|
env = plan["env"]
|
|
424
438
|
if plan["needs_claude_isolation"]:
|
|
425
|
-
|
|
426
|
-
argv = [arg.replace("${isolated_config_dir}",
|
|
427
|
-
env = {
|
|
439
|
+
isolation = self.claude_isolation()
|
|
440
|
+
argv = [arg.replace("${isolated_config_dir}", isolation.config_dir) for arg in argv]
|
|
441
|
+
env = {
|
|
442
|
+
key: value.replace("${isolated_config_dir}", isolation.config_dir) for key, value in env.items()
|
|
443
|
+
} | isolation.env
|
|
428
444
|
except BaseException:
|
|
429
445
|
for path in paths.values():
|
|
430
446
|
Path(path).unlink(missing_ok=True)
|
|
@@ -446,13 +462,16 @@ class CliBackend(LlmBackend):
|
|
|
446
462
|
spec: The configured run; the plan gates isolation on `spec.isolated`.
|
|
447
463
|
|
|
448
464
|
Returns:
|
|
449
|
-
The plan's env map with `${isolated_config_dir}` resolved
|
|
465
|
+
The plan's env map with `${isolated_config_dir}` resolved plus the
|
|
466
|
+
isolation's own entries, or `{}`.
|
|
450
467
|
"""
|
|
451
468
|
plan = self.core_plan(spec)
|
|
452
469
|
if not plan["needs_claude_isolation"]:
|
|
453
470
|
return plan["env"]
|
|
454
|
-
|
|
455
|
-
return {
|
|
471
|
+
isolation = self.claude_isolation()
|
|
472
|
+
return {
|
|
473
|
+
key: value.replace("${isolated_config_dir}", isolation.config_dir) for key, value in plan["env"].items()
|
|
474
|
+
} | isolation.env
|
|
456
475
|
|
|
457
476
|
def accounting(self, raw: str) -> tuple[float | None, dict[str, object] | None]:
|
|
458
477
|
"""Return the `(cost_usd, usage)` the core's `resolve` op reads from `raw`."""
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import TYPE_CHECKING, ClassVar
|
|
13
13
|
|
|
14
14
|
from spawnllm import _core
|
|
15
|
-
from spawnllm.backends.base import CliBackend
|
|
15
|
+
from spawnllm.backends.base import ClaudeIsolation, CliBackend
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from spawnllm.types import ProviderName, TModel
|
|
@@ -43,7 +43,8 @@ class ClaudeCliBackend(CliBackend):
|
|
|
43
43
|
|
|
44
44
|
The core plans the `claude -p` argv (prompt delivered over stdin, result read
|
|
45
45
|
from a stdout file) and lays out the host-free config home this backend seeds
|
|
46
|
-
with only the active-account pointer
|
|
46
|
+
with only the active-account pointer; the claude.ai access token reaches the
|
|
47
|
+
child as `CLAUDE_CODE_OAUTH_TOKEN`, never as a file.
|
|
47
48
|
|
|
48
49
|
Attributes:
|
|
49
50
|
models: Mapping from abstract model size to a Claude model alias
|
|
@@ -63,18 +64,20 @@ class ClaudeCliBackend(CliBackend):
|
|
|
63
64
|
|
|
64
65
|
_isolated_config_dir: str | None = None
|
|
65
66
|
|
|
66
|
-
def claude_isolation(self) ->
|
|
67
|
-
"""Return the process-lifetime
|
|
67
|
+
def claude_isolation(self) -> ClaudeIsolation:
|
|
68
|
+
"""Return the isolation for one run: the process-lifetime config home and the env resolved now.
|
|
68
69
|
|
|
69
70
|
The core's `claude_isolation_sources` op resolves the account pointer,
|
|
70
71
|
credentials file, and Keychain service from the caller's effective config
|
|
71
|
-
home
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
home, naming no credential source when the process already carries
|
|
73
|
+
`CLAUDE_CODE_OAUTH_TOKEN`; this host reads those sources (falling back to
|
|
74
|
+
the Keychain when the credentials file is absent) and hands them to
|
|
75
|
+
`claude_isolation_seed` for the exact files-and-modes to write and the env
|
|
76
|
+
to set. The files land in a fresh temp dir removed at interpreter exit,
|
|
77
|
+
created on the first call and cached on the backend; the env is resolved
|
|
78
|
+
on every call so a renewed Keychain token reaches the next run, and it
|
|
79
|
+
only ever lives in memory.
|
|
75
80
|
"""
|
|
76
|
-
if self._isolated_config_dir is not None:
|
|
77
|
-
return self._isolated_config_dir
|
|
78
81
|
sources = _core.dispatch(
|
|
79
82
|
"claude_isolation_sources",
|
|
80
83
|
{
|
|
@@ -84,21 +87,23 @@ class ClaudeCliBackend(CliBackend):
|
|
|
84
87
|
"claude_config_dir_env": os.environ.get("CLAUDE_CONFIG_DIR") or None,
|
|
85
88
|
"claude_securestorage_config_dir_env": os.environ.get("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
|
|
86
89
|
"claude_code_custom_oauth_url_env": os.environ.get("CLAUDE_CODE_CUSTOM_OAUTH_URL"),
|
|
90
|
+
"claude_code_oauth_token_env": os.environ.get("CLAUDE_CODE_OAUTH_TOKEN"),
|
|
87
91
|
}
|
|
88
92
|
},
|
|
89
93
|
)
|
|
90
94
|
account_json = read_file_opt(sources["account_path"])
|
|
91
|
-
credentials_json = read_file_opt(sources["credentials_path"])
|
|
95
|
+
credentials_json = read_file_opt(sources["credentials_path"]) if sources["credentials_path"] else None
|
|
92
96
|
if credentials_json is None and sources["keychain_service"] is not None:
|
|
93
97
|
credentials_json = keychain_credentials(sources["keychain_service"])
|
|
94
98
|
seed = _core.dispatch(
|
|
95
99
|
"claude_isolation_seed", {"account_json": account_json, "credentials_json": credentials_json}
|
|
96
100
|
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if self._isolated_config_dir is None:
|
|
102
|
+
config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
|
|
103
|
+
for file in seed["files"]:
|
|
104
|
+
fd = os.open(config_dir / file["name"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, int(file["mode"], 8))
|
|
105
|
+
with os.fdopen(fd, "w") as handle:
|
|
106
|
+
handle.write(file["content"])
|
|
107
|
+
atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
|
|
108
|
+
self._isolated_config_dir = str(config_dir)
|
|
109
|
+
return ClaudeIsolation(self._isolated_config_dir, seed["env"])
|
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|
|
12
12
|
from spawnllm.backends.base import BackendStatus
|
|
13
13
|
from spawnllm.response import Response
|
|
14
14
|
from spawnllm.spec import RunSpec
|
|
15
|
-
from spawnllm.types import ProviderName, TModel
|
|
15
|
+
from spawnllm.types import ProviderName, TModel, TReasoningEffort
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class OpenAiEndpointBackend(LlmBackend):
|
|
@@ -30,6 +30,9 @@ class OpenAiEndpointBackend(LlmBackend):
|
|
|
30
30
|
model: The literal model id sent in every request body.
|
|
31
31
|
api_key: Bearer token for the `Authorization` header; defaults to
|
|
32
32
|
`"local"` for self-hosted servers that ignore it.
|
|
33
|
+
reasoning_effort: The `reasoning_effort` sent in every request body;
|
|
34
|
+
`None` omits it and leaves the server's default, which on a
|
|
35
|
+
reasoning model can spend most of a call's latency thinking.
|
|
33
36
|
transport: Async transport injected into the `httpx.AsyncClient` used by
|
|
34
37
|
`aexecute` — e.g. a record/replay caching transport; `None` uses
|
|
35
38
|
httpx's default transport. The synchronous `execute` path always uses
|
|
@@ -44,17 +47,29 @@ class OpenAiEndpointBackend(LlmBackend):
|
|
|
44
47
|
schema_dialect: ClassVar[str | None] = "openai"
|
|
45
48
|
|
|
46
49
|
def __init__(
|
|
47
|
-
self,
|
|
50
|
+
self,
|
|
51
|
+
base_url: str,
|
|
52
|
+
model: str,
|
|
53
|
+
*,
|
|
54
|
+
api_key: str = "local",
|
|
55
|
+
reasoning_effort: TReasoningEffort | None = None,
|
|
56
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
48
57
|
) -> None:
|
|
49
58
|
self.base_url = base_url.rstrip("/")
|
|
50
59
|
self.model = model
|
|
51
60
|
self.api_key = api_key
|
|
61
|
+
self.reasoning_effort = reasoning_effort
|
|
52
62
|
self.transport = transport
|
|
53
63
|
self.models: dict[TModel, str] = {"small": model, "medium": model, "large": model}
|
|
54
64
|
|
|
55
65
|
def openai_section(self) -> dict[str, Any]:
|
|
56
66
|
"""Return the `openai_endpoint` wire section the core turns into the HTTP request."""
|
|
57
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
"api_key": self.api_key,
|
|
69
|
+
"base_url": self.base_url,
|
|
70
|
+
"model": self.model,
|
|
71
|
+
"reasoning_effort": self.reasoning_effort,
|
|
72
|
+
}
|
|
58
73
|
|
|
59
74
|
def resolve(self, resp: httpx.Response, spec: RunSpec) -> Response:
|
|
60
75
|
return self.to_response(
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Literal
|
|
6
6
|
|
|
7
|
-
__all__ = ["ProviderName", "TModel", "TSettingSource", "TSpecialty"]
|
|
7
|
+
__all__ = ["ProviderName", "TModel", "TReasoningEffort", "TSettingSource", "TSpecialty"]
|
|
8
8
|
|
|
9
9
|
TSpecialty = Literal["debugging", "review", "general"]
|
|
10
10
|
"""Task specialty; `LlmBackends.for_specialty` maps each to its registered backend."""
|
|
@@ -15,5 +15,8 @@ TSettingSource = Literal["user", "project", "local"]
|
|
|
15
15
|
TModel = Literal["small", "medium", "large"]
|
|
16
16
|
"""Abstract model tier; each backend maps it to a provider-specific model name."""
|
|
17
17
|
|
|
18
|
+
TReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
|
|
19
|
+
"""A `reasoning_effort` an OpenAI-compatible endpoint accepts; `OpenAiEndpointBackend` sends it with every request."""
|
|
20
|
+
|
|
18
21
|
ProviderName = Literal["claude", "claude-sdk", "codex", "gemini", "antigravity", "mlx", "apple", "openai_endpoint"]
|
|
19
22
|
"""Backend provider identifier; keys the per-backend `provider_configs` on a `RunSpec`."""
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|