polyllmkit 0.1.0__tar.gz → 0.1.2__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 (27) hide show
  1. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/PKG-INFO +6 -1
  2. polyllmkit-0.1.2/pyproject.toml +37 -0
  3. polyllmkit-0.1.0/pyproject.toml → polyllmkit-0.1.2/pyproject.toml.orig +9 -1
  4. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/adapters/anthropic.py +8 -8
  5. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/adapters/claude_code.py +32 -20
  6. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/adapters/google.py +4 -7
  7. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/bridge.py +9 -9
  8. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/cli.py +6 -0
  9. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/client.py +4 -7
  10. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/__init__.py +0 -0
  11. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/__init__.py +0 -0
  12. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/__main__.py +0 -0
  13. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/adapters/__init__.py +0 -0
  14. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/adapters/openai.py +0 -0
  15. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/config.py +0 -0
  16. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/sinks.py +0 -0
  17. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/bridge/stream.py +0 -0
  18. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/__init__.py +0 -0
  19. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/render/__init__.py +0 -0
  20. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/render/__main__.py +0 -0
  21. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/render/cli.py +0 -0
  22. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/render/stream.py +0 -0
  23. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/view/__init__.py +0 -0
  24. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/view/__main__.py +0 -0
  25. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/view/app.py +0 -0
  26. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/md/view/cli.py +0 -0
  27. {polyllmkit-0.1.0 → polyllmkit-0.1.2}/src/llmkit/py.typed +0 -0
@@ -1,7 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: polyllmkit
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Streaming, multi-provider LLM bridge + markdown rendering toolkit for terminal apps.
5
+ Requires-Dist: openai>=1.0 ; extra == 'all'
6
+ Requires-Dist: claude-agent-sdk>=0.2 ; extra == 'all'
7
+ Requires-Dist: anthropic>=0.40 ; extra == 'all'
8
+ Requires-Dist: google-genai>=1.0 ; extra == 'all'
5
9
  Requires-Dist: anthropic>=0.40 ; extra == 'anthropic'
6
10
  Requires-Dist: openai>=1.0 ; extra == 'bridge'
7
11
  Requires-Dist: claude-agent-sdk>=0.2 ; extra == 'claude'
@@ -10,6 +14,7 @@ Requires-Dist: rich>=13.0 ; extra == 'md'
10
14
  Requires-Dist: textual>=0.50 ; extra == 'md'
11
15
  Requires-Dist: markdown-it-py>=3.0 ; extra == 'md'
12
16
  Requires-Python: >=3.11
17
+ Provides-Extra: all
13
18
  Provides-Extra: anthropic
14
19
  Provides-Extra: bridge
15
20
  Provides-Extra: claude
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "polyllmkit"
3
+ version = "0.1.2"
4
+ description = "Streaming, multi-provider LLM bridge + markdown rendering toolkit for terminal apps."
5
+ requires-python = ">=3.11"
6
+ dependencies = []
7
+
8
+ [project.optional-dependencies]
9
+ bridge = ["openai>=1.0"]
10
+ claude = ["claude-agent-sdk>=0.2"]
11
+ anthropic = ["anthropic>=0.40"]
12
+ google = ["google-genai>=1.0"]
13
+ all = [
14
+ "openai>=1.0",
15
+ "claude-agent-sdk>=0.2",
16
+ "anthropic>=0.40",
17
+ "google-genai>=1.0",
18
+ ]
19
+ md = [
20
+ "rich>=13.0",
21
+ "textual>=0.50",
22
+ "markdown-it-py>=3.0",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["uv_build>=0.5,<0.12"]
27
+ build-backend = "uv_build"
28
+
29
+ [tool.uv.build-backend]
30
+ module-root = "src"
31
+ module-name = "llmkit"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "ruff>=0.6",
36
+ "mypy>=1.10",
37
+ ]
@@ -4,7 +4,7 @@
4
4
  # module-name below): consumers keep `import llmkit`, `pip install polyllmkit`.
5
5
  [project]
6
6
  name = "polyllmkit"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Streaming, multi-provider LLM bridge + markdown rendering toolkit for terminal apps."
9
9
  requires-python = ">=3.11" # tomllib (bridge.config) is stdlib only from 3.11
10
10
  # Base install pulls nothing — pick the subpackage(s) you need via extras.
@@ -21,6 +21,14 @@ bridge = ["openai>=1.0"]
21
21
  claude = ["claude-agent-sdk>=0.2"] # claude_code adapter (Claude Agent SDK)
22
22
  anthropic = ["anthropic>=0.40"] # anthropic adapter (native Messages API)
23
23
  google = ["google-genai>=1.0"] # google adapter (native Gemini API)
24
+ # Every backend at once — for hosts that resolve the adapter at runtime and
25
+ # must have all of them present (e.g. cribsheet's daemon).
26
+ all = [
27
+ "openai>=1.0",
28
+ "claude-agent-sdk>=0.2",
29
+ "anthropic>=0.40",
30
+ "google-genai>=1.0",
31
+ ]
24
32
  # llmkit.md — markdown rendering (one-shot + live stream + scrollable modal).
25
33
  # markdown-it-py is a direct import in md.render.stream (textual also pulls
26
34
  # it, but we depend on it explicitly since we use it ourselves).
@@ -29,7 +29,6 @@ openai-compatible path never requires it.
29
29
  from __future__ import annotations
30
30
 
31
31
  import os
32
- import sys
33
32
  from typing import Any, Optional
34
33
  from urllib.parse import urlsplit, urlunsplit
35
34
 
@@ -57,17 +56,18 @@ def _base_url(endpoint: str) -> str:
57
56
 
58
57
 
59
58
  def _require_sdk() -> Any:
60
- """Import the SDK or exit(2) with an actionable install hint."""
59
+ """Import the SDK, or raise ImportError with an actionable install hint.
60
+
61
+ Catchable exception, never ``SystemExit`` — see the note in
62
+ ``claude_code._require_sdk``."""
61
63
  try:
62
64
  import anthropic # type: ignore[import-not-found]
63
- except ImportError:
64
- print(
65
+ except ImportError as e:
66
+ raise ImportError(
65
67
  "llmkit: the anthropic adapter needs the Anthropic SDK. "
66
68
  "Install it with `pip install llmkit[anthropic]` (or `pip install "
67
- "anthropic`).",
68
- file=sys.stderr,
69
- )
70
- raise SystemExit(2)
69
+ "anthropic`)."
70
+ ) from e
71
71
  return anthropic
72
72
 
73
73
 
@@ -18,7 +18,6 @@ CLI governs those itself. Only ``model``, the request's ``system``, and
18
18
  from __future__ import annotations
19
19
 
20
20
  import os
21
- import sys
22
21
  from typing import Any
23
22
 
24
23
  from ..bridge import ChatRequest, _Emitter
@@ -26,17 +25,21 @@ from ..config import Provider
26
25
 
27
26
 
28
27
  def _require_sdk() -> Any:
29
- """Import the SDK or exit(2) with an actionable install hint."""
28
+ """Import the SDK, or raise ImportError with an actionable install hint.
29
+
30
+ A missing optional dependency must surface as a *catchable* exception, never
31
+ ``SystemExit``: llmkit is a library, and hosts embed it inside long-lived
32
+ processes (e.g. an MCP daemon's lifespan startup) where a stray ``SystemExit``
33
+ — a ``BaseException`` — escapes ``except Exception`` and tears the whole
34
+ process down. The CLI translates this back into a clean exit(2)."""
30
35
  try:
31
36
  import claude_agent_sdk as sdk # type: ignore[import-not-found]
32
- except ImportError:
33
- print(
37
+ except ImportError as e:
38
+ raise ImportError(
34
39
  "llmkit: the claude_code adapter needs the Claude Agent SDK. "
35
40
  "Install it with `pip install llmkit[claude]` (or `pip install "
36
- "claude-agent-sdk`), and make sure the `claude` CLI is on PATH.",
37
- file=sys.stderr,
38
- )
39
- raise SystemExit(2)
41
+ "claude-agent-sdk`), and make sure the `claude` CLI is on PATH."
42
+ ) from e
40
43
  return sdk
41
44
 
42
45
 
@@ -55,15 +58,15 @@ def _thinking_budget() -> int:
55
58
 
56
59
  def _build_options(sdk: Any, provider: Provider, request: ChatRequest) -> Any:
57
60
  """Assemble ClaudeAgentOptions for a tool-free single-turn chat."""
58
- opts: dict[str, Any] = dict(
59
- tools=[], # no built-in tools — plain chat, no file/shell access
60
- allowed_tools=[],
61
- disallowed_tools=[],
62
- max_turns=1,
63
- permission_mode="bypassPermissions",
64
- include_partial_messages=True, # token-level streaming deltas
65
- setting_sources=[], # ignore project/user CLAUDE.md, settings, etc.
66
- )
61
+ opts: dict[str, Any] = {
62
+ "tools": [], # no built-in tools — plain chat, no file/shell access
63
+ "allowed_tools": [],
64
+ "disallowed_tools": [],
65
+ "max_turns": 1,
66
+ "permission_mode": "bypassPermissions",
67
+ "include_partial_messages": True, # token-level streaming deltas
68
+ "setting_sources": [], # ignore project/user CLAUDE.md, settings, etc.
69
+ }
67
70
  if provider.model:
68
71
  opts["model"] = provider.model
69
72
  if request.system:
@@ -84,13 +87,22 @@ def _build_options(sdk: Any, provider: Provider, request: ChatRequest) -> Any:
84
87
  opts["thinking"] = {"type": "disabled"}
85
88
  return sdk.ClaudeAgentOptions(**opts)
86
89
 
90
+ # ``display: "summarized"`` is required on current models: they default it
91
+ # to ``omitted``, which silences thinking entirely — no deltas stream and
92
+ # the final ThinkingBlock is empty (verified: enabled+budget without display
93
+ # streams 0 chars of thinking; with display, ~tokens arrive). Mirrors the
94
+ # anthropic adapter's same-named field.
87
95
  flag = provider.enable_thinking or "auto"
88
96
  if flag == "false":
89
97
  opts["thinking"] = {"type": "disabled"}
90
98
  elif flag == "true":
91
- opts["thinking"] = {"type": "enabled", "budget_tokens": _thinking_budget()}
99
+ opts["thinking"] = {
100
+ "type": "enabled",
101
+ "budget_tokens": _thinking_budget(),
102
+ "display": "summarized",
103
+ }
92
104
  else:
93
- opts["thinking"] = {"type": "adaptive"}
105
+ opts["thinking"] = {"type": "adaptive", "display": "summarized"}
94
106
 
95
107
  return sdk.ClaudeAgentOptions(**opts)
96
108
 
@@ -143,7 +155,7 @@ async def _run(
143
155
 
144
156
  def structured_claude_code(
145
157
  provider: Provider, request: ChatRequest
146
- ) -> "tuple[int, Any]":
158
+ ) -> tuple[int, Any]:
147
159
  """Structured output via the SDK's native ``output_format`` (set in
148
160
  ``_build_options`` when the request carries a schema). Runs the single-turn
149
161
  query and returns ``(exit_code, structured_output)`` from the ResultMessage —
@@ -20,7 +20,6 @@ openai-compatible path never requires it.
20
20
  from __future__ import annotations
21
21
 
22
22
  import os
23
- import sys
24
23
  from typing import Any, Optional
25
24
 
26
25
  from ..bridge import DEFAULT_ENDPOINT, ChatRequest, _Emitter
@@ -33,14 +32,12 @@ def _require_sdk() -> Any:
33
32
  try:
34
33
  from google import genai # type: ignore[import-untyped,import-not-found]
35
34
  from google.genai import types # type: ignore[import-untyped,import-not-found]
36
- except ImportError:
37
- print(
35
+ except ImportError as e:
36
+ raise ImportError(
38
37
  "llmkit: the google adapter needs the Google GenAI SDK. "
39
38
  "Install it with `pip install llmkit[google]` (or `pip install "
40
- "google-genai`).",
41
- file=sys.stderr,
42
- )
43
- raise SystemExit(2)
39
+ "google-genai`)."
40
+ ) from e
44
41
  return genai, types
45
42
 
46
43
 
@@ -336,7 +336,7 @@ def complete(
336
336
  ) -> int:
337
337
  """Run a text-completion (FIM) request, streaming text to ``out``
338
338
  (default stdout). ``openai-compatible`` only."""
339
- out = out if out is not None else sys.stdout
339
+ sink = out if out is not None else sys.stdout
340
340
  provider = with_defaults(provider)
341
341
  if not provider_supports_complete(provider):
342
342
  print(
@@ -349,12 +349,12 @@ def complete(
349
349
  return 2
350
350
 
351
351
  client = build_client(provider)
352
- kwargs: dict[str, Any] = dict(
353
- model=provider.model,
354
- prompt=request.prompt,
355
- max_tokens=provider.max_tokens,
356
- temperature=provider.temperature,
357
- )
352
+ kwargs: dict[str, Any] = {
353
+ "model": provider.model,
354
+ "prompt": request.prompt,
355
+ "max_tokens": provider.max_tokens,
356
+ "temperature": provider.temperature,
357
+ }
358
358
  if request.suffix:
359
359
  kwargs["suffix"] = request.suffix
360
360
  if request.stop:
@@ -364,8 +364,8 @@ def complete(
364
364
  for chunk in client.completions.create(**kwargs):
365
365
  t = chunk.choices[0].text if chunk.choices else ""
366
366
  if t:
367
- out.write(t)
368
- out.flush()
367
+ sink.write(t)
368
+ sink.flush()
369
369
  except KeyboardInterrupt:
370
370
  return 130
371
371
  except Exception as e:
@@ -172,3 +172,9 @@ if __name__ == "__main__":
172
172
  sys.exit(main())
173
173
  except BrokenPipeError:
174
174
  sys.exit(0)
175
+ except ImportError as e:
176
+ # A missing optional-adapter dependency. The adapters raise a catchable
177
+ # ImportError (never SystemExit, so library hosts survive); the CLI turns
178
+ # it back into the actionable message + exit(2).
179
+ print(e, file=sys.stderr)
180
+ sys.exit(2)
@@ -10,7 +10,6 @@ from __future__ import annotations
10
10
 
11
11
  import os
12
12
  import re
13
- import sys
14
13
  import time
15
14
  import uuid
16
15
  from typing import Any, Callable, Dict, Optional
@@ -55,14 +54,12 @@ def build_client(provider: Provider) -> Any:
55
54
  hard import of ``openai.OpenAI`` at module load."""
56
55
  try:
57
56
  from openai import OpenAI
58
- except ImportError:
59
- print(
57
+ except ImportError as e:
58
+ raise ImportError(
60
59
  "llmkit: the openai package is not installed. "
61
60
  "Install it with `pip install llmkit[bridge]` (or `pip install "
62
- "openai`).",
63
- file=sys.stderr,
64
- )
65
- raise SystemExit(2)
61
+ "openai`)."
62
+ ) from e
66
63
  return OpenAI(
67
64
  base_url=provider.endpoint,
68
65
  api_key=_resolve_api_key(provider),