context-engineering-core 0.12.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.
Files changed (84) hide show
  1. context_engine/__init__.py +15 -0
  2. context_engine/__main__.py +3 -0
  3. context_engine/_sqlite.py +19 -0
  4. context_engine/_windows_security.py +125 -0
  5. context_engine/_windows_storage.py +110 -0
  6. context_engine/adapters.py +148 -0
  7. context_engine/answers.py +90 -0
  8. context_engine/budget.py +93 -0
  9. context_engine/cli.py +222 -0
  10. context_engine/config.py +193 -0
  11. context_engine/context.py +116 -0
  12. context_engine/demo.py +98 -0
  13. context_engine/errors.py +91 -0
  14. context_engine/evaluation/__init__.py +1 -0
  15. context_engine/evaluation/commands.py +90 -0
  16. context_engine/evaluation/corpus.py +247 -0
  17. context_engine/evaluation/data/freeze.json +26 -0
  18. context_engine/evaluation/data/ground_truth.json +287 -0
  19. context_engine/evaluation/data/pin_retention.json +8 -0
  20. context_engine/evaluation/data/protocol.json +122 -0
  21. context_engine/evaluation/data/scenario.json +1226 -0
  22. context_engine/evaluation/data/summary_fixture.json +7 -0
  23. context_engine/evaluation/execution.py +538 -0
  24. context_engine/evaluation/fixtures.py +32 -0
  25. context_engine/evaluation/grading.py +62 -0
  26. context_engine/evaluation/harness.py +165 -0
  27. context_engine/evaluation/heldout/__init__.py +260 -0
  28. context_engine/evaluation/heldout/__main__.py +40 -0
  29. context_engine/evaluation/heldout/data/ground_truth.json +18 -0
  30. context_engine/evaluation/heldout/data/manifest.json +9 -0
  31. context_engine/evaluation/heldout/data/protocol.json +14 -0
  32. context_engine/evaluation/heldout/data/scenarios.json +18 -0
  33. context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
  34. context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
  35. context_engine/evaluation/heldout/scoring.py +176 -0
  36. context_engine/evaluation/protocol.py +264 -0
  37. context_engine/evaluation/reporting.py +294 -0
  38. context_engine/evaluation/results.py +331 -0
  39. context_engine/evaluation/run_commands.py +146 -0
  40. context_engine/evaluation/strategies.py +261 -0
  41. context_engine/inspection.py +266 -0
  42. context_engine/layers/__init__.py +18 -0
  43. context_engine/layers/cap.py +99 -0
  44. context_engine/layers/common.py +162 -0
  45. context_engine/layers/pin.py +68 -0
  46. context_engine/layers/retrieve.py +233 -0
  47. context_engine/layers/summarize.py +202 -0
  48. context_engine/layers/window.py +29 -0
  49. context_engine/mcp_server.py +267 -0
  50. context_engine/memory/__init__.py +5 -0
  51. context_engine/memory/codec.py +91 -0
  52. context_engine/memory/demo.py +90 -0
  53. context_engine/memory/store.py +770 -0
  54. context_engine/models.py +384 -0
  55. context_engine/pipeline.py +383 -0
  56. context_engine/providers/__init__.py +1 -0
  57. context_engine/providers/client.py +301 -0
  58. context_engine/providers/commands.py +138 -0
  59. context_engine/providers/contracts.py +324 -0
  60. context_engine/providers/credentials.py +66 -0
  61. context_engine/providers/memory.py +112 -0
  62. context_engine/providers/store.py +408 -0
  63. context_engine/providers/transport.py +195 -0
  64. context_engine/providers/windows_credentials.py +23 -0
  65. context_engine/py.typed +0 -0
  66. context_engine/service/__init__.py +1 -0
  67. context_engine/service/__main__.py +68 -0
  68. context_engine/service/app.py +404 -0
  69. context_engine/service/auth.py +132 -0
  70. context_engine/service/client.py +52 -0
  71. context_engine/service/control.py +134 -0
  72. context_engine/service/demo.py +151 -0
  73. context_engine/service/integrations.py +28 -0
  74. context_engine/service/schemas.py +59 -0
  75. context_engine/service/worker.py +59 -0
  76. context_engine/service/workers.py +156 -0
  77. context_engine/summaries.py +196 -0
  78. context_engine/tokens.py +171 -0
  79. context_engine/work.py +46 -0
  80. context_engineering_core-0.12.0.dist-info/METADATA +203 -0
  81. context_engineering_core-0.12.0.dist-info/RECORD +84 -0
  82. context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
  83. context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
  84. context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,383 @@
1
+ """Public one-call context assembly with immutable outputs and final budget admission."""
2
+
3
+ import hashlib
4
+ from dataclasses import asdict, dataclass, replace
5
+ from datetime import UTC, datetime
6
+
7
+ from . import __version__
8
+ from .budget import validate_request
9
+ from .config import BudgetConfig, CapConfig, RetrievalConfig
10
+ from .context import ContextCarrier, LayerDiagnostic
11
+ from .errors import ContractError, LayerStateError, RequiredContextTooLarge
12
+ from .layers import CapLayer, PinLayer, RetrieveLayer, SummaryLayer, WindowLayer
13
+ from .layers.common import ORDER, history_digest, render_request, require_prepared, window_block
14
+ from .layers.retrieve import evidence_block, represented_in_window, window_messages
15
+ from .layers.summarize import SummaryPolicy
16
+ from .models import (
17
+ BlockKind,
18
+ ChatRequest,
19
+ ContextBlock,
20
+ KeyedPins,
21
+ Message,
22
+ Role,
23
+ SourceRef,
24
+ ToolDefinition,
25
+ Turn,
26
+ aware_time,
27
+ canonical_json,
28
+ freeze_sequence,
29
+ )
30
+ from .tokens import TiktokenCounter, TokenCounter, TokenEstimate
31
+ from .work import Cancellation, raise_if_cancelled, validate_cancellation
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class AssemblyOptions:
36
+ """Independent optional stages; mandatory reservation and WINDOW always run."""
37
+
38
+ cap: bool = True
39
+ pins: bool = True
40
+ retrieve: bool = True
41
+ summarize: bool = True
42
+
43
+ def __post_init__(self):
44
+ if any(type(value) is not bool for value in asdict(self).values()):
45
+ raise ContractError("Assembly options must be booleans")
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class BlockUsage:
50
+ kind: BlockKind
51
+ content_tokens: int
52
+ input_delta_tokens: int
53
+ sources: tuple[SourceRef, ...]
54
+ included: bool
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class Drop:
59
+ kind: BlockKind
60
+ source_id: str | None
61
+ reason: str = "final_request_over_budget"
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class AssemblyDiagnostics:
66
+ input_allowance: int
67
+ estimate: TokenEstimate
68
+ base_request_tokens: int
69
+ blocks: tuple[BlockUsage, ...]
70
+ planned_window_turn_ids: tuple[str, ...]
71
+ kept_turn_ids: tuple[str, ...]
72
+ omitted_turn_ids: tuple[str, ...]
73
+ retrieved_turn_ids: tuple[str, ...]
74
+ retrieved_chunk_ids: tuple[str, ...]
75
+ layer_diagnostics: tuple[LayerDiagnostic, ...]
76
+ drops: tuple[Drop, ...]
77
+ history_fingerprint: str
78
+ input_fingerprint: str
79
+ config_fingerprint: str
80
+ evaluated_at: datetime
81
+ package_version: str = __version__
82
+
83
+ @property
84
+ def remaining_tokens(self) -> int:
85
+ return self.input_allowance - self.estimate.estimated_tokens
86
+
87
+ def to_dict(self) -> dict:
88
+ return {
89
+ "input_allowance": self.input_allowance,
90
+ "estimate": self.estimate.to_dict(),
91
+ "remaining_tokens": self.remaining_tokens,
92
+ "base_request_tokens": self.base_request_tokens,
93
+ "blocks": [asdict(b) for b in self.blocks],
94
+ "planned_window_turn_ids": list(self.planned_window_turn_ids),
95
+ "kept_turn_ids": list(self.kept_turn_ids),
96
+ "omitted_turn_ids": list(self.omitted_turn_ids),
97
+ "retrieved_turn_ids": list(self.retrieved_turn_ids),
98
+ "retrieved_chunk_ids": list(self.retrieved_chunk_ids),
99
+ "summary_used": any(b.kind == BlockKind.SUMMARY and b.included for b in self.blocks),
100
+ "layer_diagnostics": [asdict(d) for d in self.layer_diagnostics],
101
+ "drops": [asdict(d) for d in self.drops],
102
+ "history_fingerprint": self.history_fingerprint,
103
+ "input_fingerprint": self.input_fingerprint,
104
+ "config_fingerprint": self.config_fingerprint,
105
+ "evaluated_at": self.evaluated_at.isoformat(),
106
+ "package_version": self.package_version,
107
+ }
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class AssembledContext:
112
+ request: ChatRequest
113
+ blocks: tuple[ContextBlock, ...]
114
+ diagnostics: AssemblyDiagnostics
115
+
116
+ def __post_init__(self) -> None:
117
+ if not isinstance(self.request, ChatRequest) or not isinstance(
118
+ self.diagnostics, AssemblyDiagnostics
119
+ ):
120
+ raise ContractError("Assembly requires validated request and diagnostics")
121
+ freeze_sequence(self, "blocks", ContextBlock)
122
+ if self.diagnostics.remaining_tokens < 0:
123
+ raise ContractError("Cannot return an over-budget assembled context")
124
+
125
+ @property
126
+ def messages(self) -> list[dict]:
127
+ """Fresh API-ready message dictionaries; callers cannot mutate the result."""
128
+ return self.request.to_wire()["messages"]
129
+
130
+ def to_dict(self, *, include_content: bool = False) -> dict:
131
+ if type(include_content) is not bool:
132
+ raise ContractError("include_content must be an explicit boolean")
133
+ result = {"diagnostics": self.diagnostics.to_dict(), "content_included": include_content}
134
+ if include_content:
135
+ result["request"] = self.request.to_wire()
136
+ return result
137
+
138
+
139
+ def _replace_block(
140
+ blocks: tuple[ContextBlock, ...], replacement: ContextBlock
141
+ ) -> tuple[ContextBlock, ...]:
142
+ return tuple(
143
+ sorted(
144
+ tuple(b for b in blocks if b.kind != replacement.kind) + (replacement,),
145
+ key=lambda b: ORDER.index(b.kind),
146
+ )
147
+ )
148
+
149
+
150
+ def _finalize(
151
+ context: ContextCarrier,
152
+ counter: TokenCounter,
153
+ budget: BudgetConfig,
154
+ *,
155
+ recover_capped_window: bool = False,
156
+ ):
157
+ """Bounded, deterministic optional removal; required payload stays byte-identical."""
158
+ require_prepared(context, counter)
159
+ if context.plan.input_allowance != budget.input_allowance or not context.window_planned:
160
+ raise LayerStateError("Finalization requires a matching completed selection plan")
161
+ blocks = context.blocks
162
+ ids = context.plan.window_turn_ids
163
+ chunks = context.retrieved_chunks
164
+ turns = {turn.turn_id: turn for turn in context.original_turns}
165
+ visible = window_messages(context)
166
+ for chunk in chunks:
167
+ if chunk.source.turn_id not in turns:
168
+ raise LayerStateError("Retrieved source conflicts with the history/window plan")
169
+ chunk.validate_source(turns[chunk.source.turn_id])
170
+ if chunk.source.turn_id in ids and (
171
+ not recover_capped_window or represented_in_window(chunk, visible)
172
+ ):
173
+ raise LayerStateError("Retrieved source duplicates or violates the WINDOW policy")
174
+ # Content/provenance must describe the same selected chunks and recent turns.
175
+ for kind, expected in (
176
+ (BlockKind.RETRIEVED, evidence_block(chunks, counter)),
177
+ (BlockKind.WINDOW, window_block(context, ids, counter)),
178
+ ):
179
+ actual = next((b for b in blocks if b.kind == kind), None)
180
+ if (
181
+ actual is None
182
+ or actual.content != expected.content
183
+ or actual.sources != expected.sources
184
+ ):
185
+ raise LayerStateError("Selected content and provenance do not agree")
186
+ drops: list[Drop] = []
187
+ # One initial count, one summary removal, one per window turn, one per chunk.
188
+ for _ in range(2 + len(ids) + len(chunks)):
189
+ raise_if_cancelled(context.cancellation)
190
+ request = render_request(blocks, context.question, context.required_request.tools)
191
+ estimate = counter.count_request(request)
192
+ if estimate.estimated_tokens <= budget.input_allowance:
193
+ validated = validate_request(request=request, config=budget, counter=counter)
194
+ if validated != estimate:
195
+ raise LayerStateError("Token counter changed for the identical final request")
196
+ sources = tuple(
197
+ SourceRef(
198
+ t.scope, t.turn_id, m.message_id, t.revision, 0, len(m.content), m.content_hash
199
+ )
200
+ for t in context.original_turns
201
+ if t.turn_id in ids
202
+ for m in t.messages
203
+ if m.content
204
+ )
205
+ final_window = replace(window_block(context, ids, counter), sources=sources)
206
+ blocks = _replace_block(blocks, final_window)
207
+ blocks = _replace_block(
208
+ blocks,
209
+ ContextBlock(
210
+ BlockKind.QUESTION,
211
+ context.question.content,
212
+ text_tokens=counter.count_text(context.question.content),
213
+ ),
214
+ )
215
+ return (
216
+ request,
217
+ tuple(sorted(blocks, key=lambda b: ORDER.index(b.kind))),
218
+ validated,
219
+ ids,
220
+ chunks,
221
+ tuple(drops),
222
+ )
223
+ summary = next((b for b in blocks if b.kind == BlockKind.SUMMARY), None)
224
+ if summary is not None and summary.content:
225
+ blocks = _replace_block(blocks, ContextBlock(BlockKind.SUMMARY, "", text_tokens=0))
226
+ drops.append(Drop(BlockKind.SUMMARY, None))
227
+ elif ids:
228
+ dropped, ids = ids[0], ids[1:]
229
+ blocks = _replace_block(blocks, window_block(context, ids, counter))
230
+ drops.append(Drop(BlockKind.WINDOW, dropped))
231
+ elif chunks:
232
+ dropped, chunks = chunks[-1], chunks[:-1]
233
+ blocks = _replace_block(blocks, evidence_block(chunks, counter))
234
+ drops.append(Drop(BlockKind.RETRIEVED, dropped.chunk_id))
235
+ else:
236
+ raise RequiredContextTooLarge(
237
+ "Required context exceeds the final input allowance",
238
+ required_tokens=estimate.estimated_tokens,
239
+ input_allowance=budget.input_allowance,
240
+ overflow_tokens=estimate.estimated_tokens - budget.input_allowance,
241
+ )
242
+ raise LayerStateError("Finalization exceeded its bounded removal steps")
243
+
244
+
245
+ def _block_accounting(blocks, question, tools, counter, estimate):
246
+ empty_question = replace(question, content="")
247
+ running: tuple[ContextBlock, ...] = ()
248
+ previous = counter.count_request(render_request((), empty_question, tools)).estimated_tokens
249
+ base = previous
250
+ result = []
251
+ by_kind = {b.kind: b for b in blocks}
252
+ for kind in ORDER:
253
+ item = by_kind.get(kind, ContextBlock(kind, "", text_tokens=0))
254
+ current_question = question if kind == BlockKind.QUESTION else empty_question
255
+ if kind != BlockKind.QUESTION:
256
+ running += (item,)
257
+ current = counter.count_request(
258
+ render_request(running, current_question, tools)
259
+ ).estimated_tokens
260
+ content = question.content if kind == BlockKind.QUESTION else item.content
261
+ result.append(
262
+ BlockUsage(
263
+ kind,
264
+ counter.count_text(content),
265
+ current - previous,
266
+ item.sources,
267
+ kind == BlockKind.QUESTION or bool(content),
268
+ )
269
+ )
270
+ previous = current
271
+ if previous != estimate.estimated_tokens:
272
+ raise LayerStateError("Per-block accounting disagrees with the final request estimate")
273
+ return base, tuple(result)
274
+
275
+
276
+ def assemble_context(
277
+ history: tuple[Turn, ...] | list[Turn],
278
+ question: Message | str,
279
+ pinned_facts: KeyedPins,
280
+ budget: BudgetConfig,
281
+ *,
282
+ system: str,
283
+ at: datetime | None = None,
284
+ token_counter: TokenCounter | None = None,
285
+ cap_config: CapConfig | None = None,
286
+ retrieval_config: RetrievalConfig | None = None,
287
+ summary_policy: SummaryPolicy | None = None,
288
+ tools: tuple[ToolDefinition, ...] | list[ToolDefinition] = (),
289
+ options: AssemblyOptions | None = None,
290
+ chunk_index=None,
291
+ cancellation: Cancellation | None = None,
292
+ ) -> AssembledContext:
293
+ """Assemble authorized history into an estimated-token-safe request, or raise a typed error.
294
+
295
+ Scope is derived from KeyedPins, including for empty history. Authorization is
296
+ the caller's responsibility. Supply `at` for reproducible pin/summary expiry.
297
+ Every call, including calls after tools, should use this boundary.
298
+ """
299
+ validate_cancellation(cancellation)
300
+ raise_if_cancelled(cancellation)
301
+ if not isinstance(pinned_facts, KeyedPins) or not isinstance(budget, BudgetConfig):
302
+ raise ContractError("Expected KeyedPins and BudgetConfig")
303
+ options = AssemblyOptions() if options is None else options
304
+ if not isinstance(options, AssemblyOptions):
305
+ raise ContractError("Expected validated assembly options")
306
+ if not options.pins:
307
+ pinned_facts = KeyedPins(pinned_facts.scope)
308
+ budget = replace(
309
+ budget,
310
+ retrieval_reserve=budget.retrieval_reserve if options.retrieve else 0,
311
+ summary_reserve=budget.summary_reserve if options.summarize else 0,
312
+ )
313
+ if not isinstance(history, (list, tuple)) or any(not isinstance(t, Turn) for t in history):
314
+ raise ContractError("History must be a sequence of validated turns")
315
+ if not isinstance(tools, (list, tuple)) or any(
316
+ not isinstance(t, ToolDefinition) for t in tools
317
+ ):
318
+ raise ContractError("Tools must be a sequence of validated definitions")
319
+ at = datetime.now(UTC) if at is None else at
320
+ aware_time("at", at)
321
+ cap_config = CapConfig() if cap_config is None else cap_config
322
+ retrieval_config = RetrievalConfig() if retrieval_config is None else retrieval_config
323
+ if not isinstance(cap_config, CapConfig) or not isinstance(retrieval_config, RetrievalConfig):
324
+ raise ContractError("Expected validated layer configuration")
325
+ if isinstance(question, str):
326
+ used = {m.message_id for t in history for m in t.messages}
327
+ identifier = "current-question"
328
+ while identifier in used:
329
+ identifier += ":next"
330
+ question = Message(identifier, Role.USER, question)
331
+ context = ContextCarrier(
332
+ pinned_facts.scope, history, history, pinned_facts, question, cancellation=cancellation
333
+ )
334
+ counter = TiktokenCounter() if token_counter is None else token_counter
335
+ if options.cap:
336
+ context = CapLayer(counter, cap_config).apply(context)
337
+ else:
338
+ context = replace(context, diagnostics=(LayerDiagnostic("CAP", "layer_disabled"),))
339
+ for layer in (
340
+ PinLayer(counter, budget, system, at, tuple(tools)),
341
+ RetrieveLayer(counter, retrieval_config, chunk_index),
342
+ WindowLayer(counter),
343
+ SummaryLayer(counter, summary_policy if options.summarize else None, at),
344
+ ):
345
+ raise_if_cancelled(cancellation)
346
+ context = layer.apply(context)
347
+ raise_if_cancelled(cancellation)
348
+ request, blocks, estimate, kept, chunks, drops = _finalize(
349
+ context, counter, budget, recover_capped_window=retrieval_config.recover_capped_window
350
+ )
351
+ base, accounting = _block_accounting(blocks, question, tuple(tools), counter, estimate)
352
+ config_data = {
353
+ "budget": asdict(budget),
354
+ "cap": asdict(cap_config),
355
+ "retrieval": asdict(retrieval_config),
356
+ "options": asdict(options),
357
+ "counting": {
358
+ "tokenizer": estimate.tokenizer_name,
359
+ "version": estimate.tokenizer_version,
360
+ "serializer": estimate.serializer_name,
361
+ "factor": estimate.calibration_factor,
362
+ "overhead": estimate.adapter_overhead_tokens,
363
+ },
364
+ }
365
+ diagnostics = AssemblyDiagnostics(
366
+ budget.input_allowance,
367
+ estimate,
368
+ base,
369
+ accounting,
370
+ context.plan.window_turn_ids,
371
+ kept,
372
+ tuple(t.turn_id for t in context.original_turns if t.turn_id not in kept),
373
+ tuple(dict.fromkeys(c.source.turn_id for c in chunks)),
374
+ tuple(c.chunk_id for c in chunks),
375
+ context.diagnostics,
376
+ drops,
377
+ history_digest(context.original_turns),
378
+ context.prepared_fingerprint,
379
+ hashlib.sha256(canonical_json(config_data).encode()).hexdigest(),
380
+ at,
381
+ )
382
+ raise_if_cancelled(cancellation)
383
+ return AssembledContext(request, blocks, diagnostics)
@@ -0,0 +1 @@
1
+ """Optional provider integration. Importing the core never loads this package."""
@@ -0,0 +1,301 @@
1
+ """Scoped provider execution, final token admission, quota reservations and local replay."""
2
+
3
+ import asyncio
4
+ import time
5
+ from dataclasses import asdict
6
+
7
+ from ..budget import validate_request
8
+ from ..config import BudgetConfig
9
+ from ..errors import ContextEngineError, ContractError, StorageError
10
+ from ..models import ChatRequest, Scope, canonical_json, text_value
11
+ from ..tokens import TiktokenCounter
12
+ from .contracts import (
13
+ ADAPTER_VERSION,
14
+ ENDPOINT,
15
+ Completion,
16
+ GenerationConfig,
17
+ InceptionGenerationConfig,
18
+ ModelResult,
19
+ PriceCard,
20
+ QuotaPolicy,
21
+ ReplayPolicy,
22
+ RetryConfig,
23
+ TransportFailure,
24
+ fingerprint,
25
+ )
26
+ from .store import RuntimeStore
27
+ from .transport import GroqTransport, InceptionTransport
28
+
29
+
30
+ class ProviderClient:
31
+ def __init__(
32
+ self,
33
+ *,
34
+ store: RuntimeStore,
35
+ quota: QuotaPolicy,
36
+ prices: PriceCard,
37
+ api_key: str | None = None,
38
+ generation: GenerationConfig | None = None,
39
+ retries: RetryConfig | None = None,
40
+ replay: ReplayPolicy | None = None,
41
+ transport=None,
42
+ counter=None,
43
+ clock=time.time,
44
+ monotonic=time.monotonic,
45
+ sleep=asyncio.sleep,
46
+ ):
47
+ self.store, self.quota, self.prices = store, quota, prices
48
+ self._api_key = api_key # Never exported, represented or persisted.
49
+ self.generation = generation or GenerationConfig()
50
+ self.retries, self.replay = retries or RetryConfig(), replay or ReplayPolicy()
51
+ default = (
52
+ InceptionTransport
53
+ if isinstance(self.generation, InceptionGenerationConfig)
54
+ else GroqTransport
55
+ )
56
+ self.transport = transport if transport is not None else default()
57
+ self.counter = counter if counter is not None else TiktokenCounter()
58
+ self.clock, self.monotonic, self.sleep = clock, monotonic, sleep
59
+ for value, cls in (
60
+ (store, RuntimeStore),
61
+ (quota, QuotaPolicy),
62
+ (prices, PriceCard),
63
+ (self.generation, (GenerationConfig, InceptionGenerationConfig)),
64
+ (self.retries, RetryConfig),
65
+ (self.replay, ReplayPolicy),
66
+ ):
67
+ if not isinstance(value, cls):
68
+ raise ContractError("Provider client requires validated configuration")
69
+ quota.validate_prices(prices)
70
+ inception = isinstance(self.generation, InceptionGenerationConfig)
71
+ if (inception and type(self.transport) is GroqTransport) or (
72
+ not inception and isinstance(self.transport, InceptionTransport)
73
+ ):
74
+ raise ContractError("Generation settings and provider transport do not match")
75
+
76
+ async def complete(
77
+ self,
78
+ request: ChatRequest,
79
+ budget: BudgetConfig,
80
+ *,
81
+ scope: Scope,
82
+ security_scope: str,
83
+ snapshot_revision: str,
84
+ policy_version="v1",
85
+ ) -> ModelResult:
86
+ """Caller authorizes scope. Task cancellation preserves uncertain accounting."""
87
+ start = self.monotonic()
88
+ generation = self.generation
89
+ if (
90
+ not isinstance(request, ChatRequest)
91
+ or not isinstance(budget, BudgetConfig)
92
+ or not isinstance(scope, Scope)
93
+ ):
94
+ raise ContractError("Provider call requires validated request, budget and scope")
95
+ for name, value in (
96
+ ("security_scope", security_scope),
97
+ ("snapshot_revision", snapshot_revision),
98
+ ("policy_version", policy_version),
99
+ ):
100
+ text_value(name, value)
101
+ if (
102
+ budget.completion_reservation != generation.max_completion_tokens
103
+ or budget.model_capacity > 131072
104
+ ):
105
+ raise ContractError("Assembly and provider completion/context budgets must agree")
106
+ estimate = validate_request(request=request, config=budget, counter=self.counter)
107
+ payload = generation.to_wire(request)
108
+ if len(canonical_json(payload).encode()) > 2_000_000:
109
+ raise ContractError("Provider request exceeds the byte limit")
110
+ key = fingerprint(
111
+ {
112
+ # Groq keys are unchanged; other transports bind their own endpoint.
113
+ "adapter": getattr(self.transport, "adapter", ADAPTER_VERSION),
114
+ "endpoint": getattr(self.transport, "endpoint", ENDPOINT),
115
+ "payload": payload,
116
+ "account": self.quota.account_id,
117
+ "scope": asdict(scope),
118
+ "security_scope": security_scope,
119
+ "snapshot_revision": snapshot_revision,
120
+ "policy_version": policy_version,
121
+ "budget": asdict(budget),
122
+ "counting": estimate.to_dict(),
123
+ "prices": asdict(self.prices),
124
+ "transport": getattr(
125
+ self.transport, "namespace", type(self.transport).__qualname__
126
+ ),
127
+ }
128
+ )
129
+ key_error = None
130
+ if not isinstance(self._api_key, str) or not self._api_key.strip():
131
+ key_error = "missing_api_key"
132
+ elif any(ord(c) < 33 or ord(c) > 126 for c in self._api_key):
133
+ key_error = "invalid_api_key"
134
+ attempts, known_cost = [], 0
135
+ reserved_tokens = estimate.estimated_tokens + generation.max_completion_tokens
136
+ reserve_cost = self.prices.reserve_cost(
137
+ estimate.estimated_tokens, generation.max_completion_tokens
138
+ )
139
+ for number in range(self.retries.max_attempts):
140
+ remaining = self.retries.total_timeout - (self.monotonic() - start)
141
+ if remaining <= 0:
142
+ return ModelResult(
143
+ "error",
144
+ key,
145
+ tuple(attempts),
146
+ error_code="deadline",
147
+ new_cost_microusd=known_cost,
148
+ )
149
+ try:
150
+ admission = self.store.admit(
151
+ policy=self.quota,
152
+ key=key,
153
+ model=generation.model,
154
+ reserved_tokens=reserved_tokens,
155
+ reserved_cost=reserve_cost,
156
+ prices=self.prices,
157
+ replay=self.replay,
158
+ now=self.clock(),
159
+ send_allowed=key_error is None,
160
+ )
161
+ except ContextEngineError as error:
162
+ return ModelResult(
163
+ "error",
164
+ key,
165
+ tuple(attempts),
166
+ error_code=key_error or error.code,
167
+ new_cost_microusd=known_cost,
168
+ )
169
+ if admission.completion is not None:
170
+ return ModelResult(
171
+ "replay",
172
+ key,
173
+ tuple(attempts),
174
+ admission.completion,
175
+ replay_of=admission.source_attempt,
176
+ new_cost_microusd=known_cost,
177
+ original_cost_microusd=admission.original_cost,
178
+ )
179
+ attempt = admission.attempt_id
180
+ attempts.append(attempt)
181
+ try:
182
+ self.store.mark_inflight(attempt)
183
+ except StorageError:
184
+ return ModelResult(
185
+ "error",
186
+ key,
187
+ tuple(attempts),
188
+ error_code="runtime_storage_error",
189
+ new_cost_microusd=known_cost,
190
+ warning="reservation_held_no_dispatch",
191
+ )
192
+ try:
193
+ remaining = self.retries.total_timeout - (self.monotonic() - start)
194
+ if remaining <= 0:
195
+ raise TransportFailure("deadline_not_sent", uncertain=False)
196
+ timeout = min(remaining, self.retries.attempt_timeout)
197
+ async with asyncio.timeout(timeout):
198
+ completion = await self.transport.send(payload, self._api_key, timeout)
199
+ if not isinstance(completion, Completion) or completion.model != generation.model:
200
+ raise TransportFailure("invalid_response", uncertain=True)
201
+ if completion.tool_calls:
202
+ names = {tool.name for tool in request.tools}
203
+ if generation.tool_choice != "auto" or any(
204
+ call.name not in names for call in completion.tool_calls
205
+ ):
206
+ raise TransportFailure(
207
+ "unexpected_tool_call", uncertain=True, usage=completion.usage
208
+ )
209
+ except asyncio.CancelledError:
210
+ # A cancelled task may already have reached the provider. No automatic release.
211
+ try:
212
+ self.store.settle(attempt, now=self.clock(), uncertain=True, reason="cancelled")
213
+ except StorageError:
214
+ pass # Existing inflight reservation remains conservatively held.
215
+ raise
216
+ except TimeoutError:
217
+ failure = TransportFailure("timeout", uncertain=True)
218
+ except TransportFailure as error:
219
+ failure = error
220
+ except Exception:
221
+ failure = TransportFailure("transport", uncertain=True)
222
+ else:
223
+ try:
224
+ cost, warning = self.store.settle(
225
+ attempt, now=self.clock(), completion=completion, replay=self.replay
226
+ )
227
+ except StorageError:
228
+ return ModelResult(
229
+ "error",
230
+ key,
231
+ tuple(attempts),
232
+ error_code="runtime_storage_error",
233
+ new_cost_microusd=None,
234
+ warning="provider_may_have_charged_reservation_held",
235
+ )
236
+ return ModelResult(
237
+ {
238
+ "stop": "success",
239
+ "length": "truncated",
240
+ "tool_calls": "tool_calls",
241
+ "content_filter": "filtered",
242
+ }[completion.finish_reason],
243
+ key,
244
+ tuple(attempts),
245
+ completion,
246
+ new_cost_microusd=known_cost + cost,
247
+ original_cost_microusd=cost,
248
+ warning=warning,
249
+ )
250
+ try:
251
+ cost, warning = self.store.settle(
252
+ attempt,
253
+ now=self.clock(),
254
+ usage=failure.usage,
255
+ uncertain=failure.uncertain,
256
+ reason=failure.code,
257
+ sent=failure.code
258
+ not in ("connection_not_sent", "deadline_not_sent", "provider_extra_missing"),
259
+ )
260
+ except StorageError:
261
+ return ModelResult(
262
+ "error",
263
+ key,
264
+ tuple(attempts),
265
+ error_code="runtime_storage_error",
266
+ new_cost_microusd=None,
267
+ warning="reservation_held",
268
+ )
269
+ if cost is None:
270
+ return ModelResult(
271
+ "error",
272
+ key,
273
+ tuple(attempts),
274
+ error_code=failure.code,
275
+ new_cost_microusd=None,
276
+ warning=warning,
277
+ )
278
+ known_cost += cost
279
+ delay = (
280
+ failure.retry_after
281
+ if failure.retry_after is not None
282
+ else min(0.25 * 2**number, self.retries.max_backoff)
283
+ )
284
+ remaining = self.retries.total_timeout - (self.monotonic() - start)
285
+ if (
286
+ not failure.retryable
287
+ or failure.uncertain
288
+ or number + 1 >= self.retries.max_attempts
289
+ or delay > self.retries.max_backoff
290
+ or delay >= remaining
291
+ ):
292
+ return ModelResult(
293
+ "error",
294
+ key,
295
+ tuple(attempts),
296
+ error_code=failure.code,
297
+ new_cost_microusd=known_cost,
298
+ warning=warning,
299
+ )
300
+ await self.sleep(delay)
301
+ raise AssertionError("Bounded provider loop exhausted unexpectedly")