OpenTelemetry GenAI export — CYBERINE_OTEL_ENDPOINT (kyv0t)
run.json/events.jsonl is a proprietary, Cyberine-only format with no interop. Setting CYBERINEOTELENDPOINT turns on an exporter that maps RunStore run/step/worker events onto spans and ships them...
run.json/events.jsonl is a proprietary, Cyberine-only format with no interop. Setting
CYBERINE_OTEL_ENDPOINT turns on an exporter that maps RunStore run/step/worker events onto
OpenTelemetry GenAI semantic-convention
spans and ships them over OTLP/HTTP to any collector (Jaeger, Tempo, an OTel Collector, a SaaS
backend) — so a run's fleet dispatch shows up in whatever observability stack you already run,
next to every other GenAI-instrumented service.
Convention version targeted: semantic-conventions-genai v1.44.0
(docs/gen-ai/gen-ai-spans.md), read live from the spec repo on 2026-09-03. That repo's own
status badge marks GenAI spans "Development" (pre-1.0/experimental) — attribute names are
liable to change again upstream. GENAI_SEMCONV_VERSION in src/store/otelGenaiExport.ts names
the exact version this exporter targets so a future drift shows up as "the spec moved", not as an
unexplained mismatch.
Off by default, zero cost when off. CYBERINE_OTEL_ENDPOINT unset (the default) means: no
@opentelemetry/* package is ever import()-ed, no network call is made, no timer or
process-exit hook is registered — RunStore pays for one process.env read and nothing else.
Every @opentelemetry/* import in src/store/otelGenaiExport.ts lives inside
RunOtelTracker.ensureSdk(), reached only once the endpoint is actually set.
# 1. Point Cyberine at any OTLP/HTTP collector (this exporter posts JSON to <endpoint>/v1/traces)
export CYBERINE_OTEL_ENDPOINT=http://localhost:4318
# 2. Run anything that writes a run.json (fleet dispatch, a workflow, chat /run, ...)
cyberine run --bg "add input validation to the signup form"
# 3. Spans land at the collector as they complete: one root span per run ("cyberine.run"), one
# child span per step ("cyberine.step"), and one gen_ai span per completed worker
# ("chat <model>"), nested under its step.Attributes emitted, per completed worker span (see buildWorkerSpanSpec() in
src/store/otelGenaiExport.ts for the pure, SDK-free mapping this is built from):
| Attribute | Source |
|---|---|
gen_ai.provider.name | WorkerRun.provider's label, normalised onto the spec's recommended enum where a direct match exists (deepseek, openai, anthropic, azure.ai.openai, aws.bedrock, gcp.vertex_ai, ...); any other label (the headless CLI wrappers — claude, codex, opencode, gemini, ...) passes through verbatim |
gen_ai.operation.name | always chat — every dispatched worker here is a chat/agentic completion |
gen_ai.request.model / gen_ai.response.model | RoutingSnapshot.candidates[].resolvedModel (kc0nz's routing snapshot, above) when the run has one, else parsed off WorkerRun.provider's existing "<label>:<model>" convention |
gen_ai.response.finish_reasons | ['stop'] (done), ['error'] (error state or an error string present), ['cancelled'] (Cyberine's own terminal state — not in the spec's example list, kept as-is rather than misreported as stop) |
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens | WorkerRun.usage.inputTokens / .outputTokens |
error.type | only on an errored worker; a short, low-cardinality token (first word of the error string) — never the full message |
cyberine.cost.usd | non-standard, custom — WorkerRun.usage.costUsd. Cost has no GenAI-spec attribute as of v1.44.0; namespaced under cyberine. so it is never mistaken for a spec field |
cyberine.run.id / cyberine.step.id / cyberine.agent.id | correlation back to the run's run.json |
Span name: "{gen_ai.operation.name} {gen_ai.request.model}" (the spec's own naming convention),
e.g. "chat deepseek-v4-flash". Span kind: CLIENT.
Known limitations and deferred work:
- Only fleet-dispatch
WorkerRuncompletions are exported. Auxiliary LLM calls (the judges, the smart router,consult_oracle) are NOT — they never appear asgen_ai.*spans today. - No embeddings/retrieval/tool-call spans — every worker maps to a single
chatoperation span, matching the current fleet (there is no embeddings/retrieval worker kind in this codebase). gen_ai.response.modelis never distinguished fromgen_ai.request.model— no adapter surfaces a provider-ECHOED response model id distinct from what was requested.- No prompt/response content capture (the spec's opt-in
gen_ai.input.messages/gen_ai.output.messages) — deliberately not implemented, so this feature can never ship prompt/response text to a collector by default. - Best-effort, like every other
RunStoreside-channel (the message bus, presence, cc-memory handoff): a span-emission failure is caught and logged, never thrown into the run path. - Delivery uses
SimpleSpanProcessor(exports each span individually, no batching) rather thanBatchSpanProcessor— right for this feature's low span volume per run, and it means no background flush timer is ever registered, but a very high-volume deployment would want to switch processors.
Regression guard: npm run selftest:otel-genai-export — asserts the pure mapping
(buildWorkerSpanSpec, deriveProviderAndModel, mapFinishReasons) attribute-by-attribute for
both a successful and an errored worker, then stands up a real local HTTP listener as a stand-in
OTLP collector and runs the SAME synthetic RunStore lifecycle twice: once with
CYBERINE_OTEL_ENDPOINT unset (asserts the listener receives ZERO requests and the tracker is
null) and once with it pointed at that listener (asserts >=4 real OTLP/HTTP JSON POSTs arrive
and decodes every gen_ai.* attribute off the wire, not just off the in-memory spec object).
Routing-explain — why a worker was picked or skipped (kc0nz)
Routing used to be a black box once the decision was made: reasons[] explained the winning fleet, but nothing recorded which OTHER candidates existed, or why a worker never even reached the router...
Swarm mode — cyberine run --mode swarm (plan -> parallel-chunk implement -> merge)
For tasks that decompose cleanly into disjoint chunks — large multi-file refactors, sweep updates across many files, any scope where one worker would burn tool rounds but N workers can split the...