innards 0.1.0__py3-none-any.whl

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.
innards/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Innards: see inside open-weight models, one message at a time.
2
+
3
+ >>> from innards import Session, AsyncSession, compare
4
+ >>> from innards.kv import predict
5
+ >>> from innards.lineage import explain
6
+ >>> from innards.schema import TurnRecord
7
+ """
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ try:
12
+ __version__ = version("innards")
13
+ except PackageNotFoundError: # pragma: no cover - running from a source tree
14
+ __version__ = "0.0.0"
15
+
16
+ from innards.schema import Turn, TurnRecord
17
+ from innards.session import AsyncSession, CompareResult, Session, compare
18
+
19
+ __all__ = ["AsyncSession", "CompareResult", "Session", "Turn", "TurnRecord", "__version__", "compare"]
innards/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """``python -m innards`` runs the CLI."""
2
+
3
+ import sys
4
+
5
+ from innards.cli import main
6
+
7
+ sys.exit(main())
innards/_text.py ADDED
@@ -0,0 +1,76 @@
1
+ """Incremental detokenization for streaming, with stop-string hold-back."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Sequence
6
+
7
+ __all__ = ["IncrementalDecoder", "strip_stop"]
8
+
9
+
10
+ def strip_stop(text: str, stop_strings: Sequence[str]) -> tuple[str, bool]:
11
+ """Cut ``text`` at the first stop string. Returns ``(text, stopped)``."""
12
+ cut = min((i for i in (text.find(s) for s in stop_strings if s) if i >= 0), default=-1)
13
+ return (text[:cut], True) if cut >= 0 else (text, False)
14
+
15
+
16
+ class IncrementalDecoder:
17
+ """Turn a growing list of token ids into text deltas.
18
+
19
+ Decodes a short window (prefix/read offsets) instead of the whole sequence,
20
+ holds back incomplete UTF-8 sequences, and holds back any tail that could be
21
+ the start of a stop string.
22
+ """
23
+
24
+ def __init__(self, decode: Callable[[list[int]], str], stop_strings: Sequence[str] = ()) -> None:
25
+ self._decode = decode
26
+ self._stop = [s for s in stop_strings if s]
27
+ self._ids: list[int] = []
28
+ self._prefix = 0
29
+ self._read = 0
30
+ self._decoded = "" # all text decoded so far
31
+ self._released = 0 # characters of _decoded handed to the caller
32
+ self.stopped = False
33
+
34
+ @property
35
+ def text(self) -> str:
36
+ """Full text so far, cut at the first stop string."""
37
+ return strip_stop(self._decoded, self._stop)[0]
38
+
39
+ def _hold(self, text: str) -> int:
40
+ """Length of the longest suffix of ``text`` that is a proper prefix of a stop string."""
41
+ best = 0
42
+ for stop in self._stop:
43
+ for n in range(min(len(stop) - 1, len(text)), 0, -1):
44
+ if text.endswith(stop[:n]):
45
+ best = max(best, n)
46
+ break
47
+ return best
48
+
49
+ def push(self, ids: Sequence[int]) -> str:
50
+ if self.stopped:
51
+ return ""
52
+ self._ids.extend(ids)
53
+ prefix_text = self._decode(self._ids[self._prefix : self._read])
54
+ new_text = self._decode(self._ids[self._prefix :])
55
+ if len(new_text) > len(prefix_text) and not new_text.endswith("�"):
56
+ self._decoded += new_text[len(prefix_text) :]
57
+ self._prefix, self._read = self._read, len(self._ids)
58
+ return self._release(final=False)
59
+
60
+ def flush(self) -> str:
61
+ if not self.stopped and self._read < len(self._ids):
62
+ tail = self._decode(self._ids[self._prefix :])[len(self._decode(self._ids[self._prefix : self._read])) :]
63
+ self._decoded += tail
64
+ self._read = len(self._ids)
65
+ return self._release(final=True)
66
+
67
+ def _release(self, final: bool) -> str:
68
+ text, stopped = strip_stop(self._decoded, self._stop)
69
+ if stopped:
70
+ self.stopped = True
71
+ end = len(text)
72
+ else:
73
+ end = len(text) if final else len(text) - self._hold(text)
74
+ out = text[self._released : end] if end > self._released else ""
75
+ self._released = max(self._released, end)
76
+ return out
@@ -0,0 +1,39 @@
1
+ """Engine adapters. ``hf`` (in-process Hugging Face Transformers) ships in this release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from innards.backends.base import (
8
+ Backend,
9
+ ContextFullError,
10
+ KVReading,
11
+ PreparedTurn,
12
+ available_backends,
13
+ get_backend,
14
+ register_backend,
15
+ )
16
+
17
+ __all__ = [
18
+ "Backend",
19
+ "ContextFullError",
20
+ "KVReading",
21
+ "PreparedTurn",
22
+ "available_backends",
23
+ "get_backend",
24
+ "register_backend",
25
+ ]
26
+
27
+
28
+ def _hf_factory(model: str, **kwargs: Any) -> Backend:
29
+ try:
30
+ from innards.backends.hf import HFBackend
31
+ except ImportError as exc: # torch / transformers missing
32
+ raise ImportError(
33
+ "The 'hf' backend needs torch and transformers. Install them with: "
34
+ "pip install 'innards[hf]' (or: uv add 'innards[hf]')"
35
+ ) from exc
36
+ return HFBackend(model, **kwargs)
37
+
38
+
39
+ register_backend("hf", _hf_factory)
@@ -0,0 +1,156 @@
1
+ """Backend interface. HF Transformers ships in this release; vLLM, Ollama and API backends plug in here."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Protocol, runtime_checkable
9
+
10
+ from innards.kv.config import ModelSpec
11
+ from innards.observe.events import TurnEnd, TurnKey, TurnStats
12
+ from innards.schema import MemoryInfo
13
+
14
+ __all__ = [
15
+ "Backend",
16
+ "ContextFullError",
17
+ "Deliver",
18
+ "Emit",
19
+ "KVReading",
20
+ "PreparedTurn",
21
+ "available_backends",
22
+ "get_backend",
23
+ "register_backend",
24
+ ]
25
+
26
+ Deliver = Callable[[Any], None]
27
+ """Receives each chunk of generated token ids for the caller's stream (output path)."""
28
+ Emit = Callable[[int, TurnKey | None, Any], bool]
29
+ """Observer hot-path hook: ``emit(kind, key, data)``; must never block."""
30
+
31
+
32
+ class ContextFullError(RuntimeError):
33
+ """The prompt plus ``max_new_tokens`` would exceed the model's context window."""
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class PreparedTurn:
38
+ """A rendered prompt and the cache-reuse decision for it."""
39
+
40
+ input_ids: list[int]
41
+ cached_tokens: int
42
+ """Prompt tokens already resident in the kept cache (prefix reuse)."""
43
+ prefix_reused: bool
44
+ extra: dict[str, Any] = field(default_factory=dict)
45
+
46
+ @property
47
+ def prompt_tokens(self) -> int:
48
+ return len(self.input_ids)
49
+
50
+ @property
51
+ def recomputed_tokens(self) -> int:
52
+ return self.prompt_tokens - self.cached_tokens
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class KVReading:
57
+ """Measured KV, produced in an observer worker from a backend snapshot."""
58
+
59
+ total_bytes: int
60
+ per_layer_bytes: tuple[int, ...]
61
+ by_layer_type: dict[str, int]
62
+ source: str
63
+ layer_tokens: tuple[int | None, ...] = ()
64
+
65
+
66
+ @runtime_checkable
67
+ class Backend(Protocol):
68
+ """What a Session needs from an engine adapter."""
69
+
70
+ name: str
71
+ model_id: str
72
+
73
+ def load(self) -> None: ...
74
+
75
+ @property
76
+ def spec(self) -> ModelSpec: ...
77
+
78
+ @property
79
+ def dtype(self) -> str | None: ...
80
+
81
+ @property
82
+ def kv_dtype(self) -> str: ...
83
+
84
+ @property
85
+ def max_new_tokens(self) -> int: ...
86
+
87
+ def runtime_facts(self) -> dict[str, Any]:
88
+ """``host``, ``device``, ``engine_version`` for RuntimeInfo."""
89
+ ...
90
+
91
+ def prepare(self, messages: list[dict[str, str]]) -> PreparedTurn:
92
+ """Render the conversation and decide how much of the kept cache to reuse."""
93
+ ...
94
+
95
+ def generate(
96
+ self,
97
+ prepared: PreparedTurn,
98
+ *,
99
+ deliver: Deliver | None = None,
100
+ emit: Emit | None = None,
101
+ key: TurnKey | None = None,
102
+ cancel: threading.Event | None = None,
103
+ ) -> tuple[TurnEnd, list[int]]:
104
+ """Run one turn in the calling thread. Returns raw end-of-turn facts and generated ids."""
105
+ ...
106
+
107
+ def decode(self, token_ids: list[int]) -> str: ...
108
+
109
+ def final_text(self, token_ids: list[int]) -> str:
110
+ """Text stored in history and the record (stop strings removed, stripped)."""
111
+ ...
112
+
113
+ @property
114
+ def stop_strings(self) -> tuple[str, ...]: ...
115
+
116
+ def reset(self) -> None:
117
+ """Drop the kept cache (next turn prefills from scratch)."""
118
+ ...
119
+
120
+ def measure_kv(self, snapshot: Any) -> KVReading | None:
121
+ """Turn a cache snapshot into bytes. Runs in an observer worker."""
122
+ ...
123
+
124
+ def memory_sampler(self) -> Callable[[], int | None] | None: ...
125
+
126
+ def memory_info(self, end: TurnEnd, stats: TurnStats, kv_bytes: int | None) -> MemoryInfo:
127
+ """Memory split for a finished turn. Runs in an observer worker."""
128
+ ...
129
+
130
+ def kv_memory_budget(self) -> int | None:
131
+ """Bytes available for KV, for capacity projections."""
132
+ ...
133
+
134
+ def composition(self, messages: list[dict[str, str]], prompt_tokens: int) -> dict[str, int | None]:
135
+ """Token counts for system / history / new message. Runs in an observer worker."""
136
+ ...
137
+
138
+ def close(self) -> None: ...
139
+
140
+
141
+ _BACKENDS: dict[str, Callable[..., Backend]] = {}
142
+
143
+
144
+ def register_backend(name: str, factory: Callable[..., Backend]) -> None:
145
+ _BACKENDS[name] = factory
146
+
147
+
148
+ def available_backends() -> list[str]:
149
+ return sorted(_BACKENDS)
150
+
151
+
152
+ def get_backend(name: str, model: str, **kwargs: Any) -> Backend:
153
+ """Create a backend by name (``"hf"``)."""
154
+ if name not in _BACKENDS:
155
+ raise ValueError(f"Unknown backend {name!r}. Available: {', '.join(available_backends())}")
156
+ return _BACKENDS[name](model, **kwargs)