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.
Files changed (34) hide show
  1. {spawnllm-0.13.3 → spawnllm-0.14.0}/PKG-INFO +1 -1
  2. {spawnllm-0.13.3 → spawnllm-0.14.0}/pyproject.toml +1 -1
  3. {spawnllm-0.13.3 → spawnllm-0.14.0}/pyproject.toml.orig +1 -1
  4. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/__init__.py +2 -1
  5. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/_core/spawnllm_core.wasm +0 -0
  6. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/base.py +27 -8
  7. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/claude.py +24 -19
  8. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/openai_endpoint.py +18 -3
  9. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/types.py +4 -1
  10. {spawnllm-0.13.3 → spawnllm-0.14.0}/LICENSE +0 -0
  11. {spawnllm-0.13.3 → spawnllm-0.14.0}/README.md +0 -0
  12. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/__main__.py +0 -0
  13. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/_core/__init__.py +0 -0
  14. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/__init__.py +0 -0
  15. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/apple.py +0 -0
  16. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/claude_sdk.py +0 -0
  17. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/codex.py +0 -0
  18. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/gemini.py +0 -0
  19. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/mlx.py +0 -0
  20. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/backends/registry.py +0 -0
  21. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/call.py +0 -0
  22. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/cli.py +0 -0
  23. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/extract.py +0 -0
  24. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/__init__.py +0 -0
  25. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/codec.py +0 -0
  26. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/engine.py +0 -0
  27. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/fuse.py +0 -0
  28. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/mlx/patches.py +0 -0
  29. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/proc.py +0 -0
  30. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/py.typed +0 -0
  31. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/response.py +0 -0
  32. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/run.py +0 -0
  33. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/spec.py +0 -0
  34. {spawnllm-0.13.3 → spawnllm-0.14.0}/spawnllm/structured.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.13.3
3
+ Version: 0.14.0
4
4
  Summary: Delete your subprocess wrappers around claude, codex, and gemini.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
- version = "0.13.3"
3
+ version = "0.14.0"
4
4
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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.3"
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",
@@ -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) -> str:
386
- """Return the isolated config home a claude run substitutes into `${isolated_config_dir}`."""
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
- directory = self.claude_isolation()
426
- argv = [arg.replace("${isolated_config_dir}", directory) for arg in argv]
427
- env = {key: value.replace("${isolated_config_dir}", directory) for key, value in env.items()}
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, or `{}`.
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
- directory = self.claude_isolation()
455
- return {key: value.replace("${isolated_config_dir}", directory) for key, value in plan["env"].items()}
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 and claude.ai OAuth token.
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) -> str:
67
- """Return the process-lifetime isolated config home, creating and seeding it once.
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; this host reads those sources (falling back to the Keychain when the
72
- credentials file is absent), hands them to `claude_isolation_seed` for the
73
- exact files-and-modes to write, and materializes them into a fresh temp
74
- dir removed at interpreter exit. The dir is cached on the backend.
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
- config_dir = Path(tempfile.mkdtemp(prefix="spawnllm-claude-config-"))
98
- for file in seed["files"]:
99
- fd = os.open(config_dir / file["name"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, int(file["mode"], 8))
100
- with os.fdopen(fd, "w") as handle:
101
- handle.write(file["content"])
102
- atexit.register(shutil.rmtree, config_dir, ignore_errors=True)
103
- self._isolated_config_dir = str(config_dir)
104
- return self._isolated_config_dir
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, base_url: str, model: str, *, api_key: str = "local", transport: httpx.AsyncBaseTransport | None = None
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 {"api_key": self.api_key, "base_url": self.base_url, "model": self.model}
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