Skip to content
Open
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
130 changes: 46 additions & 84 deletions src/instrumentation/openai-agents/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import {
extractAttributesFromMapping,
extractAttributesFromArray,
extractAttributesFromMappingWithIndex,
AttributeMap,
IndexedAttributeMap
} from '../../attributes';
Expand Down Expand Up @@ -100,108 +101,69 @@ const RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES: IndexedAttributeMap = {
* 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
Object.assign(attributes,
extractAttributesFromMapping(item, RESPONSE_INPUT_ATTRIBUTES));
break;
case 'function_call': // Input 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
debug('Skipping function call result');
break;
default:
debug('Unknown input item type:', item.type);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also retain the debug message for unhandled types so we know to instrument them in the future.

break;
// Include index in attribute keys so multiple prompts and completions are captured
const attrs: AttributeMap = {};
Object.assign(attrs, extractAttributesFromMapping(data, RESPONSE_ATTRIBUTES));

if (Array.isArray(data._input)) {
data._input.forEach((item, i) => {
if (item.type === 'message') {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked this as a case statement for extensibility.

Object.assign(
attrs,
extractAttributesFromMappingWithIndex(item, RESPONSE_INPUT_ATTRIBUTES, i)
);
} else if (item.type === 'function_call') {
Object.assign(
attrs,
extractAttributesFromMappingWithIndex(
item,
RESPONSE_INPUT_FUNCTION_CALL_ATTRIBUTES,
i
)
);
}
}
});
}

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

const completions = [];
const completions: any[] = [];
if (Array.isArray(data._response.output)) {
for (const item of data._response.output) {
switch (item.type) {
case 'message': { // ResponseOutputMessage
for (const contentItem of item.content || []) {
switch (contentItem.type) {
case 'output_text': // ResponseOutputText
completions.push({
role: item.role || 'assistant',
content: contentItem.text
});
break;
case 'refusal': // ResponseOutputRefusal
completions.push({
role: item.role || 'assistant',
content: contentItem.refusal
});
break;
default:
debug('Unknown message content type:', contentItem.type);
break;
if (item.type === 'message') {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I prefer the case statements.

for (const c of item.content || []) {
if (c.type === 'output_text') {
completions.push({ role: item.role || 'assistant', content: c.text });
} else if (c.type === 'refusal') {
completions.push({ role: item.role || 'assistant', content: c.refusal });
}
}
break;
}
case 'reasoning': { // ResponseReasoningItem
const reasoningText = item.summary
?.filter((item: any) => item.type === 'summary_text')
?.map((item: any) => item.text)
?.join('') || '';

} else if (
item.type === 'function_call' ||
item.type === 'file_search_call' ||
item.type === 'web_search_call' ||
item.type === 'computer_call'
) {
Object.assign(attrs, extractAttributesFromMapping(item, RESPONSE_TOOL_CALL_ATTRIBUTES));
} else if (item.type === 'reasoning') {
const reasoningText = (item.summary || [])
.filter((r: any) => r.type === 'summary_text')
.map((r: any) => r.text)
.join('');
if (reasoningText) {
completions.push({
role: 'assistant',
content: reasoningText
});
completions.push({ role: 'assistant', content: reasoningText });
}
break;
}
case 'function_call': // ResponseFunctionToolCall
case 'file_search_call': // ResponseFileSearchToolCall
case 'web_search_call': // ResponseFunctionWebSearch
case 'computer_call': { // ResponseComputerToolCall
Object.assign(attributes,
extractAttributesFromMapping(item, RESPONSE_TOOL_CALL_ATTRIBUTES));
break;
}
case 'image_generation_call': // ResponseOutputItem.ImageGenerationCall
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintain these cases and the comments for future development.

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
debug('Unhandled output item type:', item.type);
break;
}
default: {
debug('Unknown output item type:', item.type);
break;
}
}
}

if (completions.length > 0) {
Object.assign(attributes,
extractAttributesFromArray(completions, RESPONSE_OUTPUT_ATTRIBUTES));
Object.assign(attrs, extractAttributesFromArray(completions, RESPONSE_OUTPUT_ATTRIBUTES));
}
}

return attributes;
return attrs;
}

3 changes: 2 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 Down Expand Up @@ -27,7 +28,7 @@ describe('InstrumentationBase', () => {
});

it('runtime targeting runs setup only once', () => {
const inst = new RuntimeInstrumentation('n','v',{});
const inst = new RuntimeInstrumentation(new Client());
inst.setupRuntimeTargeting();
expect(inst.setup).toHaveBeenCalledTimes(1);
inst.setupRuntimeTargeting();
Expand Down
22 changes: 18 additions & 4 deletions tests/openai-converters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { convertGenerationSpan } from '../src/instrumentation/openai-agents/generation';
import { convertAgentSpan } from '../src/instrumentation/openai-agents/agent';
import { convertFunctionSpan } from '../src/instrumentation/openai-agents/function';
import { convertResponseSpan, convertEnhancedResponseSpan, createEnhancedResponseSpanData } from '../src/instrumentation/openai-agents/response';
import { convertResponseSpan } from '../src/instrumentation/openai-agents/response';
import { convertHandoffSpan } from '../src/instrumentation/openai-agents/handoff';
import { convertCustomSpan } from '../src/instrumentation/openai-agents/custom';
import { convertGuardrailSpan } from '../src/instrumentation/openai-agents/guardrail';
Expand Down Expand Up @@ -39,10 +39,24 @@ describe('OpenAI converters', () => {
expect(convertResponseSpan({ type:'response', response_id:'r' } as any)['response.id']).toBe('r');
});

it('enhances response data', () => {
const enhanced = createEnhancedResponseSpanData({ model:'m', input:[{type:'message', role:'user', content:'c'}] }, { responseId:'id', usage:{ inputTokens:1, outputTokens:2, totalTokens:3 } });
const attrs = convertEnhancedResponseSpan(enhanced);
it('converts response data', () => {
const data = {
type: 'response',
response_id: 'id',
_input: [{ type: 'message', role: 'user', content: 'c' }],
_response: {
id: 'id',
model: 'm',
usage: { input_tokens: 1, output_tokens: 2, total_tokens: 3 },
output: [
{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'o' }] }
]
}
};
const attrs = convertResponseSpan(data as any);
expect(attrs['response.id']).toBe('id');
expect(attrs['gen_ai.prompt.0.content']).toBe('c');
expect(attrs['gen_ai.completion.0.content']).toBe('o');
expect(attrs['gen_ai.usage.total_tokens']).toBe('3');
});

Expand Down
5 changes: 3 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,10 @@ describe('InstrumentationRegistry', () => {
AVAILABLE_INSTRUMENTORS: [RuntimeInst, SimpleInst]
}));
const { InstrumentationRegistry } = require('../src/instrumentation/registry');
const registry = new InstrumentationRegistry();
const registry = new InstrumentationRegistry(new Client());
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