spawnllm 0.6.0__tar.gz → 0.6.1__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 (29) hide show
  1. {spawnllm-0.6.0 → spawnllm-0.6.1}/PKG-INFO +2 -1
  2. {spawnllm-0.6.0 → spawnllm-0.6.1}/pyproject.toml +2 -1
  3. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/__init__.py +2 -0
  4. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/__init__.py +2 -0
  5. spawnllm-0.6.1/spawnllm/backends/openai_endpoint.py +137 -0
  6. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/types.py +1 -1
  7. {spawnllm-0.6.0 → spawnllm-0.6.1}/LICENSE +0 -0
  8. {spawnllm-0.6.0 → spawnllm-0.6.1}/README.md +0 -0
  9. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/__main__.py +0 -0
  10. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/base.py +0 -0
  11. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/claude.py +0 -0
  12. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/codex.py +0 -0
  13. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/gemini.py +0 -0
  14. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/mlx.py +0 -0
  15. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/backends/registry.py +0 -0
  16. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/call.py +0 -0
  17. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/cli.py +0 -0
  18. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/extract.py +0 -0
  19. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/mlx/__init__.py +0 -0
  20. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/mlx/codec.py +0 -0
  21. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/mlx/engine.py +0 -0
  22. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/mlx/fuse.py +0 -0
  23. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/mlx/patches.py +0 -0
  24. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/proc.py +0 -0
  25. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/py.typed +0 -0
  26. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/response.py +0 -0
  27. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/run.py +0 -0
  28. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/spec.py +0 -0
  29. {spawnllm-0.6.0 → spawnllm-0.6.1}/spawnllm/structured.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Delete your subprocess wrappers around claude, codex, and gemini.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Typing :: Typed
16
16
  Requires-Dist: click>=8
17
+ Requires-Dist: httpx>=0.27
17
18
  Requires-Dist: loguru>=0.7
18
19
  Requires-Dist: pydantic>=2
19
20
  Requires-Dist: openai>=2.43
@@ -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.6.0"
4
+ version = "0.6.1"
5
5
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -19,6 +19,7 @@ classifiers = [
19
19
  requires-python = ">=3.13"
20
20
  dependencies = [
21
21
  "click>=8",
22
+ "httpx>=0.27",
22
23
  "loguru>=0.7",
23
24
  "pydantic>=2",
24
25
  # Per-backend strict-schema transforms: openai.lib._pydantic.to_strict_json_schema
@@ -24,6 +24,7 @@ from spawnllm.backends import (
24
24
  LlmBackend,
25
25
  LlmBackends,
26
26
  MlxBackend,
27
+ OpenAiEndpointBackend,
27
28
  select_backend,
28
29
  )
29
30
  from spawnllm.call import call, call_sync
@@ -53,6 +54,7 @@ __all__ = [
53
54
  "LlmBackend",
54
55
  "LlmBackends",
55
56
  "MlxBackend",
57
+ "OpenAiEndpointBackend",
56
58
  "Output",
57
59
  "ProviderName",
58
60
  "Response",
@@ -16,6 +16,7 @@ from spawnllm.backends.claude import ClaudeCliBackend
16
16
  from spawnllm.backends.codex import CodexCliBackend
17
17
  from spawnllm.backends.gemini import AntigravityCliBackend, GeminiCliBackend
18
18
  from spawnllm.backends.mlx import MlxBackend
19
+ from spawnllm.backends.openai_endpoint import OpenAiEndpointBackend
19
20
  from spawnllm.backends.registry import LlmBackends, select_backend
20
21
 
21
22
  __all__ = [
@@ -33,5 +34,6 @@ __all__ = [
33
34
  "LlmBackend",
34
35
  "LlmBackends",
35
36
  "MlxBackend",
37
+ "OpenAiEndpointBackend",
36
38
  "select_backend",
37
39
  ]
@@ -0,0 +1,137 @@
1
+ """HTTP backend for any OpenAI-compatible `/chat/completions` endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import TYPE_CHECKING, ClassVar
7
+
8
+ import httpx
9
+
10
+ from spawnllm.backends.base import BackendReady, LlmBackend
11
+ from spawnllm.structured import extract_json_block
12
+
13
+ if TYPE_CHECKING:
14
+ from pydantic import BaseModel
15
+
16
+ from spawnllm.backends.base import BackendStatus
17
+ from spawnllm.response import Response
18
+ from spawnllm.spec import RunSpec
19
+ from spawnllm.types import ProviderName, TModel
20
+
21
+
22
+ class OpenAiEndpointBackend(LlmBackend):
23
+ """`LlmBackend` that POSTs to an OpenAI-compatible `/chat/completions` endpoint.
24
+
25
+ Unlike the CLI backends this drives a raw `httpx` request rather than a
26
+ subprocess, and it is never auto-selected; the consumer constructs it
27
+ explicitly with a `base_url` and a literal `model`. Every abstract tier maps
28
+ to that one pinned `model`, and `RunSpec.model` is ignored at request time —
29
+ the endpoint always serves the constructor's `model`. Structured output rides
30
+ on `response_format` with a strict `json_schema`; text output reads
31
+ `choices[0].message.content`.
32
+
33
+ Args:
34
+ base_url: Root URL of the server; `/chat/completions` is appended.
35
+ model: The literal model id sent in every request body.
36
+ api_key: Bearer token for the `Authorization` header; defaults to
37
+ `"local"` for self-hosted servers that ignore it.
38
+ transport: Async transport injected into the `httpx.AsyncClient` used by
39
+ `aexecute` — e.g. a record/replay caching transport; `None` uses
40
+ httpx's default transport. The synchronous `execute` path always uses
41
+ the default transport.
42
+
43
+ Example:
44
+ >>> backend = OpenAiEndpointBackend("http://localhost:8000/v1", "qwen3")
45
+ >>> backend.execute(RunSpec(prompt="ping", model="qwen3"))
46
+ """
47
+
48
+ provider: ClassVar[ProviderName] = "openai_endpoint"
49
+
50
+ def __init__(
51
+ self, base_url: str, model: str, *, api_key: str = "local", transport: httpx.AsyncBaseTransport | None = None
52
+ ) -> None:
53
+ self.base_url = base_url.rstrip("/")
54
+ self.model = model
55
+ self.api_key = api_key
56
+ self.transport = transport
57
+ self.models: dict[TModel, str] = {"small": model, "medium": model, "large": model}
58
+
59
+ @property
60
+ def url(self) -> str:
61
+ return f"{self.base_url}/chat/completions"
62
+
63
+ def headers(self) -> dict[str, str]:
64
+ return {"Authorization": f"Bearer {self.api_key}"}
65
+
66
+ def payload(self, spec: RunSpec) -> dict[str, object]:
67
+ base: dict[str, object] = {"model": self.model, "messages": [{"role": "user", "content": spec.prompt}]}
68
+ if (schema := self.schema_arg(spec)) is None:
69
+ return base
70
+ json_schema = {"name": "response", "strict": True, "schema": json.loads(schema)}
71
+ return base | {"response_format": {"type": "json_schema", "json_schema": json_schema}}
72
+
73
+ def resolve(self, resp: httpx.Response, spec: RunSpec) -> Response:
74
+ return self.to_response(
75
+ resp.text,
76
+ returncode=0 if resp.is_success else resp.status_code,
77
+ stderr="" if resp.is_success else resp.text,
78
+ spec=spec,
79
+ )
80
+
81
+ async def aexecute(self, spec: RunSpec) -> Response:
82
+ async with httpx.AsyncClient(timeout=spec.timeout, transport=self.transport) as client:
83
+ resp = await client.post(self.url, headers=self.headers(), json=self.payload(spec))
84
+ return self.resolve(resp, spec)
85
+
86
+ def execute(self, spec: RunSpec) -> Response:
87
+ with httpx.Client(timeout=spec.timeout) as client:
88
+ resp = client.post(self.url, headers=self.headers(), json=self.payload(spec))
89
+ return self.resolve(resp, spec)
90
+
91
+ def schema_for(self, model: type[BaseModel]) -> str:
92
+ """Serialize a Pydantic model into an OpenAI strict JSON schema for `response_format`.
93
+
94
+ Uses the OpenAI SDK's `to_strict_json_schema`, which recursively sets
95
+ `additionalProperties: false` and forces every property into `required`
96
+ across `$defs`, `anyOf`, and array items — the form a `json_schema`
97
+ `response_format` requires.
98
+
99
+ Args:
100
+ model: The Pydantic model describing the structured output.
101
+
102
+ Returns:
103
+ A strict JSON-schema string embedded in the request body.
104
+ """
105
+ from openai.lib._pydantic import to_strict_json_schema
106
+
107
+ return json.dumps(to_strict_json_schema(model))
108
+
109
+ def result_text(self, raw: str) -> str:
110
+ """Return `choices[0].message.content` from the chat-completion response body."""
111
+ return json.loads(raw)["choices"][0]["message"]["content"]
112
+
113
+ def result_value(self, raw: str) -> object:
114
+ """Return the JSON value parsed from the message content, tolerating fences or surrounding prose."""
115
+ return json.loads(extract_json_block(self.result_text(raw)))
116
+
117
+ def envelope_error(self, raw: str) -> str | None:
118
+ """Return the message from an OpenAI `{"error": {...}}` body carried on a 2xx response, else `None`."""
119
+ match json.loads(raw):
120
+ case {"error": {"message": str(msg)}}:
121
+ return msg
122
+ case {"error": str(msg)}:
123
+ return msg
124
+ case _:
125
+ return None
126
+
127
+ def env(self, _spec: RunSpec) -> dict[str, str]:
128
+ """Return no extra environment variables; the endpoint is reached over HTTP with nothing to isolate."""
129
+ return {}
130
+
131
+ def is_authenticated(self, *, timeout: int) -> bool:
132
+ """Report `True`; credentials travel inline as the `Authorization` bearer token on every request."""
133
+ return True
134
+
135
+ def check_status(self, *, timeout: int = 10) -> BackendStatus:
136
+ """Report `BackendReady`; the endpoint is reached per-request and carries its own auth."""
137
+ return BackendReady(binary="openai_endpoint")
@@ -12,5 +12,5 @@ TSpecialty = Literal["debugging", "review", "general"]
12
12
  TModel = Literal["small", "medium", "large"]
13
13
  """Abstract model tier; each backend maps it to a provider-specific model name."""
14
14
 
15
- ProviderName = Literal["claude", "codex", "gemini", "antigravity", "mlx"]
15
+ ProviderName = Literal["claude", "codex", "gemini", "antigravity", "mlx", "openai_endpoint"]
16
16
  """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