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.
- aimai_kit-1.0.0/.gitignore +16 -0
- aimai_kit-1.0.0/LICENSE +21 -0
- aimai_kit-1.0.0/PKG-INFO +238 -0
- aimai_kit-1.0.0/README.md +208 -0
- aimai_kit-1.0.0/config/pricing.toml +44 -0
- aimai_kit-1.0.0/docs/01-provider.md +129 -0
- aimai_kit-1.0.0/docs/02-prompts.md +192 -0
- aimai_kit-1.0.0/docs/03-tools.md +194 -0
- aimai_kit-1.0.0/docs/04-agent.md +160 -0
- aimai_kit-1.0.0/docs/05-harness.md +215 -0
- aimai_kit-1.0.0/docs/measurements.md +223 -0
- aimai_kit-1.0.0/prompts/compaction@v1.md +3 -0
- aimai_kit-1.0.0/prompts/compaction@v2.md +57 -0
- aimai_kit-1.0.0/prompts/extract_contract@v1.md +4 -0
- aimai_kit-1.0.0/prompts/extract_contract@v2.md +74 -0
- aimai_kit-1.0.0/prompts/summarize@v1.md +7 -0
- aimai_kit-1.0.0/prompts/summarize@v2.md +53 -0
- aimai_kit-1.0.0/pyproject.toml +82 -0
- aimai_kit-1.0.0/src/aimai_kit/__init__.py +1 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/__init__.py +28 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/budget.py +118 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/loop.py +296 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/loopdetect.py +70 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/metrics.py +112 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/scripted.py +72 -0
- aimai_kit-1.0.0/src/aimai_kit/agent/thread.py +142 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/__init__.py +38 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/compaction.py +126 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/context.py +133 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/memory.py +230 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/sandbox.py +97 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/segments.py +112 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/spill.py +134 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/stub_summarizer.py +81 -0
- aimai_kit-1.0.0/src/aimai_kit/harness/subagent.py +162 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/__init__.py +27 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/blocks.py +89 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/budget.py +238 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/cli.py +448 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/grounding.py +155 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/guard.py +77 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/pipeline.py +96 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/registry.py +171 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/schemas.py +247 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/structured.py +196 -0
- aimai_kit-1.0.0/src/aimai_kit/prompts/stub.py +203 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/__init__.py +1 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/__init__.py +51 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/_shared.py +37 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/anthropic_.py +185 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/azure_openai.py +64 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/gemini_.py +162 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/adapters/openai_.py +187 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/cli/__init__.py +1 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/cli/probe.py +313 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/client.py +45 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/counters.py +62 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/counting.py +135 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/determinism.py +63 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/errors.py +101 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/pricing.py +129 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/resilient.py +229 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/telemetry.py +147 -0
- aimai_kit-1.0.0/src/aimai_kit/provider/types.py +192 -0
- aimai_kit-1.0.0/src/aimai_kit/py.typed +0 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/__init__.py +36 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/decorator.py +175 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/evaluation.py +227 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/examples/__init__.py +12 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/examples/orders.py +237 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/executor.py +334 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/export.py +111 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/idempotency.py +86 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/registry.py +74 -0
- aimai_kit-1.0.0/src/aimai_kit/tools/spec.py +97 -0
- aimai_kit-1.0.0/tests/conftest.py +134 -0
- aimai_kit-1.0.0/tests/test_adapter_contract.py +196 -0
- aimai_kit-1.0.0/tests/test_agent_budgets.py +138 -0
- aimai_kit-1.0.0/tests/test_agent_loop.py +177 -0
- aimai_kit-1.0.0/tests/test_agent_metrics.py +80 -0
- aimai_kit-1.0.0/tests/test_budget.py +121 -0
- aimai_kit-1.0.0/tests/test_budget_guard.py +44 -0
- aimai_kit-1.0.0/tests/test_checkpoint.py +126 -0
- aimai_kit-1.0.0/tests/test_compaction.py +129 -0
- aimai_kit-1.0.0/tests/test_counting.py +46 -0
- aimai_kit-1.0.0/tests/test_eval_harness.py +127 -0
- aimai_kit-1.0.0/tests/test_executor_gates.py +219 -0
- aimai_kit-1.0.0/tests/test_grounding.py +102 -0
- aimai_kit-1.0.0/tests/test_idempotency.py +125 -0
- aimai_kit-1.0.0/tests/test_injection.py +121 -0
- aimai_kit-1.0.0/tests/test_loop_detection.py +124 -0
- aimai_kit-1.0.0/tests/test_memory.py +122 -0
- aimai_kit-1.0.0/tests/test_no_vendor_leak.py +57 -0
- aimai_kit-1.0.0/tests/test_prefix_stable.py +86 -0
- aimai_kit-1.0.0/tests/test_pricing.py +97 -0
- aimai_kit-1.0.0/tests/test_registry.py +69 -0
- aimai_kit-1.0.0/tests/test_repair_loop.py +119 -0
- aimai_kit-1.0.0/tests/test_resilient.py +147 -0
- aimai_kit-1.0.0/tests/test_sandbox.py +84 -0
- aimai_kit-1.0.0/tests/test_schema_regression.py +127 -0
- aimai_kit-1.0.0/tests/test_segments.py +126 -0
- aimai_kit-1.0.0/tests/test_spill.py +102 -0
- aimai_kit-1.0.0/tests/test_subagent.py +151 -0
- aimai_kit-1.0.0/tests/test_telemetry.py +99 -0
- aimai_kit-1.0.0/tests/test_tool_evaluation.py +82 -0
- aimai_kit-1.0.0/tests/test_tool_export.py +113 -0
- 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
|
aimai_kit-1.0.0/LICENSE
ADDED
|
@@ -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.
|
aimai_kit-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/fport/aimai-kit/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pypi.org/project/aimai-kit/)
|
|
35
|
+
[](https://pypi.org/project/aimai-kit/)
|
|
36
|
+
[](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
|
+
[](https://github.com/fport/aimai-kit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/aimai-kit/)
|
|
5
|
+
[](https://pypi.org/project/aimai-kit/)
|
|
6
|
+
[](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.
|