@runtypelabs/flue-otel 0.2.0 → 0.2.2

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.
package/README.md CHANGED
@@ -1,17 +1,30 @@
1
1
  # @runtypelabs/flue-otel
2
2
 
3
- OpenTelemetry instrumentation for [Flue](https://flueframework.com) agents that
4
- reports runs to [Runtype](https://runtype.com) at full fidelity.
3
+ OpenTelemetry instrumentation for [Flue](https://flueframework.com) agents.
4
+ It subscribes to a Flue runtime through `instrument()` and turns every agent
5
+ run into a trace of GenAI semantic-convention spans carrying Runtype's
6
+ `runtype.*` attributes. Export those spans to [Runtype](https://runtype.com)
7
+ and each run appears in your dashboard as a first-class execution: model,
8
+ token usage, cost, loop iterations, tool calls and stop reason.
5
9
 
6
- Install it with Flue's `instrument()`, point an OTLP exporter at Runtype, and
7
- every agent run appears in your Runtype dashboard as a first-class execution —
8
- with the model, token counts, cost, loop iterations, tool calls and stop reason
9
- that a native Runtype run has.
10
+ - Works with Flue `>=1.0.0-beta.9` and 2.x from one entry point.
11
+ - Depends on `@opentelemetry/api` only. It brings no SDK, provider, exporter or
12
+ sampler; it writes through whatever your application has registered.
13
+ - Emits identifiers, structure and metrics only. No prompts, completions, tool
14
+ arguments, tool results, error messages or stack traces reach the wire.
15
+
16
+ Full guide: [Instrumenting a Flue agent](https://docs.runtype.com/developer-guides/guides/flue-instrumentation).
17
+
18
+ ## Install
10
19
 
11
20
  ```bash
12
21
  npm install @runtypelabs/flue-otel @opentelemetry/api
13
22
  ```
14
23
 
24
+ Requires Node 22+ and a Flue runtime that exposes `instrument()`.
25
+
26
+ ## Quick start
27
+
15
28
  ```ts
16
29
  import { instrument } from '@flue/runtime'
17
30
  import { createRuntypeFlueInstrumentation } from '@runtypelabs/flue-otel'
@@ -19,59 +32,19 @@ import { createRuntypeFlueInstrumentation } from '@runtypelabs/flue-otel'
19
32
  const stopInstrumenting = instrument(createRuntypeFlueInstrumentation())
20
33
  ```
21
34
 
22
- Works on **both** Flue lines — `>=1.0.0-beta.9` and 2.x — from one entry point.
23
-
24
- ## Already exporting Flue traces to Runtype? Read this first
25
-
26
- Two things change, and one of them is a regression, so decide before you install:
27
-
28
- - **Replace your stock instrumentation, do not add to it.** Flue's
29
- `instrument()` composes, and this package carries its own key, so calling both
30
- is possible and silently wrong: Runtype receives two `invoke_agent` spans for
31
- one run and the **token counts and cost double**. Point exactly one Flue
32
- instrumentation at a given Runtype endpoint.
33
- - **You will lose the transcript.** Stock `@flue/opentelemetry` exports content
34
- by default, so your runs currently carry prompts and completions. This release
35
- exports none (below), which also means **eval capture from these runs stops
36
- working**. In exchange the runs gain loop structure, an accurate iteration
37
- count, a stop reason, and priced usage. If the transcript is what you rely on,
38
- stay on stock until content ships.
39
-
40
- ## It emits no content
41
-
42
- This release puts **no prompts, completions, tool arguments, tool results, error
43
- messages or stack traces on the wire.** Only identifiers, structure and metrics.
44
-
45
- That is deliberate and it is the default we intend to keep earning: a package
46
- that lands inside your process should not start shipping your users' text
47
- somewhere because you installed it. Content export is a later, explicitly
48
- opt-in increment. Until it exists, there is nothing to configure and nothing to
49
- audit — the spans carry model ids, token counts, durations, tool _names_, and
50
- correlation ids.
51
-
52
- Two consequences worth knowing:
53
-
54
- - Your Runtype executions will show timing, cost, iteration counts and the tool
55
- call sequence, but **no transcript**. Eval capture from an external run needs
56
- content and is not available yet.
57
- - If you also run stock `@flue/opentelemetry`, note that it treats calling
58
- `instrument()` as consent to export content by default. That is a different
59
- choice, not a bug — but it is worth checking before you point it at a
60
- third-party backend.
61
-
62
- ## It brings no OpenTelemetry SDK
63
-
64
- This package depends on `@opentelemetry/api` and nothing else. It creates no
65
- provider, no exporter, no sampler, no resource, and it never flushes. Your
66
- application owns all of that, and this instrumentation writes through whatever
67
- you have registered.
68
-
69
- If you already run OpenTelemetry, you are done after the `instrument()` call
70
- above — add the resource attributes below so Runtype knows which agent the runs
71
- belong to.
35
+ That is the whole integration if your process already runs an OpenTelemetry
36
+ SDK. Spans are written to the globally registered tracer provider, so anything
37
+ you already export (an OTLP exporter, a vendor exporter, a collector) receives
38
+ them. To send them to Runtype, add an OTLP/HTTP exporter pointed at
39
+ `https://api.runtype.com/v1/otel` with a Runtype API key that has the
40
+ `TELEMETRY:WRITE` scope, and set the resource attributes described under
41
+ [Attribution](#attribution).
72
42
 
73
43
  ### If you have no OpenTelemetry setup yet
74
44
 
45
+ You need `@opentelemetry/sdk-trace-node`,
46
+ `@opentelemetry/exporter-trace-otlp-http` and `@opentelemetry/resources`.
47
+
75
48
  ```ts
76
49
  import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'
77
50
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
@@ -85,7 +58,6 @@ import {
85
58
  const provider = new NodeTracerProvider({
86
59
  resource: resourceFromAttributes({
87
60
  'service.name': 'my-agent',
88
- // Which Runtype agent these runs file under, plus adapter provenance.
89
61
  ...runtypeFlueResourceAttributes({ agentId: process.env.RUNTYPE_AGENT_ID }),
90
62
  }),
91
63
  spanProcessors: [
@@ -97,45 +69,61 @@ const provider = new NodeTracerProvider({
97
69
  ),
98
70
  ],
99
71
  })
100
- // `register()` also installs the context manager, WITHOUT WHICH nothing nests:
101
- // the OTel API's default is a no-op that makes every span a trace root.
72
+
73
+ // Registers the provider AND a context manager. Without a context manager the
74
+ // OTel API falls back to a no-op and every span becomes its own trace root.
102
75
  provider.register()
103
76
 
104
77
  instrument(createRuntypeFlueInstrumentation())
105
78
 
106
- // Flush before the process exits — see "Shutdown" below. This is not optional.
79
+ // Flush before exit. See "Shutdown" below.
107
80
  process.on('beforeExit', () => void provider.shutdown())
108
81
  ```
109
82
 
110
- You will also need `@opentelemetry/sdk-trace-node`,
111
- `@opentelemetry/exporter-trace-otlp-http` and `@opentelemetry/resources`.
83
+ ## Configuration
84
+
85
+ `createRuntypeFlueInstrumentation(options?)` accepts:
86
+
87
+ | Option | Type | Purpose |
88
+ | -------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `agents` | `Record<string, string>` | Runtype agent id per Flue agent name, e.g. `{ triage: 'agent_01j...' }`. Stamped as `runtype.agent.id` on that agent's `invoke_agent` span. See [Attribution](#attribution). |
90
+ | `tracer` | `Tracer` | Where spans are written. Defaults to `trace.getTracer('@runtypelabs/flue-otel')` on the global provider. Pass one to route Runtype spans through a separate provider. |
112
91
 
113
- ## Shutdown is yours, and it matters
92
+ The returned object implements Flue's full `FlueInstrumentation` contract:
93
+ `observe` (builds spans from Flue's observation stream), `interceptor` (makes
94
+ each span the active OTel context around the agent, model and tool work, so
95
+ your own HTTP and database spans nest inside the run, and joins a dispatched
96
+ run to its originating trace via `executionContext.traceCarrier`), and
97
+ `dispose` (closes anything still open as `interrupted`).
114
98
 
115
- A `BatchSpanProcessor` flushes children before their parents. A process that
116
- exits without `forceFlush()` or `shutdown()` therefore tends to lose the
117
- **closing `invoke_agent` span specifically** — and a run whose envelope never
118
- arrives is recorded as still in flight, permanently.
99
+ `instrument()` returns a disposer; call it when you want to stop instrumenting.
100
+ Instrumentations compose, so installing this one never replaces another
101
+ subscriber. Its key is exported as `RUNTYPE_FLUE_INSTRUMENTATION_KEY`.
119
102
 
120
- Wire the shutdown. In a serverless handler, `await provider.forceFlush()` before
121
- returning.
103
+ ### Other exports
122
104
 
123
- Flue's `instrument()` returns a disposer. Calling it ends any span still open as
124
- `interrupted`, which is what you want on an unclean exit — but it does not
125
- flush, because the exporter is not ours to flush.
105
+ - `runtypeFlueResourceAttributes({ agentId? })` builds the `runtype.*` resource
106
+ attributes (`schema.version`, `adapter.name`, `adapter.version`, and
107
+ `agent.id` when given). Spread it into your SDK `Resource`.
108
+ - `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits.
109
+ - `RUNTYPE_STOP_REASONS`, `RUNTYPE_TOOL_TYPES`, `RUNTYPE_SCHEMA_VERSION`,
110
+ `ADAPTER_NAME`, `ADAPTER_VERSION`, plus the `RuntypeStopReason`,
111
+ `RuntypeToolType`, `FlueInstrumentation`, `FlueProjectionOptions`,
112
+ `SpanAttributes` and `SpanIntent` types.
126
113
 
127
- ## Attribution: which Runtype agent owns the run
114
+ ## Attribution
128
115
 
129
- Runtype resolves the owning agent in this order:
116
+ Runtype files each trace under one agent, resolved in this order:
130
117
 
131
- 1. the `runtype.agent.id` **resource** attribute (what
132
- `runtypeFlueResourceAttributes({ agentId })` sets) — preferred;
133
- 2. the `x-runtype-agent-id` request header on the exporter;
134
- 3. the `runtype.agent.id` attribute on the run's `invoke_agent` span.
118
+ 1. The `runtype.agent.id` **resource** attribute. `runtypeFlueResourceAttributes({ agentId })` sets it. Preferred.
119
+ 2. The `x-runtype-agent-id` **request header** on the exporter (for example via
120
+ `OTEL_EXPORTER_OTLP_HEADERS=x-runtype-agent-id=agent_...`).
121
+ 3. The `runtype.agent.id` attribute on the run's `invoke_agent` **span**, which
122
+ the `agents` option sets.
135
123
 
136
- The first two describe a whole process, so they are the right answer when it
137
- runs one Runtype agent. **One process running several Runtype agents** cannot
138
- express that in a shared resource, which is what the third placement is for:
124
+ Use 1 or 2 when a process runs one Runtype agent. Use the `agents` option when a
125
+ single process runs several Runtype agents, since a shared resource cannot name
126
+ the right one per run:
139
127
 
140
128
  ```ts
141
129
  createRuntypeFlueInstrumentation({
@@ -146,72 +134,104 @@ createRuntypeFlueInstrumentation({
146
134
  })
147
135
  ```
148
136
 
149
- A delegated sub-agent is deliberately **not** attributed separately. Its work
150
- runs inside the delegating agent's trace, and one trace is one execution.
151
-
152
- > **One agent invocation per trace.** Runtype's model is that one trace is one
153
- > execution, and spans inherit whatever OTel context is active. So if an
154
- > enclosing span is active while two agent invocations run — an HTTP server span
155
- > from auto-instrumentation is the usual way this happens — both land in the
156
- > same trace, and Runtype either merges them into one execution (dropping the
157
- > second run's usage and stop reason) or, when they claim different
158
- > `runtype.agent.id` values through the `agents` map above with no resource
159
- > attribute and no header to fall back on, rejects the trace as
160
- > `ambiguous_agent_attribution` and writes nothing.
161
- >
162
- > If you dispatch several agent invocations inside one request or job, start
163
- > each one in its own trace, or attribute at the resource level with one process
164
- > per agent. Flue's own `dispatch(...)` does not propagate trace context, so a
165
- > plain dispatch is already its own trace.
166
-
167
- ## Composing with other instrumentations
168
-
169
- Flue's `instrument()` composes — an error reporter and a tracer subscribe side
170
- by side — and this package carries its own key, so installing it never replaces
171
- `@flue/opentelemetry`.
172
-
173
- > **Point exactly one instrumentation at Runtype.** If this package and a stock
174
- > `@flue/opentelemetry` both export to the same Runtype endpoint, Runtype sees
175
- > two `invoke_agent` spans for one run and the **token counts double**. Running
176
- > both is fine when they export to different backends.
137
+ A trace with no attribution from any source is rejected, not guessed. A
138
+ delegated sub-agent is not attributed separately: its work belongs to the
139
+ delegating agent's trace, and one trace is one execution.
177
140
 
178
- ## What it emits
141
+ ### One agent invocation per trace
142
+
143
+ Spans inherit whatever OTel context is active. If an enclosing span is active
144
+ while two agent invocations run (an HTTP server span from auto-instrumentation
145
+ is the common case), both land in the same trace. Runtype then either merges
146
+ them into one execution, dropping the second run's usage and stop reason, or,
147
+ when they claim different ids through `agents` with no resource attribute or
148
+ header to settle it, rejects the trace as `ambiguous_agent_attribution`.
149
+
150
+ If you dispatch several agent invocations inside one request or job, start each
151
+ in its own trace, or attribute at the resource level with one process per agent.
152
+ Flue's own `dispatch(...)` does not propagate trace context, so a plain dispatch
153
+ is already its own trace.
154
+
155
+ ## Shutdown
179
156
 
180
- | Span | When | Carries |
181
- | -------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
- | `invoke_agent <agent>` | one per agent invocation | the run's model, summed token usage, `runtype.stop_reason`, `runtype.tools.reported`, the highest loop iteration, `runtype.execution.id`, and `runtype.agent.id` when `agents` names the agent |
183
- | `chat <model>` | one per model turn | provider, request/response model, finish reason, per-turn usage, `runtype.turn.id` / `.turn.index` / `.iteration` |
184
- | `execute_tool <tool>` | one per tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, and `runtype.tool.type` when the tool's class is actually known |
185
- | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell | correlation ids only — these are framework structure, not agent invocations |
157
+ This package never flushes; the exporter is yours. A `BatchSpanProcessor`
158
+ exports children before their parents, so a process that exits without
159
+ `forceFlush()` or `shutdown()` tends to lose the closing `invoke_agent` span
160
+ specifically, and a run whose envelope never arrives is recorded as still in
161
+ flight. Wire the shutdown. In a serverless handler, `await provider.forceFlush()`
162
+ before returning.
186
163
 
187
- Every span also carries the `flue.*` correlation attributes stock
188
- `@flue/opentelemetry` emits, so dashboards you have already built keep working.
164
+ The disposer returned by `instrument()` ends any still-open span as
165
+ `interrupted`, folding the usage and iteration count seen so far onto the
166
+ envelope first. It does not flush.
189
167
 
190
- ### Two places it deliberately says nothing
168
+ ## Point one instrumentation at Runtype
169
+
170
+ Flue's `instrument()` composes and this package carries its own key, so it can
171
+ run alongside `@flue/opentelemetry`. Do not export both to the same Runtype
172
+ endpoint: Runtype would receive two `invoke_agent` spans for one run and count
173
+ its tokens and cost twice. Running both is fine when they export to different
174
+ backends.
175
+
176
+ ## What it emits
191
177
 
192
- - **`runtype.tool.type` for an ordinary tool.** Flue's `origin` field classifies
193
- who _initiated_ a call (`model` for every model-requested tool, whoever wrote
194
- it), not what the tool _is_. Mapping it would fill the column with a value
195
- that means nothing on that axis. Only a genuine class fact is emitted: a
196
- sub-agent delegation, and a 1.x `datastore` tool.
197
- - **A stop reason it cannot determine.** A run that ended mid-tool-call reports
178
+ | Span | When | Carries |
179
+ | -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
180
+ | `invoke_agent <agent>` | once per agent invocation | `gen_ai.agent.name`, `gen_ai.conversation.id`, the run's request/response model, summed token usage across every turn (`gen_ai.usage.*`), `runtype.stop_reason`, `runtype.tools.reported`, the highest loop iteration (`runtype.iteration`), `runtype.execution.id`, and `runtype.agent.id` when `agents` names the agent |
181
+ | `chat <model>` | once per model turn | `gen_ai.provider.name`, request/response model, response id, finish reason, per-turn usage, request parameters (`max_tokens`, `temperature`, reasoning level, server address), `runtype.turn.id` / `runtype.turn.index` / `runtype.iteration`, and `runtype.provider.finish_reason` / `runtype.gateway.log_id` when the provider records them (Workers AI attaches both — the gateway log id is a pointer to that exact request in your AI Gateway dashboard) |
182
+ | `execute_tool <tool>` | once per model-requested tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, `gen_ai.tool.type` (`function`) for every tool except a sub-agent delegation, and `runtype.tool.type` when the tool's class is known |
183
+ | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell call | correlation ids only; framework structure, not agent invocations |
184
+
185
+ Every span also carries Flue's own `flue.*` correlation attributes
186
+ (`flue.instance.id`, `flue.submission.id`, `flue.agent.name`,
187
+ `flue.session.name`, `flue.operation.id`, `flue.turn.id`, `flue.task.id`, and
188
+ so on), so dashboards grouped on those keys keep working.
189
+
190
+ Rules the projection follows:
191
+
192
+ - **Exactly one `invoke_agent` span per run.** A sub-agent delegation is
193
+ reported as an `execute_tool` span typed `subagent`; the sub-agent's own work
194
+ gets a `flue.task` span. Nested `prompt` operations inside a task do not open
195
+ a second envelope.
196
+ - **Usage is summed from model-turn leaves onto the envelope**, compaction
197
+ turns included, so the batch carrying the terminal span always carries the
198
+ complete total. `gen_ai.usage.input_tokens` is the total prompt-side count;
199
+ cache reads and writes are broken out separately.
200
+ - **Loop position is counted in process.** `runtype.turn.index` and
201
+ `runtype.iteration` are absolute ordinals; only the root agent's turns carry
202
+ `runtype.iteration`, so delegated work never inflates the run's iteration
203
+ count.
204
+ - **A failed span carries the error type and exception class name only.**
205
+ - **Framework bookkeeping is never a trace root.** `flue.compaction` and
206
+ `flue.operation shell` are dropped when there is no run to nest under, so a
207
+ host-initiated `session.compact()` or `session.shell()` cannot register as an
208
+ execution.
209
+
210
+ ### Where it deliberately says nothing
211
+
212
+ - **`runtype.tool.type` for an ordinary tool.** Flue's `origin` field says who
213
+ initiated a call, not what the tool is, so it is not mapped. Only genuine
214
+ class facts are emitted: `subagent` for a delegation, `data_connection` for a
215
+ 1.x `datastore` tool.
216
+ - **A stop reason it cannot determine.** A run that ended mid tool call reports
198
217
  `unknown` rather than guessing between a turn cap, a tool cap and a host
199
- abort.
218
+ abort. `submission_settled` outcomes `failed` / `aborted` map to `error`; a
219
+ turn's finish reason maps to `end_turn`, `length`, `content_filter` or `error`.
200
220
 
201
221
  An absent attribute costs one column. A wrong one renders as a measurement.
202
222
 
203
- ## Deriving from Flue's stable surface only
223
+ ## Flue compatibility
204
224
 
205
- Everything here comes from the observations Flue publishes as stable — event
206
- type names, envelope and correlation fields, and the normalized `turn_request` /
225
+ The instrumentation reads only Flue's stable observation surface: event type
226
+ names, envelope and correlation fields, and the normalized `turn_request` /
207
227
  `turn` / `tool_*` / `task` / `operation` / `compaction` / `submission_settled`
208
- payloads.
228
+ payloads. It never reads `AgentMessage`, which Flue marks unstable. The few
229
+ fields that differ between the 1.x and 2.x lines are detected by probing the
230
+ observation rather than comparing versions, which is why one build serves both.
209
231
 
210
- Nothing reads `AgentMessage`, which Flue documents as explicitly unstable and
211
- which rides `message_start`, `message_end`, `turn_messages` and `agent_end`.
212
- That exclusion is why one entry point serves both the 1.x and 2.x lines: the
213
- observation plane was essentially frozen across Flue's major rewrite, and the
214
- handful of fields that did change are probed rather than version-compared.
232
+ An instrumentation must never break the agent it observes: every observer and
233
+ disposer path swallows its own errors, and an unparseable Flue timestamp falls
234
+ back to the SDK clock instead of throwing.
215
235
 
216
236
  ## License
217
237
 
package/dist/index.cjs CHANGED
@@ -76,7 +76,7 @@ function mapSettlementOutcome(outcome) {
76
76
  }
77
77
 
78
78
  // package.json
79
- var version = "0.2.0";
79
+ var version = "0.2.2";
80
80
 
81
81
  // src/semconv.ts
82
82
  var GEN_AI = {
@@ -118,7 +118,9 @@ var RUNTYPE = {
118
118
  toolsReported: "runtype.tools.reported",
119
119
  toolType: "runtype.tool.type",
120
120
  turnId: "runtype.turn.id",
121
- turnIndex: "runtype.turn.index"
121
+ turnIndex: "runtype.turn.index",
122
+ providerFinishReason: "runtype.provider.finish_reason",
123
+ gatewayLogId: "runtype.gateway.log_id"
122
124
  };
123
125
  var FLUE = {
124
126
  instanceId: "flue.instance.id",
@@ -459,7 +461,17 @@ function createFlueProjection(options = {}) {
459
461
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
460
462
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
461
463
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
462
- ...usageAttributes(response.usage)
464
+ ...usageAttributes(response.usage),
465
+ // Provider-diagnostic pointers, emitted only when Flue actually provides
466
+ // them. Both are optional and provider-dependent (Workers AI attaches
467
+ // both today); an absent value costs one column, a synthesized one would
468
+ // render as a measurement. `gatewayLogId` is a pointer to the content
469
+ // without shipping the content — a customer can click through to that
470
+ // exact request in their own AI Gateway dashboard. `providerFinishReason`
471
+ // is the provider's exact finish value before normalization, which our
472
+ // `GEN_AI.finishReasons` above deliberately hides.
473
+ ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
474
+ ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
463
475
  };
464
476
  const intents = [];
465
477
  if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
package/dist/index.d.cts CHANGED
@@ -100,6 +100,13 @@ interface FlueModelResponse {
100
100
  finishReason?: string;
101
101
  /** 2.x only. The provider's raw finish value before normalization. */
102
102
  providerFinishReason?: string;
103
+ /**
104
+ * The response's own gateway log id (e.g. Cloudflare AI Gateway's
105
+ * `cf-aig-log-id`), for correlating a specific turn with its entry in the
106
+ * gateway dashboard. Telemetry only — present only when the provider records
107
+ * one. The Workers AI provider attaches it today.
108
+ */
109
+ gatewayLogId?: string;
103
110
  error?: FlueErrorInfo;
104
111
  }
105
112
  /**
@@ -475,6 +482,8 @@ declare const RUNTYPE: {
475
482
  readonly toolType: "runtype.tool.type";
476
483
  readonly turnId: "runtype.turn.id";
477
484
  readonly turnIndex: "runtype.turn.index";
485
+ readonly providerFinishReason: "runtype.provider.finish_reason";
486
+ readonly gatewayLogId: "runtype.gateway.log_id";
478
487
  };
479
488
  /**
480
489
  * `runtype.tool.type` values — the closed domain that drives display and
package/dist/index.d.ts CHANGED
@@ -100,6 +100,13 @@ interface FlueModelResponse {
100
100
  finishReason?: string;
101
101
  /** 2.x only. The provider's raw finish value before normalization. */
102
102
  providerFinishReason?: string;
103
+ /**
104
+ * The response's own gateway log id (e.g. Cloudflare AI Gateway's
105
+ * `cf-aig-log-id`), for correlating a specific turn with its entry in the
106
+ * gateway dashboard. Telemetry only — present only when the provider records
107
+ * one. The Workers AI provider attaches it today.
108
+ */
109
+ gatewayLogId?: string;
103
110
  error?: FlueErrorInfo;
104
111
  }
105
112
  /**
@@ -475,6 +482,8 @@ declare const RUNTYPE: {
475
482
  readonly toolType: "runtype.tool.type";
476
483
  readonly turnId: "runtype.turn.id";
477
484
  readonly turnIndex: "runtype.turn.index";
485
+ readonly providerFinishReason: "runtype.provider.finish_reason";
486
+ readonly gatewayLogId: "runtype.gateway.log_id";
478
487
  };
479
488
  /**
480
489
  * `runtype.tool.type` values — the closed domain that drives display and
package/dist/index.mjs CHANGED
@@ -43,7 +43,7 @@ function mapSettlementOutcome(outcome) {
43
43
  }
44
44
 
45
45
  // package.json
46
- var version = "0.2.0";
46
+ var version = "0.2.2";
47
47
 
48
48
  // src/semconv.ts
49
49
  var GEN_AI = {
@@ -85,7 +85,9 @@ var RUNTYPE = {
85
85
  toolsReported: "runtype.tools.reported",
86
86
  toolType: "runtype.tool.type",
87
87
  turnId: "runtype.turn.id",
88
- turnIndex: "runtype.turn.index"
88
+ turnIndex: "runtype.turn.index",
89
+ providerFinishReason: "runtype.provider.finish_reason",
90
+ gatewayLogId: "runtype.gateway.log_id"
89
91
  };
90
92
  var FLUE = {
91
93
  instanceId: "flue.instance.id",
@@ -426,7 +428,17 @@ function createFlueProjection(options = {}) {
426
428
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
427
429
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
428
430
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
429
- ...usageAttributes(response.usage)
431
+ ...usageAttributes(response.usage),
432
+ // Provider-diagnostic pointers, emitted only when Flue actually provides
433
+ // them. Both are optional and provider-dependent (Workers AI attaches
434
+ // both today); an absent value costs one column, a synthesized one would
435
+ // render as a measurement. `gatewayLogId` is a pointer to the content
436
+ // without shipping the content — a customer can click through to that
437
+ // exact request in their own AI Gateway dashboard. `providerFinishReason`
438
+ // is the provider's exact finish value before normalization, which our
439
+ // `GEN_AI.finishReasons` above deliberately hides.
440
+ ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
441
+ ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
430
442
  };
431
443
  const intents = [];
432
444
  if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/flue-otel",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenTelemetry instrumentation for Flue agents that emits GenAI semconv spans plus Runtype's runtype.* extension vocabulary, so a Flue run lands in Runtype at full fidelity.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -34,7 +34,7 @@
34
34
  "tsup": "^8.0.2",
35
35
  "typescript": "^6.0.3",
36
36
  "vitest": "^4.1.0",
37
- "@runtypelabs/shared": "3.27.1"
37
+ "@runtypelabs/shared": "3.28.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"