graphite-code 0.3.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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Hardened adapter for governed OpenRouter development execution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from .claude_executor import AdapterError
|
|
12
|
+
from .contracts import CliIdentity, Effort, ProviderId
|
|
13
|
+
from .edit_apply import (
|
|
14
|
+
EDIT_RESULT_MARKER,
|
|
15
|
+
MAX_EDIT_FILE_BYTES,
|
|
16
|
+
apply_whole_file_edit,
|
|
17
|
+
)
|
|
18
|
+
from .lifecycle import LifecycleProviderId, ProviderRuntimeIdentity
|
|
19
|
+
from .openrouter_probe import (
|
|
20
|
+
CANONICAL_ENDPOINT,
|
|
21
|
+
HttpProbe,
|
|
22
|
+
OpenRouterPricing,
|
|
23
|
+
completion_cost_microunits,
|
|
24
|
+
observe_openrouter_with_pricing,
|
|
25
|
+
)
|
|
26
|
+
from .probe_runner import (
|
|
27
|
+
MAX_INFERENCE_REQUEST_BYTES,
|
|
28
|
+
MAX_INFERENCE_RESPONSE_BYTES,
|
|
29
|
+
MAX_INFERENCE_TIMEOUT_SECONDS,
|
|
30
|
+
HttpProbeEndpoint,
|
|
31
|
+
HttpProbeResult,
|
|
32
|
+
ProbeEndpointPurpose,
|
|
33
|
+
ProviderProbeError,
|
|
34
|
+
run_http_probe,
|
|
35
|
+
)
|
|
36
|
+
from .schema_validation import is_supported_schema, matches_schema
|
|
37
|
+
|
|
38
|
+
ADAPTER_PROTOCOL_VERSION: Final = "1.0.0"
|
|
39
|
+
API_CONTRACT_VERSION: Final = "1.0.0"
|
|
40
|
+
MAX_TOKEN_COUNT: Final = 10_000_000
|
|
41
|
+
MAX_OUTPUT_SCHEMA_BYTES: Final = 65_536
|
|
42
|
+
MAX_COST_MICROUNITS: Final = 1_000_000_000
|
|
43
|
+
_EXECUTION_EFFORTS: Final = frozenset({Effort.LOW, Effort.MEDIUM, Effort.HIGH})
|
|
44
|
+
_PROBE_FAILURE_CODES: Final = {
|
|
45
|
+
"probe_model_unavailable": "model_unavailable",
|
|
46
|
+
"probe_auth_unhealthy": "auth_required",
|
|
47
|
+
}
|
|
48
|
+
_EXECUTION_TRANSPORT_CODES: Final = {
|
|
49
|
+
"probe_response_limit": "response_limit",
|
|
50
|
+
"probe_timeout": "timeout",
|
|
51
|
+
# A non-2xx / redirect from the completions POST is a provider-side
|
|
52
|
+
# rejection of the request (e.g. an unsupported response_format), NOT a
|
|
53
|
+
# transient network failure -- surface it distinctly so it is not
|
|
54
|
+
# conflated with dns_busy/unavailable/failed under one opaque code.
|
|
55
|
+
"probe_http_status": "http_status",
|
|
56
|
+
"probe_redirect_rejected": "http_status",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"ADAPTER_PROTOCOL_VERSION",
|
|
61
|
+
"API_CONTRACT_VERSION",
|
|
62
|
+
"CANONICAL_ENDPOINT",
|
|
63
|
+
"EDIT_RESULT_MARKER",
|
|
64
|
+
"MAX_EDIT_FILE_BYTES",
|
|
65
|
+
"OpenRouterExecutionResult",
|
|
66
|
+
"OpenRouterPreflight",
|
|
67
|
+
"apply_whole_file_edit",
|
|
68
|
+
"execute_openrouter",
|
|
69
|
+
"preflight_openrouter",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class OpenRouterPreflight:
|
|
75
|
+
"""Endpoint runtime identity plus pinned pricing for one governed model."""
|
|
76
|
+
|
|
77
|
+
identity: CliIdentity
|
|
78
|
+
runtime: ProviderRuntimeIdentity
|
|
79
|
+
pricing: OpenRouterPricing
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _canonical_sha256(value: object) -> str:
|
|
83
|
+
return hashlib.sha256(
|
|
84
|
+
json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
|
85
|
+
).hexdigest()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def preflight_openrouter(
|
|
89
|
+
*,
|
|
90
|
+
api_key: str,
|
|
91
|
+
model_id: str,
|
|
92
|
+
routing_policy: Mapping[str, object],
|
|
93
|
+
observed_at: int,
|
|
94
|
+
policy_version: str,
|
|
95
|
+
transport: HttpProbe = run_http_probe,
|
|
96
|
+
) -> OpenRouterPreflight:
|
|
97
|
+
"""Bind endpoint, model, routing policy, and pricing into one identity digest."""
|
|
98
|
+
try:
|
|
99
|
+
observation = observe_openrouter_with_pricing(
|
|
100
|
+
endpoint=CANONICAL_ENDPOINT,
|
|
101
|
+
api_key=api_key,
|
|
102
|
+
model_id=model_id,
|
|
103
|
+
routing_policy=routing_policy,
|
|
104
|
+
observed_at=observed_at,
|
|
105
|
+
policy_version=policy_version,
|
|
106
|
+
transport=transport,
|
|
107
|
+
)
|
|
108
|
+
except ProviderProbeError as exc:
|
|
109
|
+
raise AdapterError(_PROBE_FAILURE_CODES.get(exc.code, "unavailable")) from None
|
|
110
|
+
runtime = observation.identity
|
|
111
|
+
composite = _canonical_sha256(
|
|
112
|
+
{
|
|
113
|
+
"endpoint": runtime.runtime_digest,
|
|
114
|
+
"model": runtime.model_identity_digest,
|
|
115
|
+
"pricing": observation.pricing.digest,
|
|
116
|
+
"routing": runtime.routing_policy_digest,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
identity = CliIdentity(
|
|
120
|
+
ProviderId.OPENROUTER,
|
|
121
|
+
composite,
|
|
122
|
+
API_CONTRACT_VERSION,
|
|
123
|
+
ADAPTER_PROTOCOL_VERSION,
|
|
124
|
+
)
|
|
125
|
+
return OpenRouterPreflight(identity, runtime, observation.pricing)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True, slots=True)
|
|
129
|
+
class OpenRouterExecutionResult:
|
|
130
|
+
"""Sanitized outcome of one schema-bound OpenRouter execution."""
|
|
131
|
+
|
|
132
|
+
effective_model: str
|
|
133
|
+
message: str = field(repr=False)
|
|
134
|
+
input_tokens: int
|
|
135
|
+
output_tokens: int
|
|
136
|
+
cost_microunits: int
|
|
137
|
+
duration_seconds: float
|
|
138
|
+
request_sha256: str
|
|
139
|
+
response_sha256: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _model_name(value: object) -> str:
|
|
143
|
+
if (
|
|
144
|
+
not isinstance(value, str)
|
|
145
|
+
or not value
|
|
146
|
+
or len(value) > 128
|
|
147
|
+
or any(character.isspace() for character in value)
|
|
148
|
+
):
|
|
149
|
+
raise AdapterError("request_invalid")
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _token(value: object) -> int:
|
|
154
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= MAX_TOKEN_COUNT:
|
|
155
|
+
raise AdapterError("protocol")
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def execute_openrouter(
|
|
160
|
+
*,
|
|
161
|
+
api_key: str,
|
|
162
|
+
prompt: bytes,
|
|
163
|
+
requested_model: str,
|
|
164
|
+
expected_effective_model: str,
|
|
165
|
+
effort: Effort,
|
|
166
|
+
output_schema: dict[str, object],
|
|
167
|
+
output_schema_sha256: str,
|
|
168
|
+
pricing: OpenRouterPricing,
|
|
169
|
+
max_output_tokens: int,
|
|
170
|
+
max_cost_microunits: int,
|
|
171
|
+
timeout_seconds: float,
|
|
172
|
+
response_format_type: str = "json_schema",
|
|
173
|
+
transport: HttpProbe = run_http_probe,
|
|
174
|
+
) -> OpenRouterExecutionResult:
|
|
175
|
+
"""Perform exactly one bounded, schema-bound chat completion; no retries.
|
|
176
|
+
|
|
177
|
+
``response_format_type`` selects the provider response contract:
|
|
178
|
+
``"json_schema"`` (default) sends the strict json_schema block;
|
|
179
|
+
``"json_object"`` sends only ``{"type": "json_object"}``. The output
|
|
180
|
+
schema is canonicalized and its digest pinned in both modes -- json_object
|
|
181
|
+
simply does not forward the schema to the provider as an enforcement
|
|
182
|
+
mechanism. Response parsing (``json.loads`` of the completion content) is
|
|
183
|
+
identical for both, so switching modes changes exactly one variable.
|
|
184
|
+
"""
|
|
185
|
+
if not isinstance(api_key, str) or not api_key:
|
|
186
|
+
raise AdapterError("auth_required")
|
|
187
|
+
if len(api_key) > 4096 or any(character in api_key for character in "\r\n\x00"):
|
|
188
|
+
raise AdapterError("request_invalid")
|
|
189
|
+
if (
|
|
190
|
+
not isinstance(prompt, bytes)
|
|
191
|
+
or not prompt
|
|
192
|
+
or len(prompt) > MAX_INFERENCE_REQUEST_BYTES
|
|
193
|
+
):
|
|
194
|
+
raise AdapterError("request_invalid")
|
|
195
|
+
try:
|
|
196
|
+
prompt_text = prompt.decode("utf-8")
|
|
197
|
+
except UnicodeDecodeError:
|
|
198
|
+
raise AdapterError("request_invalid") from None
|
|
199
|
+
requested = _model_name(requested_model)
|
|
200
|
+
expected = _model_name(expected_effective_model)
|
|
201
|
+
try:
|
|
202
|
+
normalized_effort = Effort(effort)
|
|
203
|
+
except (TypeError, ValueError):
|
|
204
|
+
raise AdapterError("request_invalid") from None
|
|
205
|
+
if normalized_effort not in _EXECUTION_EFFORTS:
|
|
206
|
+
raise AdapterError("request_invalid")
|
|
207
|
+
if response_format_type not in ("json_schema", "json_object"):
|
|
208
|
+
raise AdapterError("request_invalid")
|
|
209
|
+
if not isinstance(output_schema, dict) or not output_schema:
|
|
210
|
+
raise AdapterError("request_invalid")
|
|
211
|
+
if not is_supported_schema(output_schema):
|
|
212
|
+
raise AdapterError("request_invalid")
|
|
213
|
+
try:
|
|
214
|
+
schema_canonical = json.dumps(
|
|
215
|
+
output_schema, sort_keys=True, separators=(",", ":"), allow_nan=False
|
|
216
|
+
)
|
|
217
|
+
except (TypeError, ValueError, RecursionError):
|
|
218
|
+
raise AdapterError("request_invalid") from None
|
|
219
|
+
if len(schema_canonical.encode()) > MAX_OUTPUT_SCHEMA_BYTES:
|
|
220
|
+
raise AdapterError("request_invalid")
|
|
221
|
+
if (
|
|
222
|
+
not isinstance(output_schema_sha256, str)
|
|
223
|
+
or hashlib.sha256(schema_canonical.encode()).hexdigest() != output_schema_sha256
|
|
224
|
+
):
|
|
225
|
+
raise AdapterError("request_invalid")
|
|
226
|
+
if not isinstance(pricing, OpenRouterPricing):
|
|
227
|
+
raise AdapterError("request_invalid")
|
|
228
|
+
for value, maximum in (
|
|
229
|
+
(max_output_tokens, MAX_TOKEN_COUNT),
|
|
230
|
+
(max_cost_microunits, MAX_COST_MICROUNITS),
|
|
231
|
+
):
|
|
232
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= maximum:
|
|
233
|
+
raise AdapterError("request_invalid")
|
|
234
|
+
if (
|
|
235
|
+
isinstance(timeout_seconds, bool)
|
|
236
|
+
or not isinstance(timeout_seconds, (int, float))
|
|
237
|
+
or not math.isfinite(timeout_seconds)
|
|
238
|
+
or not 0.1 <= timeout_seconds <= MAX_INFERENCE_TIMEOUT_SECONDS
|
|
239
|
+
):
|
|
240
|
+
raise AdapterError("request_invalid")
|
|
241
|
+
if response_format_type == "json_schema":
|
|
242
|
+
response_format: dict[str, object] = {
|
|
243
|
+
"json_schema": {
|
|
244
|
+
"name": "graphite_response",
|
|
245
|
+
"schema": output_schema,
|
|
246
|
+
"strict": True,
|
|
247
|
+
},
|
|
248
|
+
"type": "json_schema",
|
|
249
|
+
}
|
|
250
|
+
else:
|
|
251
|
+
response_format = {"type": "json_object"}
|
|
252
|
+
payload = {
|
|
253
|
+
"max_tokens": max_output_tokens,
|
|
254
|
+
"messages": [{"content": prompt_text, "role": "user"}],
|
|
255
|
+
"model": requested,
|
|
256
|
+
"reasoning": {"effort": normalized_effort.value},
|
|
257
|
+
"response_format": response_format,
|
|
258
|
+
"stream": False,
|
|
259
|
+
"temperature": 0,
|
|
260
|
+
"usage": {"include": True},
|
|
261
|
+
}
|
|
262
|
+
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
|
263
|
+
if len(body) > MAX_INFERENCE_REQUEST_BYTES:
|
|
264
|
+
raise AdapterError("request_invalid")
|
|
265
|
+
endpoint = HttpProbeEndpoint(
|
|
266
|
+
LifecycleProviderId.OPENROUTER,
|
|
267
|
+
"https",
|
|
268
|
+
"openrouter.ai",
|
|
269
|
+
443,
|
|
270
|
+
ProbeEndpointPurpose.OPENROUTER_CHAT_COMPLETIONS,
|
|
271
|
+
)
|
|
272
|
+
try:
|
|
273
|
+
result = transport(
|
|
274
|
+
endpoint=endpoint,
|
|
275
|
+
timeout_seconds=timeout_seconds,
|
|
276
|
+
request_body=body,
|
|
277
|
+
authorization=f"Bearer {api_key}",
|
|
278
|
+
max_response_bytes=MAX_INFERENCE_RESPONSE_BYTES,
|
|
279
|
+
)
|
|
280
|
+
except ProviderProbeError as error:
|
|
281
|
+
raise AdapterError(
|
|
282
|
+
_EXECUTION_TRANSPORT_CODES.get(error.code, "unavailable")
|
|
283
|
+
) from None
|
|
284
|
+
except Exception:
|
|
285
|
+
raise AdapterError("unavailable") from None
|
|
286
|
+
if not isinstance(result, HttpProbeResult):
|
|
287
|
+
raise AdapterError("unavailable")
|
|
288
|
+
try:
|
|
289
|
+
envelope = json.loads(result.body.decode("utf-8"))
|
|
290
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
|
|
291
|
+
raise AdapterError("unavailable") from None
|
|
292
|
+
if not isinstance(envelope, dict):
|
|
293
|
+
raise AdapterError("protocol")
|
|
294
|
+
reported_model = envelope.get("model")
|
|
295
|
+
if not isinstance(reported_model, str) or not reported_model:
|
|
296
|
+
raise AdapterError("model_identity_unverified")
|
|
297
|
+
if reported_model != expected:
|
|
298
|
+
raise AdapterError("model_mismatch")
|
|
299
|
+
choices = envelope.get("choices")
|
|
300
|
+
if not isinstance(choices, list) or len(choices) != 1 or not isinstance(choices[0], dict):
|
|
301
|
+
raise AdapterError("protocol")
|
|
302
|
+
message = choices[0].get("message")
|
|
303
|
+
if not isinstance(message, dict):
|
|
304
|
+
raise AdapterError("protocol")
|
|
305
|
+
content = message.get("content")
|
|
306
|
+
if not isinstance(content, str):
|
|
307
|
+
raise AdapterError("protocol")
|
|
308
|
+
try:
|
|
309
|
+
structured = json.loads(content)
|
|
310
|
+
except (json.JSONDecodeError, RecursionError):
|
|
311
|
+
raise AdapterError("response_contract_invalid") from None
|
|
312
|
+
if not isinstance(structured, dict):
|
|
313
|
+
raise AdapterError("response_contract_invalid")
|
|
314
|
+
if not matches_schema(structured, output_schema):
|
|
315
|
+
raise AdapterError("response_contract_invalid")
|
|
316
|
+
usage = envelope.get("usage")
|
|
317
|
+
if not isinstance(usage, dict):
|
|
318
|
+
raise AdapterError("protocol")
|
|
319
|
+
input_tokens = _token(usage.get("prompt_tokens"))
|
|
320
|
+
output_tokens = _token(usage.get("completion_tokens"))
|
|
321
|
+
try:
|
|
322
|
+
cost = completion_cost_microunits(
|
|
323
|
+
pricing, input_tokens=input_tokens, output_tokens=output_tokens
|
|
324
|
+
)
|
|
325
|
+
except ProviderProbeError:
|
|
326
|
+
raise AdapterError("protocol") from None
|
|
327
|
+
if cost > max_cost_microunits:
|
|
328
|
+
raise AdapterError("cost_ceiling_exceeded")
|
|
329
|
+
return OpenRouterExecutionResult(
|
|
330
|
+
expected,
|
|
331
|
+
json.dumps(structured, sort_keys=True, separators=(",", ":")),
|
|
332
|
+
input_tokens,
|
|
333
|
+
output_tokens,
|
|
334
|
+
cost,
|
|
335
|
+
result.duration_seconds,
|
|
336
|
+
hashlib.sha256(body).hexdigest(),
|
|
337
|
+
hashlib.sha256(result.body).hexdigest(),
|
|
338
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Bounded, non-inference OpenRouter lifecycle observation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from decimal import Decimal, InvalidOperation
|
|
12
|
+
|
|
13
|
+
from .lifecycle import LifecycleProviderId, ProviderRuntimeIdentity, RuntimeKind
|
|
14
|
+
from .probe_runner import (
|
|
15
|
+
MAX_CATALOG_RESPONSE_BYTES,
|
|
16
|
+
HttpProbeEndpoint,
|
|
17
|
+
HttpProbeResult,
|
|
18
|
+
ProbeEndpointPurpose,
|
|
19
|
+
ProviderProbeError,
|
|
20
|
+
run_http_probe,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CANONICAL_ENDPOINT = "https://openrouter.ai/api/v1"
|
|
24
|
+
API_CONTRACT_VERSION = "1.0.0"
|
|
25
|
+
_MODEL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}/[A-Za-z0-9][A-Za-z0-9._:+-]{0,127}$")
|
|
26
|
+
_PRICE = re.compile(r"^(0|[0-9]{1,10}(\.[0-9]{1,18})?|\.[0-9]{1,18})$")
|
|
27
|
+
HttpProbe = Callable[..., HttpProbeResult]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _digest(value: object) -> str:
|
|
31
|
+
try:
|
|
32
|
+
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False).encode()
|
|
33
|
+
except (TypeError, ValueError, RecursionError):
|
|
34
|
+
raise ProviderProbeError("probe_request_invalid") from None
|
|
35
|
+
if len(encoded) > 16 * 1024:
|
|
36
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
37
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _json(result: HttpProbeResult) -> object:
|
|
41
|
+
if not isinstance(result, HttpProbeResult):
|
|
42
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(result.body.decode("utf-8"))
|
|
45
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
|
|
46
|
+
raise ProviderProbeError("probe_protocol_invalid") from None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class OpenRouterPricing:
|
|
51
|
+
"""Catalog-reported per-token USD prices as exact decimal strings."""
|
|
52
|
+
|
|
53
|
+
prompt: str
|
|
54
|
+
completion: str
|
|
55
|
+
|
|
56
|
+
def __post_init__(self) -> None:
|
|
57
|
+
for value in (self.prompt, self.completion):
|
|
58
|
+
if (
|
|
59
|
+
not isinstance(value, str)
|
|
60
|
+
or len(value) > 64
|
|
61
|
+
or _PRICE.fullmatch(value) is None
|
|
62
|
+
):
|
|
63
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
64
|
+
try:
|
|
65
|
+
parsed = Decimal(value)
|
|
66
|
+
except InvalidOperation:
|
|
67
|
+
raise ProviderProbeError("probe_protocol_invalid") from None
|
|
68
|
+
if not Decimal(0) <= parsed <= Decimal(1):
|
|
69
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def digest(self) -> str:
|
|
73
|
+
return _digest({"completion": self.completion, "prompt": self.prompt})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class OpenRouterObservation:
|
|
78
|
+
identity: ProviderRuntimeIdentity
|
|
79
|
+
pricing: OpenRouterPricing
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def completion_cost_microunits(
|
|
83
|
+
pricing: OpenRouterPricing, *, input_tokens: int, output_tokens: int
|
|
84
|
+
) -> int:
|
|
85
|
+
"""Ceiling of the exact-decimal USD cost expressed in microunits."""
|
|
86
|
+
if not isinstance(pricing, OpenRouterPricing):
|
|
87
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
88
|
+
for value in (input_tokens, output_tokens):
|
|
89
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 100_000_000:
|
|
90
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
91
|
+
cost = (
|
|
92
|
+
Decimal(input_tokens) * Decimal(pricing.prompt)
|
|
93
|
+
+ Decimal(output_tokens) * Decimal(pricing.completion)
|
|
94
|
+
) * Decimal(1_000_000)
|
|
95
|
+
whole = int(cost)
|
|
96
|
+
return whole if cost == whole else whole + 1
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _observe(
|
|
100
|
+
*, endpoint: str, api_key: str, model_id: str, routing_policy: Mapping[str, object],
|
|
101
|
+
observed_at: int, policy_version: str, timeout_seconds: float = 10.0,
|
|
102
|
+
transport: HttpProbe = run_http_probe, clock: Callable[[], float] = time.monotonic,
|
|
103
|
+
) -> tuple[ProviderRuntimeIdentity, dict[str, object]]:
|
|
104
|
+
if endpoint != CANONICAL_ENDPOINT:
|
|
105
|
+
raise ProviderProbeError("probe_endpoint_invalid")
|
|
106
|
+
if not isinstance(api_key, str) or not api_key or len(api_key) > 4096 or any(char in api_key for char in "\r\n\x00") or not isinstance(model_id, str) or _MODEL_ID.fullmatch(model_id) is None or not isinstance(routing_policy, Mapping):
|
|
107
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
108
|
+
if isinstance(timeout_seconds, bool) or not isinstance(timeout_seconds, (int, float)) or not math.isfinite(timeout_seconds) or not 0.1 <= timeout_seconds <= 30:
|
|
109
|
+
raise ProviderProbeError("probe_request_invalid")
|
|
110
|
+
routing_digest = _digest(dict(routing_policy))
|
|
111
|
+
deadline = clock() + float(timeout_seconds)
|
|
112
|
+
authorization = f"Bearer {api_key}"
|
|
113
|
+
|
|
114
|
+
def call(purpose: ProbeEndpointPurpose) -> object:
|
|
115
|
+
remaining = deadline - clock()
|
|
116
|
+
if remaining <= 0:
|
|
117
|
+
raise ProviderProbeError("probe_timeout")
|
|
118
|
+
target = HttpProbeEndpoint(LifecycleProviderId.OPENROUTER, "https", "openrouter.ai", 443, purpose)
|
|
119
|
+
allowance: dict[str, int] = (
|
|
120
|
+
{"max_response_bytes": MAX_CATALOG_RESPONSE_BYTES}
|
|
121
|
+
if purpose is ProbeEndpointPurpose.OPENROUTER_MODELS
|
|
122
|
+
else {}
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
result = transport(endpoint=target, timeout_seconds=remaining, authorization=authorization, **allowance)
|
|
126
|
+
except ProviderProbeError as exc:
|
|
127
|
+
if purpose is ProbeEndpointPurpose.OPENROUTER_AUTH_KEY and exc.code == "probe_http_status":
|
|
128
|
+
raise ProviderProbeError("probe_auth_unhealthy") from None
|
|
129
|
+
raise
|
|
130
|
+
except Exception:
|
|
131
|
+
raise ProviderProbeError("probe_failed") from None
|
|
132
|
+
return _json(result)
|
|
133
|
+
|
|
134
|
+
auth = call(ProbeEndpointPurpose.OPENROUTER_AUTH_KEY)
|
|
135
|
+
if not isinstance(auth, dict):
|
|
136
|
+
raise ProviderProbeError("probe_auth_unhealthy")
|
|
137
|
+
models = call(ProbeEndpointPurpose.OPENROUTER_MODELS)
|
|
138
|
+
data = models.get("data") if isinstance(models, dict) else None
|
|
139
|
+
if not isinstance(data, list) or len(data) > 2048:
|
|
140
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
141
|
+
matched = next(
|
|
142
|
+
(
|
|
143
|
+
item
|
|
144
|
+
for item in data
|
|
145
|
+
if isinstance(item, dict) and item.get("id") == model_id
|
|
146
|
+
),
|
|
147
|
+
None,
|
|
148
|
+
)
|
|
149
|
+
if matched is None:
|
|
150
|
+
raise ProviderProbeError("probe_model_unavailable")
|
|
151
|
+
try:
|
|
152
|
+
identity = ProviderRuntimeIdentity(LifecycleProviderId.OPENROUTER, RuntimeKind.REMOTE_HTTPS, API_CONTRACT_VERSION, hashlib.sha256(endpoint.encode("ascii")).hexdigest(), _digest(model_id), routing_digest, ("credential_health", "models_metadata"), policy_version, observed_at)
|
|
153
|
+
except ValueError:
|
|
154
|
+
raise ProviderProbeError("probe_request_invalid") from None
|
|
155
|
+
return identity, matched
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def observe_openrouter(
|
|
159
|
+
*, endpoint: str, api_key: str, model_id: str, routing_policy: Mapping[str, object],
|
|
160
|
+
observed_at: int, policy_version: str, timeout_seconds: float = 10.0,
|
|
161
|
+
transport: HttpProbe = run_http_probe, clock: Callable[[], float] = time.monotonic,
|
|
162
|
+
) -> ProviderRuntimeIdentity:
|
|
163
|
+
"""Observe exact OpenRouter auth/model metadata; never invoke inference."""
|
|
164
|
+
return _observe(
|
|
165
|
+
endpoint=endpoint, api_key=api_key, model_id=model_id,
|
|
166
|
+
routing_policy=routing_policy, observed_at=observed_at,
|
|
167
|
+
policy_version=policy_version, timeout_seconds=timeout_seconds,
|
|
168
|
+
transport=transport, clock=clock,
|
|
169
|
+
)[0]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def observe_openrouter_with_pricing(
|
|
173
|
+
*, endpoint: str, api_key: str, model_id: str, routing_policy: Mapping[str, object],
|
|
174
|
+
observed_at: int, policy_version: str, timeout_seconds: float = 10.0,
|
|
175
|
+
transport: HttpProbe = run_http_probe, clock: Callable[[], float] = time.monotonic,
|
|
176
|
+
) -> OpenRouterObservation:
|
|
177
|
+
"""Observe auth/model metadata and bind the catalog's exact pricing; never invoke inference."""
|
|
178
|
+
identity, entry = _observe(
|
|
179
|
+
endpoint=endpoint, api_key=api_key, model_id=model_id,
|
|
180
|
+
routing_policy=routing_policy, observed_at=observed_at,
|
|
181
|
+
policy_version=policy_version, timeout_seconds=timeout_seconds,
|
|
182
|
+
transport=transport, clock=clock,
|
|
183
|
+
)
|
|
184
|
+
raw = entry.get("pricing")
|
|
185
|
+
if not isinstance(raw, dict):
|
|
186
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
187
|
+
pricing = OpenRouterPricing(prompt=raw.get("prompt"), completion=raw.get("completion"))
|
|
188
|
+
return OpenRouterObservation(identity, pricing)
|