Skip to content

Commit f071abb

Browse files
lpcoxCopilot
andauthored
fix: align OTEL attributes with gen_ai semconv spec (#3488)
* fix: use --build-local in smoke-otel-tracing to test latest api-proxy code The workflow was using --skip-pull with pre-built GHCR images (v0.25.29) which don't include the recently-merged otel.js module. Switch to --build-local so the api-proxy container is built from source and includes the OTEL tracing implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: inject stream_options for token tracking in spans Streaming OpenAI/Copilot API responses only include usage data in the final SSE chunk when the client sends stream_options: {include_usage: true}. Without this, the token tracker never extracts usage and OTEL spans have no gen_ai.usage.* attributes. The api-proxy now injects this option automatically for OpenAI-compatible providers (copilot, openai, opencode) when stream: true is set and stream_options is not already present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: align OTEL attributes with gen_ai semconv spec Rename attributes to match the OpenTelemetry GenAI semantic conventions: - awf.provider → gen_ai.provider.name - gen_ai.system → removed (deprecated) - awf.cache_read_tokens → gen_ai.usage.cache_read.input_tokens - awf.cache_write_tokens → gen_ai.usage.cache_creation.input_tokens - awf.streaming → gen_ai.request.stream Add new standard attributes: - gen_ai.operation.name (set to 'chat') - gen_ai.usage.reasoning.output_tokens Reference: https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 565cef7 commit f071abb

2 files changed

Lines changed: 20 additions & 16 deletions

File tree

containers/api-proxy/otel.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,10 @@ function startRequestSpan({ provider, method, path, requestId }) {
415415
attributes: {
416416
[ATTR_HTTP_REQUEST_METHOD]: method,
417417
[ATTR_URL_PATH]: path,
418-
'awf.provider': provider,
418+
'gen_ai.provider.name': provider,
419+
'gen_ai.operation.name': 'chat',
420+
'gen_ai.request.stream': true,
419421
'awf.request_id': requestId,
420-
'gen_ai.system': provider,
421422
},
422423
},
423424
parentCtx,
@@ -444,17 +445,19 @@ function setTokenAttributes(span, { provider, model, normalizedUsage, streaming
444445
span.setAttribute('gen_ai.response.model', model);
445446
}
446447
span.setAttributes({
447-
'gen_ai.usage.input_tokens': normalizedUsage.input_tokens,
448-
'gen_ai.usage.output_tokens': normalizedUsage.output_tokens,
449-
'awf.cache_read_tokens': normalizedUsage.cache_read_tokens,
450-
'awf.cache_write_tokens': normalizedUsage.cache_write_tokens,
451-
'awf.streaming': streaming,
448+
'gen_ai.usage.input_tokens': normalizedUsage.input_tokens,
449+
'gen_ai.usage.output_tokens': normalizedUsage.output_tokens,
450+
'gen_ai.usage.cache_read.input_tokens': normalizedUsage.cache_read_tokens,
451+
'gen_ai.usage.cache_creation.input_tokens': normalizedUsage.cache_write_tokens,
452+
'gen_ai.usage.reasoning.output_tokens': normalizedUsage.reasoning_tokens || 0,
453+
'gen_ai.request.stream': streaming,
452454
});
453455
span.addEvent('gen_ai.usage', {
454-
'gen_ai.usage.input_tokens': normalizedUsage.input_tokens,
455-
'gen_ai.usage.output_tokens': normalizedUsage.output_tokens,
456-
'awf.cache_read_tokens': normalizedUsage.cache_read_tokens,
457-
'awf.cache_write_tokens': normalizedUsage.cache_write_tokens,
456+
'gen_ai.usage.input_tokens': normalizedUsage.input_tokens,
457+
'gen_ai.usage.output_tokens': normalizedUsage.output_tokens,
458+
'gen_ai.usage.cache_read.input_tokens': normalizedUsage.cache_read_tokens,
459+
'gen_ai.usage.cache_creation.input_tokens': normalizedUsage.cache_write_tokens,
460+
'gen_ai.usage.reasoning.output_tokens': normalizedUsage.reasoning_tokens || 0,
458461
});
459462
} catch { /* best-effort */ }
460463
}

containers/api-proxy/otel.test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,10 @@ describe('otel — startRequestSpan', () => {
145145
expect(s.kind).toBe(SpanKind.CLIENT);
146146
expect(s.attributes['http.request.method']).toBe('POST');
147147
expect(s.attributes['url.path']).toBe('/v1/chat/completions');
148-
expect(s.attributes['awf.provider']).toBe('openai');
148+
expect(s.attributes['gen_ai.provider.name']).toBe('openai');
149149
expect(s.attributes['awf.request_id']).toBe('req-001');
150-
expect(s.attributes['gen_ai.system']).toBe('openai');
150+
expect(s.attributes['gen_ai.operation.name']).toBe('chat');
151+
expect(s.attributes['gen_ai.request.stream']).toBe(true);
151152
});
152153

153154
test('span name includes provider name', async () => {
@@ -234,9 +235,9 @@ describe('otel — setTokenAttributes', () => {
234235
expect(s.attributes['gen_ai.response.model']).toBe('gpt-4o');
235236
expect(s.attributes['gen_ai.usage.input_tokens']).toBe(1000);
236237
expect(s.attributes['gen_ai.usage.output_tokens']).toBe(500);
237-
expect(s.attributes['awf.cache_read_tokens']).toBe(200);
238-
expect(s.attributes['awf.cache_write_tokens']).toBe(50);
239-
expect(s.attributes['awf.streaming']).toBe(false);
238+
expect(s.attributes['gen_ai.usage.cache_read.input_tokens']).toBe(200);
239+
expect(s.attributes['gen_ai.usage.cache_creation.input_tokens']).toBe(50);
240+
expect(s.attributes['gen_ai.request.stream']).toBe(false);
240241

241242
const usageEvent = s.events.find(e => e.name === 'gen_ai.usage');
242243
expect(usageEvent).toBeDefined();

0 commit comments

Comments
 (0)