aimai-kit 1.0.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 (107) hide show
  1. aimai_kit-1.0.0/.gitignore +16 -0
  2. aimai_kit-1.0.0/LICENSE +21 -0
  3. aimai_kit-1.0.0/PKG-INFO +238 -0
  4. aimai_kit-1.0.0/README.md +208 -0
  5. aimai_kit-1.0.0/config/pricing.toml +44 -0
  6. aimai_kit-1.0.0/docs/01-provider.md +129 -0
  7. aimai_kit-1.0.0/docs/02-prompts.md +192 -0
  8. aimai_kit-1.0.0/docs/03-tools.md +194 -0
  9. aimai_kit-1.0.0/docs/04-agent.md +160 -0
  10. aimai_kit-1.0.0/docs/05-harness.md +215 -0
  11. aimai_kit-1.0.0/docs/measurements.md +223 -0
  12. aimai_kit-1.0.0/prompts/compaction@v1.md +3 -0
  13. aimai_kit-1.0.0/prompts/compaction@v2.md +57 -0
  14. aimai_kit-1.0.0/prompts/extract_contract@v1.md +4 -0
  15. aimai_kit-1.0.0/prompts/extract_contract@v2.md +74 -0
  16. aimai_kit-1.0.0/prompts/summarize@v1.md +7 -0
  17. aimai_kit-1.0.0/prompts/summarize@v2.md +53 -0
  18. aimai_kit-1.0.0/pyproject.toml +82 -0
  19. aimai_kit-1.0.0/src/aimai_kit/__init__.py +1 -0
  20. aimai_kit-1.0.0/src/aimai_kit/agent/__init__.py +28 -0
  21. aimai_kit-1.0.0/src/aimai_kit/agent/budget.py +118 -0
  22. aimai_kit-1.0.0/src/aimai_kit/agent/loop.py +296 -0
  23. aimai_kit-1.0.0/src/aimai_kit/agent/loopdetect.py +70 -0
  24. aimai_kit-1.0.0/src/aimai_kit/agent/metrics.py +112 -0
  25. aimai_kit-1.0.0/src/aimai_kit/agent/scripted.py +72 -0
  26. aimai_kit-1.0.0/src/aimai_kit/agent/thread.py +142 -0
  27. aimai_kit-1.0.0/src/aimai_kit/harness/__init__.py +38 -0
  28. aimai_kit-1.0.0/src/aimai_kit/harness/compaction.py +126 -0
  29. aimai_kit-1.0.0/src/aimai_kit/harness/context.py +133 -0
  30. aimai_kit-1.0.0/src/aimai_kit/harness/memory.py +230 -0
  31. aimai_kit-1.0.0/src/aimai_kit/harness/sandbox.py +97 -0
  32. aimai_kit-1.0.0/src/aimai_kit/harness/segments.py +112 -0
  33. aimai_kit-1.0.0/src/aimai_kit/harness/spill.py +134 -0
  34. aimai_kit-1.0.0/src/aimai_kit/harness/stub_summarizer.py +81 -0
  35. aimai_kit-1.0.0/src/aimai_kit/harness/subagent.py +162 -0
  36. aimai_kit-1.0.0/src/aimai_kit/prompts/__init__.py +27 -0
  37. aimai_kit-1.0.0/src/aimai_kit/prompts/blocks.py +89 -0
  38. aimai_kit-1.0.0/src/aimai_kit/prompts/budget.py +238 -0
  39. aimai_kit-1.0.0/src/aimai_kit/prompts/cli.py +448 -0
  40. aimai_kit-1.0.0/src/aimai_kit/prompts/grounding.py +155 -0
  41. aimai_kit-1.0.0/src/aimai_kit/prompts/guard.py +77 -0
  42. aimai_kit-1.0.0/src/aimai_kit/prompts/pipeline.py +96 -0
  43. aimai_kit-1.0.0/src/aimai_kit/prompts/registry.py +171 -0
  44. aimai_kit-1.0.0/src/aimai_kit/prompts/schemas.py +247 -0
  45. aimai_kit-1.0.0/src/aimai_kit/prompts/structured.py +196 -0
  46. aimai_kit-1.0.0/src/aimai_kit/prompts/stub.py +203 -0
  47. aimai_kit-1.0.0/src/aimai_kit/provider/__init__.py +1 -0
  48. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/__init__.py +51 -0
  49. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/_shared.py +37 -0
  50. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/anthropic_.py +185 -0
  51. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/azure_openai.py +64 -0
  52. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/gemini_.py +162 -0
  53. aimai_kit-1.0.0/src/aimai_kit/provider/adapters/openai_.py +187 -0
  54. aimai_kit-1.0.0/src/aimai_kit/provider/cli/__init__.py +1 -0
  55. aimai_kit-1.0.0/src/aimai_kit/provider/cli/probe.py +313 -0
  56. aimai_kit-1.0.0/src/aimai_kit/provider/client.py +45 -0
  57. aimai_kit-1.0.0/src/aimai_kit/provider/counters.py +62 -0
  58. aimai_kit-1.0.0/src/aimai_kit/provider/counting.py +135 -0
  59. aimai_kit-1.0.0/src/aimai_kit/provider/determinism.py +63 -0
  60. aimai_kit-1.0.0/src/aimai_kit/provider/errors.py +101 -0
  61. aimai_kit-1.0.0/src/aimai_kit/provider/pricing.py +129 -0
  62. aimai_kit-1.0.0/src/aimai_kit/provider/resilient.py +229 -0
  63. aimai_kit-1.0.0/src/aimai_kit/provider/telemetry.py +147 -0
  64. aimai_kit-1.0.0/src/aimai_kit/provider/types.py +192 -0
  65. aimai_kit-1.0.0/src/aimai_kit/py.typed +0 -0
  66. aimai_kit-1.0.0/src/aimai_kit/tools/__init__.py +36 -0
  67. aimai_kit-1.0.0/src/aimai_kit/tools/decorator.py +175 -0
  68. aimai_kit-1.0.0/src/aimai_kit/tools/evaluation.py +227 -0
  69. aimai_kit-1.0.0/src/aimai_kit/tools/examples/__init__.py +12 -0
  70. aimai_kit-1.0.0/src/aimai_kit/tools/examples/orders.py +237 -0
  71. aimai_kit-1.0.0/src/aimai_kit/tools/executor.py +334 -0
  72. aimai_kit-1.0.0/src/aimai_kit/tools/export.py +111 -0
  73. aimai_kit-1.0.0/src/aimai_kit/tools/idempotency.py +86 -0
  74. aimai_kit-1.0.0/src/aimai_kit/tools/registry.py +74 -0
  75. aimai_kit-1.0.0/src/aimai_kit/tools/spec.py +97 -0
  76. aimai_kit-1.0.0/tests/conftest.py +134 -0
  77. aimai_kit-1.0.0/tests/test_adapter_contract.py +196 -0
  78. aimai_kit-1.0.0/tests/test_agent_budgets.py +138 -0
  79. aimai_kit-1.0.0/tests/test_agent_loop.py +177 -0
  80. aimai_kit-1.0.0/tests/test_agent_metrics.py +80 -0
  81. aimai_kit-1.0.0/tests/test_budget.py +121 -0
  82. aimai_kit-1.0.0/tests/test_budget_guard.py +44 -0
  83. aimai_kit-1.0.0/tests/test_checkpoint.py +126 -0
  84. aimai_kit-1.0.0/tests/test_compaction.py +129 -0
  85. aimai_kit-1.0.0/tests/test_counting.py +46 -0
  86. aimai_kit-1.0.0/tests/test_eval_harness.py +127 -0
  87. aimai_kit-1.0.0/tests/test_executor_gates.py +219 -0
  88. aimai_kit-1.0.0/tests/test_grounding.py +102 -0
  89. aimai_kit-1.0.0/tests/test_idempotency.py +125 -0
  90. aimai_kit-1.0.0/tests/test_injection.py +121 -0
  91. aimai_kit-1.0.0/tests/test_loop_detection.py +124 -0
  92. aimai_kit-1.0.0/tests/test_memory.py +122 -0
  93. aimai_kit-1.0.0/tests/test_no_vendor_leak.py +57 -0
  94. aimai_kit-1.0.0/tests/test_prefix_stable.py +86 -0
  95. aimai_kit-1.0.0/tests/test_pricing.py +97 -0
  96. aimai_kit-1.0.0/tests/test_registry.py +69 -0
  97. aimai_kit-1.0.0/tests/test_repair_loop.py +119 -0
  98. aimai_kit-1.0.0/tests/test_resilient.py +147 -0
  99. aimai_kit-1.0.0/tests/test_sandbox.py +84 -0
  100. aimai_kit-1.0.0/tests/test_schema_regression.py +127 -0
  101. aimai_kit-1.0.0/tests/test_segments.py +126 -0
  102. aimai_kit-1.0.0/tests/test_spill.py +102 -0
  103. aimai_kit-1.0.0/tests/test_subagent.py +151 -0
  104. aimai_kit-1.0.0/tests/test_telemetry.py +99 -0
  105. aimai_kit-1.0.0/tests/test_tool_evaluation.py +82 -0
  106. aimai_kit-1.0.0/tests/test_tool_export.py +113 -0
  107. aimai_kit-1.0.0/tests/test_tool_schema.py +117 -0
@@ -0,0 +1,16 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .coverage
8
+ htmlcov/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ evals/**/local-*.json
13
+ .DS_Store
14
+
15
+ # Local artifacts from the example tools and experiments
16
+ .aimai-*.sqlite3
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fport
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.5
2
+ Name: aimai-kit
3
+ Version: 1.0.0
4
+ Summary: A framework-free LLM engineering toolkit: provider adapters, prompt and context engineering, structured outputs, tools, and a bounded agent loop
5
+ Project-URL: Homepage, https://github.com/fport/aimai-kit
6
+ Project-URL: Repository, https://github.com/fport/aimai-kit
7
+ Project-URL: Documentation, https://github.com/fport/aimai-kit/tree/main/docs
8
+ Project-URL: Issues, https://github.com/fport/aimai-kit/issues
9
+ Author: fport
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agents,anthropic,context-engineering,gemini,llm,openai,prompt-engineering,structured-outputs,tool-calling
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: jinja2>=3.1
22
+ Requires-Dist: pydantic>=2.9
23
+ Requires-Dist: python-dotenv>=1.0
24
+ Requires-Dist: tiktoken>=0.14
25
+ Provides-Extra: providers
26
+ Requires-Dist: anthropic<2,>=1.0; extra == 'providers'
27
+ Requires-Dist: google-genai>=2.0; extra == 'providers'
28
+ Requires-Dist: openai>=3.0; extra == 'providers'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # aimai-kit
32
+
33
+ [![CI](https://github.com/fport/aimai-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/fport/aimai-kit/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/aimai-kit.svg)](https://pypi.org/project/aimai-kit/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/aimai-kit.svg)](https://pypi.org/project/aimai-kit/)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
37
+
38
+ A framework-free LLM engineering toolkit in one Python package: provider
39
+ adapters, prompt and context engineering, structured outputs, a tool layer,
40
+ a bounded agent loop, and a harness for long-running work.
41
+
42
+ Five layers, each built on the one below it, each with its own measurements.
43
+
44
+ | Layer | Package | What it adds |
45
+ |---|---|---|
46
+ | 1 | `provider/` | Token/cost/latency measurement, four adapters, retry and fallback |
47
+ | 2 | `prompts/` | Versioned prompts, context budget, structured output, a repair loop |
48
+ | 3 | `tools/` | Schemas from signatures, three provider exports, a five-gate executor |
49
+ | 4 | `agent/` | A bounded loop, four budgets, loop detection, checkpoints |
50
+ | 5 | `harness/` | Segments, spill, compaction, a memory store, sub-agents |
51
+
52
+ **343 tests**, no vendor SDK outside `provider/adapters/`, and every layer
53
+ runnable without an API key.
54
+
55
+ ---
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ pip install "aimai-kit[providers]"
61
+ ```
62
+
63
+ From source:
64
+
65
+ ```bash
66
+ uv sync --all-extras --group dev
67
+ cp .env.example .env # add your keys
68
+ uv run pytest # 343 tests, live ones excluded
69
+ ```
70
+
71
+ ## Quick start
72
+
73
+ ```python
74
+ from aimai_kit.provider.adapters import make_adapter
75
+ from aimai_kit.provider.types import ChatRequest, Message, Role
76
+
77
+ client = make_adapter("anthropic:claude-opus-5")
78
+ result = client.complete(
79
+ ChatRequest(
80
+ messages=[Message(role=Role.USER, content="Say hello.")],
81
+ system="Be brief.",
82
+ max_output_tokens=64,
83
+ )
84
+ )
85
+ print(result.text, result.usage.input_tokens, result.usage.output_tokens)
86
+ ```
87
+
88
+ Structured extraction with citation verification:
89
+
90
+ ```python
91
+ from aimai_kit.prompts import PromptRegistry, build_request, generate_structured
92
+ from aimai_kit.prompts.grounding import verify_citations
93
+ from aimai_kit.prompts.schemas import ContractSummary
94
+
95
+ built = build_request(
96
+ PromptRegistry("prompts"), "extract_contract@v2", document, schema=ContractSummary
97
+ )
98
+ repaired = generate_structured(client, built.req, ContractSummary)
99
+ summary, grounding = verify_citations(repaired.value, document)
100
+ print(summary.amount_minor, grounding.ratio) # ungrounded fields are dropped
101
+ ```
102
+
103
+ An agent with tools:
104
+
105
+ ```python
106
+ from aimai_kit.agent import Agent, Budgets, Thread
107
+ from aimai_kit.tools import CallContext, ToolExecutor
108
+ from aimai_kit.tools.examples.orders import build_registry, seed_database
109
+
110
+ seed_database()
111
+ executor = ToolExecutor(build_registry())
112
+ agent = Agent(client, executor, budgets=Budgets(max_steps=8, max_seconds=60))
113
+ run = agent.run(Thread(), "What is the status of order 1002?",
114
+ ctx=CallContext(user_id="u-1", tenant_id="t-1"))
115
+ print(run.stop_reason, run.answer)
116
+ ```
117
+
118
+ ## Command line
119
+
120
+ ```bash
121
+ # Compare models: TTFT from streaming, real usage from one complete call
122
+ uv run model-probe --prompt evals/probe/sample-prompt.txt \
123
+ --models anthropic:claude-opus-5 anthropic:claude-haiku-4-5 -n 5
124
+
125
+ # Extraction quality against a golden set (no API key needed)
126
+ uv run prompt-lab eval --prompt extract_contract@v2 --schema v2 \
127
+ --pricing config/pricing.toml --pricing-model claude-opus-5
128
+
129
+ # With a real model
130
+ uv run prompt-lab --model anthropic:claude-opus-5 eval
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Why each layer looks the way it does
136
+
137
+ Short version below; the full reasoning, including what was rejected and why,
138
+ is in `docs/`.
139
+
140
+ **[Provider](docs/01-provider.md)** — one internal message format and five
141
+ error classes. Retry decisions are driven by the error *class*, never by
142
+ matching message text. Cached tokens are normalized to a single rule across
143
+ providers, because Anthropic reports them outside `input_tokens` and every
144
+ cost calculation downstream depends on which convention you picked.
145
+
146
+ **[Prompts and context](docs/02-prompts.md)** — prompts are versioned files
147
+ identified as `name@vN+fingerprint`. The document goes into a user message,
148
+ never the system block, so the cache prefix stays byte-identical across
149
+ requests. Trimming is an explicit decision that produces a report line, and
150
+ the non-trimmable sections raise rather than shrink.
151
+
152
+ **[Tools](docs/03-tools.md)** — the schema is derived from the function
153
+ signature, so the two cannot drift apart. Five gates run before anything
154
+ executes, each producing a message the model can act on. Server context
155
+ (`tenant_id`) is injected from the call and is absent from the schema, so a
156
+ model cannot claim to be another tenant.
157
+
158
+ **[Agent loop](docs/04-agent.md)** — four budgets, one stop reason, and a
159
+ final tool-free turn so a stopped run still answers. Every tool call gets a
160
+ result, including refused ones. Repetition is warned about before it is
161
+ stopped, because a warned model usually recovers.
162
+
163
+ **[Harness](docs/05-harness.md)** — the atomic unit of context is a segment,
164
+ not a message, so trimming can never separate a tool call from its result.
165
+ Large output spills to disk with a reference the agent can follow. Compaction
166
+ converts old turns instead of dropping them, with a versioned prompt that
167
+ names what must survive.
168
+
169
+ ---
170
+
171
+ ## Measurements
172
+
173
+ Every number below is reproducible from this repository with no credentials.
174
+ Full tables and the honest caveats are in **[docs/measurements.md](docs/measurements.md)**.
175
+
176
+ | Experiment | Finding |
177
+ |---|---|
178
+ | Schema v1 vs v2 | Grounding 0% → 100%, at 0.06 more attempts and 16% more cost per document |
179
+ | Grounding attribution | v1's `start_date` reads 0% with grounding on and 88.9% with it off — the drop is the missing citation field, not extraction |
180
+ | Tool descriptions | Cutting descriptions to one line leaves selection accuracy unchanged but raises forbidden-tool calls from 0% to 4.5% |
181
+ | Loop detection | p95 steps 7 → 3, at the cost of completion 100% → 75% on runs that would have recovered on their own |
182
+ | Harness configurations | Naive trim 101k tokens and no answer; compaction 69.5k and no answer; compaction plus a sub-agent 12k and the answer survives |
183
+
184
+ The models behind these numbers are deterministic stubs, not providers. That
185
+ is deliberate: the point is that the measurement harness works and the
186
+ comparisons are reproducible. Run the same commands with `--model
187
+ anthropic:claude-opus-5` for numbers about a model.
188
+
189
+ ---
190
+
191
+ ## Testing
192
+
193
+ CI runs the suite on Python 3.12 and 3.13, and a second job re-runs every
194
+ measurement script and fails if a committed result changed. A table in the
195
+ docs that no longer matches the code is a broken build, not a reader's
196
+ problem.
197
+
198
+ ```bash
199
+ uv run pytest # 343 tests
200
+ uv run pytest -m live # calls real APIs, needs keys
201
+ uv run ruff check src/ tests/
202
+ ```
203
+
204
+ A few tests are worth calling out because of what they protect:
205
+
206
+ | Test | Guards against |
207
+ |---|---|
208
+ | `test_no_vendor_leak.py` | An SDK import escaping `adapters/` (AST-based, with an inverse check) |
209
+ | `test_prefix_stable.py` | A variable field leaking into the cache prefix and silently multiplying the bill |
210
+ | `test_schema_regression.py` | A schema changing without anyone noticing that old eval results are now incomparable |
211
+ | `test_segments.py` | A trim separating a tool call from its result |
212
+ | `test_compaction.py` | Compaction dropping a planted fact — three needles, both prompt versions |
213
+ | `test_eval_harness.py` | The eval harness silently returning "correct" for everything |
214
+
215
+ ---
216
+
217
+ ## Known limits
218
+
219
+ - **The golden sets are synthetic.** `scripts/generate_golden_set.py` writes 36
220
+ contracts, 10 of them deliberate edge cases. Replace them with your own
221
+ documents for a real evaluation; the `EDGE_CASES` map is the guide for what
222
+ to look for.
223
+ - **The stubs are not models.** They perform real extraction and real
224
+ selection, but they know the shape of the synthetic data, so their accuracy
225
+ is optimistic.
226
+ - **The pricing catalog only ships Anthropic rows.** OpenAI and Gemini are
227
+ commented out in `config/pricing.toml`; fill them from your own billing
228
+ page. A missing model produces a visible "window not in catalog" warning
229
+ rather than a silent zero.
230
+ - **Synchronous only.** Async adds no teaching value at this size.
231
+ - **Isolation is three layers and only two are in this repository.** The
232
+ in-process path jail and the cleaned subprocess environment are here;
233
+ closing the network belongs to deployment, and it is the layer that matters
234
+ most.
235
+
236
+ ## License
237
+
238
+ MIT.
@@ -0,0 +1,208 @@
1
+ # aimai-kit
2
+
3
+ [![CI](https://github.com/fport/aimai-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/fport/aimai-kit/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/aimai-kit.svg)](https://pypi.org/project/aimai-kit/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/aimai-kit.svg)](https://pypi.org/project/aimai-kit/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ A framework-free LLM engineering toolkit in one Python package: provider
9
+ adapters, prompt and context engineering, structured outputs, a tool layer,
10
+ a bounded agent loop, and a harness for long-running work.
11
+
12
+ Five layers, each built on the one below it, each with its own measurements.
13
+
14
+ | Layer | Package | What it adds |
15
+ |---|---|---|
16
+ | 1 | `provider/` | Token/cost/latency measurement, four adapters, retry and fallback |
17
+ | 2 | `prompts/` | Versioned prompts, context budget, structured output, a repair loop |
18
+ | 3 | `tools/` | Schemas from signatures, three provider exports, a five-gate executor |
19
+ | 4 | `agent/` | A bounded loop, four budgets, loop detection, checkpoints |
20
+ | 5 | `harness/` | Segments, spill, compaction, a memory store, sub-agents |
21
+
22
+ **343 tests**, no vendor SDK outside `provider/adapters/`, and every layer
23
+ runnable without an API key.
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install "aimai-kit[providers]"
31
+ ```
32
+
33
+ From source:
34
+
35
+ ```bash
36
+ uv sync --all-extras --group dev
37
+ cp .env.example .env # add your keys
38
+ uv run pytest # 343 tests, live ones excluded
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ from aimai_kit.provider.adapters import make_adapter
45
+ from aimai_kit.provider.types import ChatRequest, Message, Role
46
+
47
+ client = make_adapter("anthropic:claude-opus-5")
48
+ result = client.complete(
49
+ ChatRequest(
50
+ messages=[Message(role=Role.USER, content="Say hello.")],
51
+ system="Be brief.",
52
+ max_output_tokens=64,
53
+ )
54
+ )
55
+ print(result.text, result.usage.input_tokens, result.usage.output_tokens)
56
+ ```
57
+
58
+ Structured extraction with citation verification:
59
+
60
+ ```python
61
+ from aimai_kit.prompts import PromptRegistry, build_request, generate_structured
62
+ from aimai_kit.prompts.grounding import verify_citations
63
+ from aimai_kit.prompts.schemas import ContractSummary
64
+
65
+ built = build_request(
66
+ PromptRegistry("prompts"), "extract_contract@v2", document, schema=ContractSummary
67
+ )
68
+ repaired = generate_structured(client, built.req, ContractSummary)
69
+ summary, grounding = verify_citations(repaired.value, document)
70
+ print(summary.amount_minor, grounding.ratio) # ungrounded fields are dropped
71
+ ```
72
+
73
+ An agent with tools:
74
+
75
+ ```python
76
+ from aimai_kit.agent import Agent, Budgets, Thread
77
+ from aimai_kit.tools import CallContext, ToolExecutor
78
+ from aimai_kit.tools.examples.orders import build_registry, seed_database
79
+
80
+ seed_database()
81
+ executor = ToolExecutor(build_registry())
82
+ agent = Agent(client, executor, budgets=Budgets(max_steps=8, max_seconds=60))
83
+ run = agent.run(Thread(), "What is the status of order 1002?",
84
+ ctx=CallContext(user_id="u-1", tenant_id="t-1"))
85
+ print(run.stop_reason, run.answer)
86
+ ```
87
+
88
+ ## Command line
89
+
90
+ ```bash
91
+ # Compare models: TTFT from streaming, real usage from one complete call
92
+ uv run model-probe --prompt evals/probe/sample-prompt.txt \
93
+ --models anthropic:claude-opus-5 anthropic:claude-haiku-4-5 -n 5
94
+
95
+ # Extraction quality against a golden set (no API key needed)
96
+ uv run prompt-lab eval --prompt extract_contract@v2 --schema v2 \
97
+ --pricing config/pricing.toml --pricing-model claude-opus-5
98
+
99
+ # With a real model
100
+ uv run prompt-lab --model anthropic:claude-opus-5 eval
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Why each layer looks the way it does
106
+
107
+ Short version below; the full reasoning, including what was rejected and why,
108
+ is in `docs/`.
109
+
110
+ **[Provider](docs/01-provider.md)** — one internal message format and five
111
+ error classes. Retry decisions are driven by the error *class*, never by
112
+ matching message text. Cached tokens are normalized to a single rule across
113
+ providers, because Anthropic reports them outside `input_tokens` and every
114
+ cost calculation downstream depends on which convention you picked.
115
+
116
+ **[Prompts and context](docs/02-prompts.md)** — prompts are versioned files
117
+ identified as `name@vN+fingerprint`. The document goes into a user message,
118
+ never the system block, so the cache prefix stays byte-identical across
119
+ requests. Trimming is an explicit decision that produces a report line, and
120
+ the non-trimmable sections raise rather than shrink.
121
+
122
+ **[Tools](docs/03-tools.md)** — the schema is derived from the function
123
+ signature, so the two cannot drift apart. Five gates run before anything
124
+ executes, each producing a message the model can act on. Server context
125
+ (`tenant_id`) is injected from the call and is absent from the schema, so a
126
+ model cannot claim to be another tenant.
127
+
128
+ **[Agent loop](docs/04-agent.md)** — four budgets, one stop reason, and a
129
+ final tool-free turn so a stopped run still answers. Every tool call gets a
130
+ result, including refused ones. Repetition is warned about before it is
131
+ stopped, because a warned model usually recovers.
132
+
133
+ **[Harness](docs/05-harness.md)** — the atomic unit of context is a segment,
134
+ not a message, so trimming can never separate a tool call from its result.
135
+ Large output spills to disk with a reference the agent can follow. Compaction
136
+ converts old turns instead of dropping them, with a versioned prompt that
137
+ names what must survive.
138
+
139
+ ---
140
+
141
+ ## Measurements
142
+
143
+ Every number below is reproducible from this repository with no credentials.
144
+ Full tables and the honest caveats are in **[docs/measurements.md](docs/measurements.md)**.
145
+
146
+ | Experiment | Finding |
147
+ |---|---|
148
+ | Schema v1 vs v2 | Grounding 0% → 100%, at 0.06 more attempts and 16% more cost per document |
149
+ | Grounding attribution | v1's `start_date` reads 0% with grounding on and 88.9% with it off — the drop is the missing citation field, not extraction |
150
+ | Tool descriptions | Cutting descriptions to one line leaves selection accuracy unchanged but raises forbidden-tool calls from 0% to 4.5% |
151
+ | Loop detection | p95 steps 7 → 3, at the cost of completion 100% → 75% on runs that would have recovered on their own |
152
+ | Harness configurations | Naive trim 101k tokens and no answer; compaction 69.5k and no answer; compaction plus a sub-agent 12k and the answer survives |
153
+
154
+ The models behind these numbers are deterministic stubs, not providers. That
155
+ is deliberate: the point is that the measurement harness works and the
156
+ comparisons are reproducible. Run the same commands with `--model
157
+ anthropic:claude-opus-5` for numbers about a model.
158
+
159
+ ---
160
+
161
+ ## Testing
162
+
163
+ CI runs the suite on Python 3.12 and 3.13, and a second job re-runs every
164
+ measurement script and fails if a committed result changed. A table in the
165
+ docs that no longer matches the code is a broken build, not a reader's
166
+ problem.
167
+
168
+ ```bash
169
+ uv run pytest # 343 tests
170
+ uv run pytest -m live # calls real APIs, needs keys
171
+ uv run ruff check src/ tests/
172
+ ```
173
+
174
+ A few tests are worth calling out because of what they protect:
175
+
176
+ | Test | Guards against |
177
+ |---|---|
178
+ | `test_no_vendor_leak.py` | An SDK import escaping `adapters/` (AST-based, with an inverse check) |
179
+ | `test_prefix_stable.py` | A variable field leaking into the cache prefix and silently multiplying the bill |
180
+ | `test_schema_regression.py` | A schema changing without anyone noticing that old eval results are now incomparable |
181
+ | `test_segments.py` | A trim separating a tool call from its result |
182
+ | `test_compaction.py` | Compaction dropping a planted fact — three needles, both prompt versions |
183
+ | `test_eval_harness.py` | The eval harness silently returning "correct" for everything |
184
+
185
+ ---
186
+
187
+ ## Known limits
188
+
189
+ - **The golden sets are synthetic.** `scripts/generate_golden_set.py` writes 36
190
+ contracts, 10 of them deliberate edge cases. Replace them with your own
191
+ documents for a real evaluation; the `EDGE_CASES` map is the guide for what
192
+ to look for.
193
+ - **The stubs are not models.** They perform real extraction and real
194
+ selection, but they know the shape of the synthetic data, so their accuracy
195
+ is optimistic.
196
+ - **The pricing catalog only ships Anthropic rows.** OpenAI and Gemini are
197
+ commented out in `config/pricing.toml`; fill them from your own billing
198
+ page. A missing model produces a visible "window not in catalog" warning
199
+ rather than a silent zero.
200
+ - **Synchronous only.** Async adds no teaching value at this size.
201
+ - **Isolation is three layers and only two are in this repository.** The
202
+ in-process path jail and the cleaned subprocess environment are here;
203
+ closing the network belongs to deployment, and it is the layer that matters
204
+ most.
205
+
206
+ ## License
207
+
208
+ MIT.
@@ -0,0 +1,44 @@
1
+ # Pricing catalog — NOT hardcoded, read from here.
2
+ # Unit: USD per 1,000,000 tokens. Prices are written IN QUOTES; a float is
3
+ # rejected outright by pricing.py (the Decimal(float) trap).
4
+ #
5
+ # cached_input_per_mtok: the prompt-cache READ price. On Anthropic that is
6
+ # roughly 10% of the input price. Delete the line to disable the discount.
7
+ #
8
+ # VERIFY: these figures are Anthropic list prices as of 2026-09. Fill in the
9
+ # OpenAI and Gemini rows from your own billing page; leaving a row out beats
10
+ # guessing — for a model missing from the catalog, model-probe prints
11
+ # "window not in catalog" and reports zero cost.
12
+
13
+ [models."claude-opus-5"]
14
+ input_per_mtok = "5.00"
15
+ output_per_mtok = "25.00"
16
+ cached_input_per_mtok = "0.50"
17
+ context_window = 1000000
18
+ max_output_tokens = 128000
19
+
20
+ [models."claude-sonnet-5"]
21
+ input_per_mtok = "2.00"
22
+ output_per_mtok = "10.00"
23
+ cached_input_per_mtok = "0.20"
24
+ context_window = 1000000
25
+ max_output_tokens = 128000
26
+
27
+ [models."claude-haiku-4-5"]
28
+ input_per_mtok = "1.00"
29
+ output_per_mtok = "5.00"
30
+ cached_input_per_mtok = "0.10"
31
+ context_window = 200000
32
+ max_output_tokens = 64000
33
+
34
+ # --- fill in from your own billing page ---------------------------------
35
+ # [models."gpt-5.5"]
36
+ # input_per_mtok = "0.00"
37
+ # output_per_mtok = "0.00"
38
+ # cached_input_per_mtok = "0.00"
39
+ # context_window = 0
40
+ #
41
+ # [models."gemini-3-pro"]
42
+ # input_per_mtok = "0.00"
43
+ # output_per_mtok = "0.00"
44
+ # context_window = 0
@@ -0,0 +1,129 @@
1
+ # Provider layer — why it looks like this
2
+
3
+ The job of this layer is to make every provider look the same to everything
4
+ above it, without flattening away the differences that matter. Those are two
5
+ opposing pressures, and most of the decisions here are about where to put the
6
+ line between them.
7
+
8
+ ## The internal message format
9
+
10
+ `ChatRequest` / `ChatResult` / `Usage` / `Message` are the only vocabulary the
11
+ rest of the package speaks. Three choices in them are worth explaining.
12
+
13
+ **`system` is a top-level field, not an entry in the message list.** Anthropic
14
+ and Gemini already expect it that way. OpenAI wants it as a message, and
15
+ converting one into the other is a single line in the adapter. Normalizing in
16
+ the other direction — burying the system prompt inside the message list and
17
+ digging it out again in two of three adapters — would mean string surgery on
18
+ every call, in the place least suited to it.
19
+
20
+ **Unused fields exist and stay empty.** `ChatRequest` carried `json_schema`
21
+ and `tools` from the beginning, filled by later layers. Adding a field is
22
+ backward compatible; renaming one is not. The whole point of a stable
23
+ protocol is that the layer above can grow without the layer below changing,
24
+ and the way to get that is to leave room rather than to guess correctly.
25
+
26
+ **Provider-specific settings go in `extra`.** Gemini's `thinking_config` and
27
+ OpenAI's `reasoning_effort` have no equivalent in the other providers. Putting
28
+ them in the internal model would pollute it; leaving them unreachable would
29
+ force callers around the abstraction. A namespaced escape hatch keeps both
30
+ properties.
31
+
32
+ ## The trap that costs money: usage field names
33
+
34
+ This is the single most expensive detail in the layer.
35
+
36
+ OpenAI reports cached tokens *inside* `input_tokens`, with a detail object
37
+ breaking out the cached portion. Anthropic reports `cache_read_input_tokens`
38
+ *separately*, not included in `input_tokens`. Both are reasonable; they are
39
+ not the same.
40
+
41
+ Pick one convention and normalize to it in the adapters. This package treats
42
+ cached tokens as a **subset of input tokens** — OpenAI's convention — and the
43
+ Anthropic adapter folds cache reads and cache writes into `input_tokens` on
44
+ the way in.
45
+
46
+ Skip that normalization and Anthropic costs come out systematically low. Not
47
+ by a rounding error: on a cache-heavy workload the cached portion is most of
48
+ the input. The failure is silent, appears only in a billing report, and
49
+ looks like a pricing bug rather than an accounting one.
50
+
51
+ `ModelPricing.cost()` now raises when `cached_input_tokens > input_tokens`,
52
+ which is the shape an unnormalized adapter produces.
53
+
54
+ ## Error classification
55
+
56
+ Five classes, and the classification carries the retry decision:
57
+
58
+ | Class | Retry? | Because |
59
+ |---|---|---|
60
+ | `RateLimited` | yes | the provider said to wait, and told you how long |
61
+ | `TransientError` | yes | 5xx and connection failures are worth one more try |
62
+ | `InvalidRequest` | no | the same 400 comes back |
63
+ | `AuthError` | no | the key is still wrong |
64
+ | `AllProvidersFailed` | no | every link in the chain is down |
65
+
66
+ `retryable` lives on the class, so the retry policy reads it from one place.
67
+ The alternative — inspecting error messages for words like "rate limit" —
68
+ breaks silently the day a provider rewrites its copy, and the breakage looks
69
+ like an outage.
70
+
71
+ ## Retry and fallback
72
+
73
+ **The SDK's own retry is disabled** (`max_retries=0`). Two layers of retry
74
+ multiply: three attempts over two layers is six calls, and a deadline
75
+ computed for three is meaningless. Exactly one layer decides.
76
+
77
+ **There is a total deadline.** Three attempts times a 60-second `Retry-After`
78
+ is a request that hangs for three minutes. The user's client gave up long
79
+ before. Without a deadline, retrying becomes an outage of its own.
80
+
81
+ **Jitter is not decoration.** Fifty clients that all receive a 429 and all
82
+ wait exactly two seconds come back at the same instant and hit the same wall.
83
+ Random spread is what turns a thundering herd back into a queue.
84
+
85
+ **No retry on streaming.** Once the first chunk reached the user, retrying
86
+ means erasing half a sentence on screen. That is a UI decision and belongs to
87
+ the caller; the library does not hide it.
88
+
89
+ **Falling back is an event, not a quiet rescue.** `llm_fallbacks_total` is
90
+ exported with labels. A service that silently degrades looks healthy on every
91
+ dashboard even while the primary provider is completely down — users are
92
+ still getting answers, just slower, more expensive, and from a weaker model.
93
+ The fallback rate needs an alert on it precisely because the error rate will
94
+ not show anything.
95
+
96
+ ## Measuring: why TTFT and duration are separate
97
+
98
+ TTFT is what a user feels. Total duration is what a capacity plan needs. A
99
+ run with 200 ms TTFT and 9 s duration and one with 4 s TTFT and 5 s duration
100
+ have similar averages and completely different user experiences.
101
+
102
+ `model-probe` measures them in the only way that actually works: N streaming
103
+ runs give TTFT and duration, then one `complete` call gives the real `usage`,
104
+ which streaming may not report. The cost is N+1 calls. The payoff is the
105
+ drift column — the local tiktoken estimate next to the provider's reported
106
+ count. tiktoken is the OpenAI vocabulary, and on Anthropic and Gemini that
107
+ drift reaches 10-20%, which is why the context budget carries a safety margin
108
+ rather than filling to the limit.
109
+
110
+ ## Percentiles, not means
111
+
112
+ Latency in an LLM service is skewed, so the reports return p50/p95/p99.
113
+
114
+ One caveat is built into the tests: nearest-rank at small N swallows outliers.
115
+ With twenty runs, p95 is the nineteenth value — a single ten-second run does
116
+ not appear until p99. "p95 is fine" does not mean "there are no bad runs".
117
+ Write N next to the SLO.
118
+
119
+ Failed calls are excluded from latency (a 429 returning in 40 ms means no work
120
+ happened) but included in cost (the input tokens may still have been billed).
121
+
122
+ ## The boundary that is tested
123
+
124
+ Vendor SDKs are imported only under `provider/adapters/`, and
125
+ `test_no_vendor_leak.py` enforces it with an AST walk rather than a regex, so
126
+ comments and string literals do not raise false alarms. It also runs the
127
+ inverse check — that the adapters really do import an SDK — because otherwise
128
+ deleting the SDK calls by accident would leave the suite green and the
129
+ guarantee empty.