execweave 0.6.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 (49) hide show
  1. execweave/__init__.py +3 -0
  2. execweave/__main__.py +5 -0
  3. execweave/analysis.py +406 -0
  4. execweave/backends.py +63 -0
  5. execweave/benchmark.py +80 -0
  6. execweave/claude_adapter.py +448 -0
  7. execweave/claude_hook_cli.py +101 -0
  8. execweave/claude_record.py +106 -0
  9. execweave/cli.py +588 -0
  10. execweave/codex_adapter.py +314 -0
  11. execweave/codex_hook_cli.py +98 -0
  12. execweave/codex_record.py +111 -0
  13. execweave/collector.py +301 -0
  14. execweave/correlation.py +604 -0
  15. execweave/cursor_adapter.py +347 -0
  16. execweave/cursor_hook_cli.py +82 -0
  17. execweave/cursor_record.py +96 -0
  18. execweave/filesystem.py +103 -0
  19. execweave/focus.py +118 -0
  20. execweave/gemini_adapter.py +265 -0
  21. execweave/gemini_hook_cli.py +77 -0
  22. execweave/gemini_record.py +94 -0
  23. execweave/graph.py +300 -0
  24. execweave/graph_ops.py +446 -0
  25. execweave/inference_gateway.py +422 -0
  26. execweave/inference_gateway_cli.py +106 -0
  27. execweave/inference_identity.py +76 -0
  28. execweave/inference_identity_cli.py +60 -0
  29. execweave/live.py +275 -0
  30. execweave/model_runtime.py +535 -0
  31. execweave/model_runtime_cli.py +154 -0
  32. execweave/opencode_adapter.py +316 -0
  33. execweave/opencode_hook_cli.py +57 -0
  34. execweave/opencode_plugin_cli.py +110 -0
  35. execweave/opencode_record.py +96 -0
  36. execweave/overhead_benchmark.py +440 -0
  37. execweave/provider_record.py +215 -0
  38. execweave/schema.py +62 -0
  39. execweave/semantic.py +346 -0
  40. execweave/sink.py +33 -0
  41. execweave/strace_backend.py +682 -0
  42. execweave/validate.py +193 -0
  43. execweave/viewer.py +283 -0
  44. execweave/workflow.py +114 -0
  45. execweave-0.6.0.dist-info/METADATA +356 -0
  46. execweave-0.6.0.dist-info/RECORD +49 -0
  47. execweave-0.6.0.dist-info/WHEEL +4 -0
  48. execweave-0.6.0.dist-info/entry_points.txt +17 -0
  49. execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import time
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from urllib.parse import urlsplit, urlunsplit
11
+
12
+
13
+ def _now() -> str:
14
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
15
+
16
+
17
+ def sanitize_gateway_endpoint(endpoint: str) -> str:
18
+ split = urlsplit(endpoint)
19
+ if split.scheme not in {"http", "https"} or not split.hostname:
20
+ raise ValueError("inference gateway endpoint must be an http(s) URL")
21
+ host = split.hostname
22
+ if ":" in host and not host.startswith("["):
23
+ host = f"[{host}]"
24
+ if split.port is not None:
25
+ host = f"{host}:{split.port}"
26
+ path = split.path.rstrip("/")
27
+ return urlunsplit((split.scheme, host, path, "", ""))
28
+
29
+
30
+ def _entity(
31
+ entity_type: str,
32
+ entity_id: str,
33
+ *,
34
+ name: str | None = None,
35
+ attributes: dict[str, Any] | None = None,
36
+ ) -> dict[str, Any]:
37
+ return {"type": entity_type, "id": entity_id, "name": name, "attributes": attributes or {}}
38
+
39
+
40
+ def _gateway_entity(gateway: str, endpoint: str) -> dict[str, Any]:
41
+ safe_endpoint = sanitize_gateway_endpoint(endpoint)
42
+ digest = hashlib.sha256(safe_endpoint.encode("utf-8")).hexdigest()[:24]
43
+ return _entity(
44
+ "inference_gateway",
45
+ f"inference-gateway:{gateway}:{digest}",
46
+ name=gateway,
47
+ attributes={"gateway": gateway, "endpoint": safe_endpoint},
48
+ )
49
+
50
+
51
+ def _model_entity(model: str) -> dict[str, Any]:
52
+ return _entity(
53
+ "model",
54
+ f"model:catalog:{model}",
55
+ name=model,
56
+ attributes={"catalog_id": model},
57
+ )
58
+
59
+
60
+ def _provider_entity(provider: str) -> dict[str, Any]:
61
+ slug = hashlib.sha256(provider.encode("utf-8", errors="replace")).hexdigest()[:24]
62
+ return _entity(
63
+ "inference_provider",
64
+ f"inference-provider:{slug}",
65
+ name=provider,
66
+ attributes={"provider_name": provider},
67
+ )
68
+
69
+
70
+ def _deployment_entity(gateway: str, deployment: str) -> dict[str, Any]:
71
+ slug = hashlib.sha256(deployment.encode("utf-8", errors="replace")).hexdigest()[:24]
72
+ return _entity(
73
+ "inference_deployment",
74
+ f"inference-deployment:{gateway}:{slug}",
75
+ name=deployment,
76
+ attributes={"gateway": gateway, "deployment_id": deployment},
77
+ )
78
+
79
+
80
+ def _event(
81
+ *,
82
+ timestamp: str,
83
+ event_type: str,
84
+ relation: str,
85
+ source: dict[str, Any],
86
+ target: dict[str, Any],
87
+ gateway: str,
88
+ attributes: dict[str, Any] | None = None,
89
+ ) -> dict[str, Any]:
90
+ merged = {
91
+ "backend": "inference_gateway",
92
+ "attribution": "gateway_api",
93
+ "evidence_source": "gateway_response",
94
+ "gateway": gateway,
95
+ "causal": False,
96
+ }
97
+ if attributes:
98
+ merged.update(attributes)
99
+ return {
100
+ "timestamp": timestamp,
101
+ "event_type": event_type,
102
+ "relation": relation,
103
+ "source": source,
104
+ "target": target,
105
+ "attributes": merged,
106
+ }
107
+
108
+
109
+ def _request_id(payload: dict[str, Any], explicit: str | None) -> str:
110
+ if explicit:
111
+ return explicit
112
+ native = payload.get("id")
113
+ if isinstance(native, str) and native:
114
+ return native
115
+ seed = {
116
+ "model": payload.get("model"),
117
+ "created": payload.get("created"),
118
+ "usage": payload.get("usage"),
119
+ }
120
+ raw = json.dumps(seed, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
121
+ return hashlib.sha256(raw.encode("utf-8", errors="replace")).hexdigest()[:32]
122
+
123
+
124
+ def _copy_int(mapping: dict[str, Any], source: str, target: str, attrs: dict[str, Any]) -> None:
125
+ value = mapping.get(source)
126
+ if isinstance(value, int) and not isinstance(value, bool) and target not in attrs:
127
+ attrs[target] = value
128
+
129
+
130
+ def _usage_attributes(payload: dict[str, Any]) -> dict[str, Any]:
131
+ attrs: dict[str, Any] = {"protocol": "openai_compatible"}
132
+ usage = payload.get("usage")
133
+ if not isinstance(usage, dict):
134
+ return attrs
135
+ for source, target in (
136
+ ("prompt_tokens", "prompt_tokens"),
137
+ ("input_tokens", "prompt_tokens"),
138
+ ("completion_tokens", "completion_tokens"),
139
+ ("output_tokens", "completion_tokens"),
140
+ ("total_tokens", "total_tokens"),
141
+ ):
142
+ _copy_int(usage, source, target, attrs)
143
+ cost = usage.get("cost")
144
+ if isinstance(cost, (int, float)) and not isinstance(cost, bool):
145
+ attrs["cost_usd"] = float(cost)
146
+ for detail_key in ("prompt_tokens_details", "input_tokens_details"):
147
+ details = usage.get(detail_key)
148
+ if not isinstance(details, dict):
149
+ continue
150
+ for source, target in (
151
+ ("cached_tokens", "cached_prompt_tokens"),
152
+ ("cache_write_tokens", "cache_write_tokens"),
153
+ ):
154
+ _copy_int(details, source, target, attrs)
155
+ for detail_key in ("completion_tokens_details", "output_tokens_details"):
156
+ details = usage.get(detail_key)
157
+ if isinstance(details, dict):
158
+ _copy_int(details, "reasoning_tokens", "reasoning_tokens", attrs)
159
+ for source, target in (
160
+ ("cache_creation_input_tokens", "cache_write_tokens"),
161
+ ("cache_read_input_tokens", "cached_prompt_tokens"),
162
+ ):
163
+ _copy_int(usage, source, target, attrs)
164
+ return attrs
165
+
166
+
167
+ def gateway_response_to_events(
168
+ payload: dict[str, Any],
169
+ *,
170
+ gateway_name: str,
171
+ endpoint: str,
172
+ requested_model: str | None = None,
173
+ resolved_model: str | None = None,
174
+ provider_name: str | None = None,
175
+ deployment_id: str | None = None,
176
+ request_id: str | None = None,
177
+ timestamp: str | None = None,
178
+ ) -> list[dict[str, Any]]:
179
+ observed_at = timestamp or _now()
180
+ gateway = _gateway_entity(gateway_name, endpoint)
181
+ native_id = _request_id(payload, request_id)
182
+ response_model = payload.get("model")
183
+ resolved = resolved_model
184
+ if not isinstance(resolved, str) or not resolved:
185
+ resolved = response_model if isinstance(response_model, str) and response_model else None
186
+
187
+ attrs = _usage_attributes(payload)
188
+ if isinstance(requested_model, str) and requested_model:
189
+ attrs["requested_model"] = requested_model
190
+ if isinstance(resolved, str) and resolved:
191
+ attrs["resolved_model"] = resolved
192
+ if isinstance(provider_name, str) and provider_name:
193
+ attrs["provider_name"] = provider_name
194
+ if isinstance(deployment_id, str) and deployment_id:
195
+ attrs["deployment_id"] = deployment_id
196
+
197
+ request = _entity(
198
+ "inference_request",
199
+ f"inference-request:{gateway_name}:{native_id}",
200
+ name=native_id,
201
+ attributes={"gateway": gateway_name, **attrs},
202
+ )
203
+ events = [
204
+ _event(
205
+ timestamp=observed_at,
206
+ event_type=f"inference_gateway.{gateway_name}.response.observed",
207
+ relation="SERVED_INFERENCE",
208
+ source=gateway,
209
+ target=request,
210
+ gateway=gateway_name,
211
+ attributes=attrs,
212
+ )
213
+ ]
214
+ if isinstance(requested_model, str) and requested_model:
215
+ events.append(
216
+ _event(
217
+ timestamp=observed_at,
218
+ event_type=f"inference_gateway.{gateway_name}.model.requested",
219
+ relation="REQUESTED_MODEL",
220
+ source=request,
221
+ target=_model_entity(requested_model),
222
+ gateway=gateway_name,
223
+ attributes=attrs,
224
+ )
225
+ )
226
+ if isinstance(resolved, str) and resolved:
227
+ events.append(
228
+ _event(
229
+ timestamp=observed_at,
230
+ event_type=f"inference_gateway.{gateway_name}.model.resolved",
231
+ relation="ROUTED_TO_MODEL",
232
+ source=request,
233
+ target=_model_entity(resolved),
234
+ gateway=gateway_name,
235
+ attributes=attrs,
236
+ )
237
+ )
238
+ if isinstance(provider_name, str) and provider_name:
239
+ events.append(
240
+ _event(
241
+ timestamp=observed_at,
242
+ event_type=f"inference_gateway.{gateway_name}.provider.resolved",
243
+ relation="ROUTED_TO_PROVIDER",
244
+ source=request,
245
+ target=_provider_entity(provider_name),
246
+ gateway=gateway_name,
247
+ attributes=attrs,
248
+ )
249
+ )
250
+ if isinstance(deployment_id, str) and deployment_id:
251
+ events.append(
252
+ _event(
253
+ timestamp=observed_at,
254
+ event_type=f"inference_gateway.{gateway_name}.deployment.resolved",
255
+ relation="ROUTED_TO_DEPLOYMENT",
256
+ source=request,
257
+ target=_deployment_entity(gateway_name, deployment_id),
258
+ gateway=gateway_name,
259
+ attributes=attrs,
260
+ )
261
+ )
262
+ return events
263
+
264
+
265
+ def openrouter_response_to_events(
266
+ payload: dict[str, Any],
267
+ *,
268
+ requested_model: str | None = None,
269
+ resolved_model: str | None = None,
270
+ provider_name: str | None = None,
271
+ deployment_id: str | None = None,
272
+ endpoint: str = "https://openrouter.ai/api/v1",
273
+ request_id: str | None = None,
274
+ timestamp: str | None = None,
275
+ ) -> list[dict[str, Any]]:
276
+ return gateway_response_to_events(
277
+ payload,
278
+ gateway_name="openrouter",
279
+ endpoint=endpoint,
280
+ requested_model=requested_model,
281
+ resolved_model=resolved_model,
282
+ provider_name=provider_name,
283
+ deployment_id=deployment_id,
284
+ request_id=request_id,
285
+ timestamp=timestamp,
286
+ )
287
+
288
+
289
+ def litellm_response_to_events(
290
+ payload: dict[str, Any],
291
+ *,
292
+ requested_model: str | None = None,
293
+ resolved_model: str | None = None,
294
+ provider_name: str | None = None,
295
+ deployment_id: str | None = None,
296
+ endpoint: str = "http://localhost:4000",
297
+ request_id: str | None = None,
298
+ timestamp: str | None = None,
299
+ ) -> list[dict[str, Any]]:
300
+ return gateway_response_to_events(
301
+ payload,
302
+ gateway_name="litellm",
303
+ endpoint=endpoint,
304
+ requested_model=requested_model,
305
+ resolved_model=resolved_model,
306
+ provider_name=provider_name,
307
+ deployment_id=deployment_id,
308
+ request_id=request_id,
309
+ timestamp=timestamp,
310
+ )
311
+
312
+
313
+ def openrouter_generation_to_events(
314
+ payload: dict[str, Any],
315
+ *,
316
+ endpoint: str = "https://openrouter.ai/api/v1",
317
+ timestamp: str | None = None,
318
+ ) -> list[dict[str, Any]]:
319
+ data = payload.get("data") if isinstance(payload.get("data"), dict) else payload
320
+ if not isinstance(data, dict):
321
+ raise ValueError("OpenRouter generation payload must be a JSON object")
322
+ generation_id = data.get("id")
323
+ if not isinstance(generation_id, str) or not generation_id:
324
+ raise ValueError("OpenRouter generation metadata requires id")
325
+ observed_at = timestamp or _now()
326
+ gateway = _gateway_entity("openrouter", endpoint)
327
+
328
+ attrs: dict[str, Any] = {"protocol": "openrouter_generation"}
329
+ for key in (
330
+ "latency",
331
+ "generation_time",
332
+ "total_cost",
333
+ "tokens_prompt",
334
+ "tokens_completion",
335
+ "native_tokens_prompt",
336
+ "native_tokens_completion",
337
+ ):
338
+ value = data.get(key)
339
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
340
+ attrs[key] = value
341
+ for key in ("streamed", "cancelled"):
342
+ value = data.get(key)
343
+ if isinstance(value, bool):
344
+ attrs[key] = value
345
+
346
+ request = _entity(
347
+ "inference_request",
348
+ f"inference-request:openrouter:{generation_id}",
349
+ name=generation_id,
350
+ attributes={"gateway": "openrouter", **attrs},
351
+ )
352
+ events = [
353
+ _event(
354
+ timestamp=observed_at,
355
+ event_type="inference_gateway.openrouter.generation.observed",
356
+ relation="REPORTED_GENERATION_METADATA",
357
+ source=gateway,
358
+ target=request,
359
+ gateway="openrouter",
360
+ attributes=attrs,
361
+ )
362
+ ]
363
+
364
+ model = data.get("model") or data.get("model_name")
365
+ if isinstance(model, str) and model:
366
+ events.append(
367
+ _event(
368
+ timestamp=observed_at,
369
+ event_type="inference_gateway.openrouter.model.resolved",
370
+ relation="ROUTED_TO_MODEL",
371
+ source=request,
372
+ target=_model_entity(model),
373
+ gateway="openrouter",
374
+ attributes=attrs,
375
+ )
376
+ )
377
+ provider = data.get("provider_name") or data.get("provider")
378
+ if isinstance(provider, str) and provider:
379
+ events.append(
380
+ _event(
381
+ timestamp=observed_at,
382
+ event_type="inference_gateway.openrouter.provider.resolved",
383
+ relation="ROUTED_TO_PROVIDER",
384
+ source=request,
385
+ target=_provider_entity(provider),
386
+ gateway="openrouter",
387
+ attributes=attrs,
388
+ )
389
+ )
390
+ return events
391
+
392
+
393
+ def append_gateway_records(path: str | Path, records: list[dict[str, Any]]) -> Path:
394
+ output = Path(path).expanduser().resolve()
395
+ output.parent.mkdir(parents=True, exist_ok=True)
396
+ if not records:
397
+ return output
398
+ blob = "".join(
399
+ json.dumps(record, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
400
+ for record in records
401
+ )
402
+ lock_dir = output.with_name(output.name + ".lock")
403
+ deadline = time.monotonic() + 5.0
404
+ while True:
405
+ try:
406
+ lock_dir.mkdir()
407
+ break
408
+ except FileExistsError:
409
+ if time.monotonic() >= deadline:
410
+ raise TimeoutError(f"timed out waiting for inference gateway sidecar lock: {lock_dir}")
411
+ time.sleep(0.01)
412
+ try:
413
+ with output.open("a", encoding="utf-8", newline="\n") as handle:
414
+ handle.write(blob)
415
+ handle.flush()
416
+ os.fsync(handle.fileno())
417
+ finally:
418
+ try:
419
+ lock_dir.rmdir()
420
+ except OSError:
421
+ pass
422
+ return output
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .inference_gateway import (
10
+ append_gateway_records,
11
+ litellm_response_to_events,
12
+ openrouter_generation_to_events,
13
+ openrouter_response_to_events,
14
+ sanitize_gateway_endpoint,
15
+ )
16
+
17
+ _GATEWAYS = ("openrouter", "litellm")
18
+
19
+
20
+ def _sidecar(value: Path | None) -> Path:
21
+ if value is not None:
22
+ return value
23
+ configured = os.environ.get("EXECWEAVE_SEMANTIC_SIDECAR")
24
+ if configured:
25
+ return Path(configured)
26
+ raise ValueError("--sidecar or EXECWEAVE_SEMANTIC_SIDECAR is required")
27
+
28
+
29
+ def _read_json_stdin() -> dict:
30
+ raw = sys.stdin.read()
31
+ if not raw.strip():
32
+ raise ValueError("stdin is empty")
33
+ try:
34
+ payload = json.loads(raw)
35
+ except json.JSONDecodeError as exc:
36
+ raise ValueError(f"stdin is invalid JSON: {exc.msg}") from exc
37
+ if not isinstance(payload, dict):
38
+ raise ValueError("stdin must contain one JSON object")
39
+ return payload
40
+
41
+
42
+ def _default_endpoint(gateway: str) -> str:
43
+ return {
44
+ "openrouter": "https://openrouter.ai/api/v1",
45
+ "litellm": "http://localhost:4000",
46
+ }[gateway]
47
+
48
+
49
+ def build_parser() -> argparse.ArgumentParser:
50
+ parser = argparse.ArgumentParser(
51
+ prog="execweave-inference-gateway",
52
+ description="Capture inference gateway routing/usage metadata without prompt or response content.",
53
+ )
54
+ sub = parser.add_subparsers(dest="command", required=True)
55
+
56
+ event = sub.add_parser("event", help="Convert one final gateway response into metadata events.")
57
+ event.add_argument("--gateway", choices=_GATEWAYS, required=True)
58
+ event.add_argument("--endpoint", default=None)
59
+ event.add_argument("--requested-model", default=None)
60
+ event.add_argument("--resolved-model", default=None)
61
+ event.add_argument("--provider-name", default=None)
62
+ event.add_argument("--deployment-id", default=None)
63
+ event.add_argument("--request-id", default=None)
64
+ event.add_argument("--sidecar", type=Path, default=None)
65
+
66
+ generation = sub.add_parser(
67
+ "generation",
68
+ help="Convert OpenRouter generation metadata into routing/performance events.",
69
+ )
70
+ generation.add_argument("--gateway", choices=["openrouter"], required=True)
71
+ generation.add_argument("--endpoint", default=None)
72
+ generation.add_argument("--sidecar", type=Path, default=None)
73
+ return parser
74
+
75
+
76
+ def main(argv: list[str] | None = None) -> int:
77
+ parser = build_parser()
78
+ args = parser.parse_args(argv)
79
+ try:
80
+ payload = _read_json_stdin()
81
+ endpoint = sanitize_gateway_endpoint(args.endpoint or _default_endpoint(args.gateway))
82
+ if args.command == "event":
83
+ converters = {
84
+ "openrouter": openrouter_response_to_events,
85
+ "litellm": litellm_response_to_events,
86
+ }
87
+ records = converters[args.gateway](
88
+ payload,
89
+ requested_model=args.requested_model,
90
+ resolved_model=args.resolved_model,
91
+ provider_name=args.provider_name,
92
+ deployment_id=args.deployment_id,
93
+ endpoint=endpoint,
94
+ request_id=args.request_id,
95
+ )
96
+ else:
97
+ records = openrouter_generation_to_events(payload, endpoint=endpoint)
98
+ output = append_gateway_records(_sidecar(args.sidecar), records)
99
+ except (OSError, TimeoutError, ValueError) as exc:
100
+ parser.error(str(exc))
101
+ print(json.dumps({"records": len(records), "sidecar": str(output)}, sort_keys=True))
102
+ return 0
103
+
104
+
105
+ if __name__ == "__main__":
106
+ raise SystemExit(main())
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+
8
+ def _now() -> str:
9
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
10
+
11
+
12
+ def _entity(
13
+ entity_type: str,
14
+ entity_id: str,
15
+ *,
16
+ name: str | None = None,
17
+ attributes: dict[str, Any] | None = None,
18
+ ) -> dict[str, Any]:
19
+ return {"type": entity_type, "id": entity_id, "name": name, "attributes": attributes or {}}
20
+
21
+
22
+ def _required(value: str, *, field: str) -> str:
23
+ if not isinstance(value, str) or not value.strip():
24
+ raise ValueError(f"{field} must be a non-empty string")
25
+ return value
26
+
27
+
28
+ def gateway_runtime_identity_event(
29
+ *,
30
+ gateway: str,
31
+ gateway_request_id: str,
32
+ runtime: str,
33
+ runtime_request_id: str,
34
+ shared_request_id: str,
35
+ timestamp: str | None = None,
36
+ ) -> dict[str, Any]:
37
+ gateway = _required(gateway, field="gateway")
38
+ gateway_request_id = _required(gateway_request_id, field="gateway_request_id")
39
+ runtime = _required(runtime, field="runtime")
40
+ runtime_request_id = _required(runtime_request_id, field="runtime_request_id")
41
+ shared_request_id = _required(shared_request_id, field="shared_request_id")
42
+
43
+ shared_hash = hashlib.sha256(
44
+ shared_request_id.encode("utf-8", errors="replace")
45
+ ).hexdigest()[:32]
46
+ source = _entity(
47
+ "inference_request",
48
+ f"inference-request:{gateway}:{gateway_request_id}",
49
+ name=gateway_request_id,
50
+ attributes={"gateway": gateway},
51
+ )
52
+ target = _entity(
53
+ "inference_request",
54
+ f"inference-request:{runtime}:{runtime_request_id}",
55
+ name=runtime_request_id,
56
+ attributes={"provider": runtime},
57
+ )
58
+ return {
59
+ "timestamp": timestamp or _now(),
60
+ "event_type": "inference_identity.gateway_runtime.exact",
61
+ "relation": "SAME_INFERENCE_REQUEST",
62
+ "source": source,
63
+ "target": target,
64
+ "attributes": {
65
+ "backend": "cross_layer_identity",
66
+ "attribution": "explicit_shared_request_id",
67
+ "evidence_source": "caller_supplied_shared_request_id",
68
+ "gateway": gateway,
69
+ "runtime": runtime,
70
+ "causal": False,
71
+ "inferred": False,
72
+ "identity_exact": True,
73
+ "identity_method": "shared_request_id",
74
+ "shared_request_id_hash": shared_hash,
75
+ },
76
+ }
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from .inference_gateway import append_gateway_records
9
+ from .inference_identity import gateway_runtime_identity_event
10
+
11
+ _GATEWAYS = ("openrouter", "litellm")
12
+ _RUNTIMES = ("ollama", "llamacpp", "vllm", "lmstudio")
13
+
14
+
15
+ def _sidecar(value: Path | None) -> Path:
16
+ if value is not None:
17
+ return value
18
+ configured = os.environ.get("EXECWEAVE_SEMANTIC_SIDECAR")
19
+ if configured:
20
+ return Path(configured)
21
+ raise ValueError("--sidecar or EXECWEAVE_SEMANTIC_SIDECAR is required")
22
+
23
+
24
+ def build_parser() -> argparse.ArgumentParser:
25
+ parser = argparse.ArgumentParser(
26
+ prog="execweave-inference-link",
27
+ description=(
28
+ "Link gateway and model-runtime request nodes only when an explicit shared request "
29
+ "identity is available."
30
+ ),
31
+ )
32
+ parser.add_argument("--gateway", choices=_GATEWAYS, required=True)
33
+ parser.add_argument("--gateway-request-id", required=True)
34
+ parser.add_argument("--runtime", choices=_RUNTIMES, required=True)
35
+ parser.add_argument("--runtime-request-id", required=True)
36
+ parser.add_argument("--shared-request-id", required=True)
37
+ parser.add_argument("--sidecar", type=Path, default=None)
38
+ return parser
39
+
40
+
41
+ def main(argv: list[str] | None = None) -> int:
42
+ parser = build_parser()
43
+ args = parser.parse_args(argv)
44
+ try:
45
+ record = gateway_runtime_identity_event(
46
+ gateway=args.gateway,
47
+ gateway_request_id=args.gateway_request_id,
48
+ runtime=args.runtime,
49
+ runtime_request_id=args.runtime_request_id,
50
+ shared_request_id=args.shared_request_id,
51
+ )
52
+ output = append_gateway_records(_sidecar(args.sidecar), [record])
53
+ except (OSError, TimeoutError, ValueError) as exc:
54
+ parser.error(str(exc))
55
+ print(json.dumps({"records": 1, "sidecar": str(output)}, sort_keys=True))
56
+ return 0
57
+
58
+
59
+ if __name__ == "__main__":
60
+ raise SystemExit(main())