Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ export class Client {
process.on('SIGTERM', () => this.shutdown());
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
// TODO we can handle error states on unexported spans here
this.shutdown();
process.exit(1);
});
Expand Down
114 changes: 60 additions & 54 deletions src/instrumentation/openai-agents/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,6 @@ import {
} from '../../attributes';
import { debug } from './index';

/**
* OpenAI Agents ResponseSpanData type definition (from @openai/agents-core/dist/tracing/spans):
*
* interface ResponseSpanData {
* type: 'response';
* response_id?: string;
* _input?: any;
* _response?: {
* id: string;
* model: string;
* usage?: {
* input_tokens: number;
* output_tokens: number;
* total_tokens: number;
* };
* output?: Array<{
* type: string;
* role?: string;
* content?: string;
* }>;
* output_text?: string;
* };
* }
*/

const RESPONSE_ATTRIBUTES: AttributeMap = {
[RESPONSE_ID]: 'response_id',
[RESPONSE_INPUT]: '_input'
Expand Down Expand Up @@ -93,29 +68,23 @@ const RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES: IndexedAttributeMap = {
[GEN_AI_MESSAGE_FUNCTION_CALL_ARGUMENTS]: 'arguments'
};

/**
* Converts OpenAI Agents ResponseSpanData to OpenTelemetry semantic conventions.
*
* Maps response spans to standard response semantic conventions using
* our centralized semantic convention constants and attribute mapping system.
*/
export function convertResponseSpan(data: ResponseSpanData): AttributeMap {
const attributes: AttributeMap = {};
Object.assign(attributes, extractAttributesFromMapping(data, RESPONSE_ATTRIBUTES));

if (data._input && Array.isArray(data._input)) {
for (const item of data._input) {
switch (item.type) {
case 'message': // Input message
case 'message':
Object.assign(attributes,
extractAttributesFromMapping(item, RESPONSE_INPUT_ATTRIBUTES));
break;
case 'function_call': // Input function call
case 'function_call':
Object.assign(attributes,
extractAttributesFromMapping(item, RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES));
debug('Extracted input function call:', item.name);
break;
case 'function_call_result': // Function call result
case 'function_call_result':
debug('Skipping function call result');
break;
default:
Expand All @@ -125,27 +94,26 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap {
}
}

// _response was added with https://github.com/openai/openai-agents-js/pull/85
if (data._response) {
if ((data as any)._response) {
Object.assign(attributes,
extractAttributesFromMapping(data._response, RESPONSE_MODEL_ATTRIBUTES));
extractAttributesFromMapping((data as any)._response, RESPONSE_MODEL_ATTRIBUTES));
Object.assign(attributes,
extractAttributesFromMapping(data._response.usage, RESPONSE_USAGE_ATTRIBUTES));
extractAttributesFromMapping((data as any)._response.usage, RESPONSE_USAGE_ATTRIBUTES));

const completions = [];
if (Array.isArray(data._response.output)) {
for (const item of data._response.output) {
if (Array.isArray((data as any)._response.output)) {
for (const item of (data as any)._response.output) {
switch (item.type) {
case 'message': { // ResponseOutputMessage
case 'message': {
for (const contentItem of item.content || []) {
switch (contentItem.type) {
case 'output_text': // ResponseOutputText
case 'output_text':
completions.push({
role: item.role || 'assistant',
content: contentItem.text
});
break;
case 'refusal': // ResponseOutputRefusal
case 'refusal':
completions.push({
role: item.role || 'assistant',
content: contentItem.refusal
Expand All @@ -158,7 +126,7 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap {
}
break;
}
case 'reasoning': { // ResponseReasoningItem
case 'reasoning': {
const reasoningText = item.summary
?.filter((item: any) => item.type === 'summary_text')
?.map((item: any) => item.text)
Expand All @@ -172,27 +140,28 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap {
}
break;
}
case 'function_call': // ResponseFunctionToolCall
case 'file_search_call': // ResponseFileSearchToolCall
case 'web_search_call': // ResponseFunctionWebSearch
case 'computer_call': { // ResponseComputerToolCall
case 'function_call':
case 'file_search_call':
case 'web_search_call':
case 'computer_call': {
Object.assign(attributes,
extractAttributesFromMapping(item, RESPONSE_TOOL_CALL_ATTRIBUTES));
break;
}
case 'image_generation_call': // ResponseOutputItem.ImageGenerationCall
case 'code_interpreter_call': // ResponseCodeInterpreterToolCall
case 'local_shell_call': // ResponseOutputItem.LocalShellCall
case 'mcp_call': // ResponseOutputItem.McpCall
case 'mcp_list_tools': // ResponseOutputItem.McpListTools
case 'mcp_approval_request': { // ResponseOutputItem.McpApprovalRequest
case 'image_generation_call':
case 'code_interpreter_call':
case 'local_shell_call':
case 'mcp_call':
case 'mcp_list_tools':
case 'mcp_approval_request': {
debug('Unhandled output item type:', item.type);
break;
}
default: {
debug('Unknown output item type:', item.type);
break;
}
}
}
}

Expand All @@ -205,3 +174,40 @@ export function convertResponseSpan(data: ResponseSpanData): AttributeMap {
return attributes;
}

export function createEnhancedResponseSpanData(
baseData: { model: string; input: Array<{ type: string; role?: string; content?: string }> },
metadata: { responseId: string; usage: { inputTokens: number; outputTokens: number; totalTokens: number } }
): any {
return {
type: 'response',
response_id: metadata.responseId,
_input: baseData.input,
_response: {
id: metadata.responseId,
model: baseData.model,
usage: {
input_tokens: metadata.usage.inputTokens,
output_tokens: metadata.usage.outputTokens,
total_tokens: metadata.usage.totalTokens
}
}
};
}

export function convertEnhancedResponseSpan(data: any): AttributeMap {
const attributes: AttributeMap = {};

Object.assign(attributes, extractAttributesFromMapping(data, RESPONSE_ATTRIBUTES));

if (data._input && Array.isArray(data._input)) {
Object.assign(attributes, extractAttributesFromArray(data._input, RESPONSE_INPUT_ATTRIBUTES));
}

if (data._response) {
Object.assign(attributes, extractAttributesFromMapping(data._response, RESPONSE_MODEL_ATTRIBUTES));
Object.assign(attributes, extractAttributesFromMapping(data._response.usage, RESPONSE_USAGE_ATTRIBUTES));
}

return attributes;
}

1 change: 0 additions & 1 deletion src/semconv/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ export const GEN_AI_RESPONSE_FINISH_REASONS = 'gen_ai.response.finish_reasons';
export const GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.prompt_tokens';
export const GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.completion_tokens';
export const GEN_AI_USAGE_TOTAL_TOKENS = 'gen_ai.usage.total_tokens';
// TODO cache and reasoning tokens
5 changes: 2 additions & 3 deletions src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const MAX_EXPORT_BATCH_SIZE = 1; // Export immediately
const SCHEDULED_DELAY_MILLIS = 0; // No delay between exports
const EXPORT_TIMEOUT_MILLIS = 5000; // 5 second timeout

// TODO make this part of config
const DASHBOARD_URL = "https://app.agentops.ai";


Expand Down Expand Up @@ -132,7 +131,7 @@ export class TracingCore {
this.sdk = new OpenTelemetryNodeSDK({
resource: resource,
instrumentations: instrumentations,
spanProcessor: this.processor,
spanProcessor: this.processor as any,
});

// Configure logging after resource attributes are settled
Expand Down Expand Up @@ -169,4 +168,4 @@ export class TracingCore {

diag.setLogger(new DiagConsoleLogger(), diagLevel);
}
}
}
9 changes: 8 additions & 1 deletion tests/base.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InstrumentationBase } from '../src/instrumentation/base';
import { Client } from '../src/client';

class DummyInstrumentation extends InstrumentationBase {
static readonly metadata = {
Expand All @@ -20,14 +21,20 @@ class RuntimeInstrumentation extends DummyInstrumentation {
}

describe('InstrumentationBase', () => {
let mockClient: Client;

beforeEach(() => {
mockClient = new Client();
});

it('reports availability of target module', () => {
expect(DummyInstrumentation.available).toBe(true);
class Missing extends DummyInstrumentation { static readonly metadata = { ...DummyInstrumentation.metadata, targetLibrary: 'nonexistentlib' }; }
expect(Missing.available).toBe(false);
});

it('runtime targeting runs setup only once', () => {
const inst = new RuntimeInstrumentation('n','v',{});
const inst = new RuntimeInstrumentation(mockClient);
inst.setupRuntimeTargeting();
expect(inst.setup).toHaveBeenCalledTimes(1);
inst.setupRuntimeTargeting();
Expand Down
6 changes: 4 additions & 2 deletions tests/registry.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InstrumentationBase } from '../src/instrumentation/base';
import { Client } from '../src/client';

class RuntimeInst extends InstrumentationBase {
static readonly metadata = {
Expand Down Expand Up @@ -29,10 +30,11 @@ describe('InstrumentationRegistry', () => {
AVAILABLE_INSTRUMENTORS: [RuntimeInst, SimpleInst]
}));
const { InstrumentationRegistry } = require('../src/instrumentation/registry');
const registry = new InstrumentationRegistry();
const mockClient = new Client();
const registry = new InstrumentationRegistry(mockClient);
registry.initialize();
expect(registry.getAvailable().length).toBe(2);
const active = registry.getActiveInstrumentors('svc');
const active = registry.getActiveInstrumentors();
expect(active.some((i: any) => i instanceof RuntimeInst)).toBe(true);
expect(active.some((i: any) => i instanceof SimpleInst)).toBe(true);
});
Expand Down