cyberine
UsageRun recipes

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):

AttributeSource
gen_ai.provider.nameWorkerRun.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.namealways chat — every dispatched worker here is a chat/agentic completion
gen_ai.request.model / gen_ai.response.modelRoutingSnapshot.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_tokensWorkerRun.usage.inputTokens / .outputTokens
error.typeonly on an errored worker; a short, low-cardinality token (first word of the error string) — never the full message
cyberine.cost.usdnon-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.idcorrelation 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 WorkerRun completions are exported. Auxiliary LLM calls (the judges, the smart router, consult_oracle) are NOT — they never appear as gen_ai.* spans today.
  • No embeddings/retrieval/tool-call spans — every worker maps to a single chat operation span, matching the current fleet (there is no embeddings/retrieval worker kind in this codebase).
  • gen_ai.response.model is never distinguished from gen_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 RunStore side-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 than BatchSpanProcessor — 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).