spawnllm 0.13.3__tar.gz → 0.13.4__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.13.4}/PKG-INFO +1 -1
- {spawnllm-0.13.3 → spawnllm-0.13.4}/pyproject.toml +1 -1
- {spawnllm-0.13.3 → spawnllm-0.13.4}/pyproject.toml.orig +1 -1
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/_core/spawnllm_core.wasm +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/base.py +27 -8
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/claude.py +24 -19
- {spawnllm-0.13.3 → spawnllm-0.13.4}/LICENSE +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/README.md +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/__main__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/_core/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/apple.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/claude_sdk.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/codex.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/gemini.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/mlx.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/openai_endpoint.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/backends/registry.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/call.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/cli.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/extract.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/__init__.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/codec.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/engine.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/fuse.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/mlx/patches.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/proc.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/py.typed +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/response.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/run.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/spec.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/structured.py +0 -0
- {spawnllm-0.13.3 → spawnllm-0.13.4}/spawnllm/types.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.13.
|
|
4
|
+
version = "0.13.4"
|
|
5
5
|
description = "Delete your subprocess wrappers around claude, codex, and gemini."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "MIT"
|
|
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"])
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|