diff --git a/.changeset/brave-clouds-trace.md b/.changeset/brave-clouds-trace.md new file mode 100644 index 000000000..ab89da001 --- /dev/null +++ b/.changeset/brave-clouds-trace.md @@ -0,0 +1,5 @@ +--- +"braintrust": patch +--- + +feat: Add OpenAI Agents SDK auto-instrumentation diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index bee0040c5..d3ed95128 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -9,6 +9,14 @@ { "variantKey": "openai-v6", "label": "v6" } ] }, + { + "scenarioDirName": "openai-agents-instrumentation", + "label": "OpenAI Agents Instrumentation", + "metadataScenario": "openai-agents-instrumentation", + "variants": [ + { "variantKey": "openai-agents-auto-hook", "label": "Auto-hook" } + ] + }, { "scenarioDirName": "anthropic-instrumentation", "label": "Anthropic Instrumentation", diff --git a/e2e/scenarios/openai-agents-instrumentation/assertions.ts b/e2e/scenarios/openai-agents-instrumentation/assertions.ts new file mode 100644 index 000000000..6fcf01e7d --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/assertions.ts @@ -0,0 +1,113 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; +import { withScenarioHarness } from "../../helpers/scenario-harness"; +import { + findChildSpans, + findLatestChildSpan, + findLatestSpan, +} from "../../helpers/trace-selectors"; +import { + AGENT_NAME, + FINAL_OUTPUT, + MODEL_NAME, + OPERATION_NAME, + ROOT_NAME, + SCENARIO_NAME, + TOOL_NAME, +} from "./constants.mjs"; + +type RunOpenAIAgentsScenario = (harness: { + runNodeScenarioDir: (options: { + entry: string; + env?: Record; + nodeArgs: string[]; + scenarioDir: string; + timeoutMs: number; + }) => Promise; +}) => Promise; + +function findModelSpans( + events: CapturedLogEvent[], + parentId: string | undefined, +): CapturedLogEvent[] { + return [ + ...findChildSpans(events, "Response", parentId), + ...findChildSpans(events, "Generation", parentId), + ]; +} + +export function defineOpenAIAgentsAutoInstrumentationAssertions(options: { + name: string; + runScenario: RunOpenAIAgentsScenario; + timeoutMs: number; +}): void { + describe(options.name, () => { + let events: CapturedLogEvent[] = []; + + beforeAll(async () => { + await withScenarioHarness(async (harness) => { + await options.runScenario(harness); + events = harness.events(); + }); + }, options.timeoutMs); + + test( + "captures OpenAI Agents spans through the auto-hook setup", + { timeout: options.timeoutMs }, + () => { + const root = findLatestSpan(events, ROOT_NAME); + const operation = findLatestSpan(events, OPERATION_NAME); + const workflow = findLatestChildSpan( + events, + "Agent workflow", + operation?.span.id, + ); + const agent = findLatestChildSpan( + events, + AGENT_NAME, + workflow?.span.id, + ); + const modelSpans = findModelSpans(events, agent?.span.id); + const toolSpan = findLatestChildSpan(events, TOOL_NAME, agent?.span.id); + + expect(root).toBeDefined(); + expect(root?.row.metadata).toMatchObject({ + scenario: SCENARIO_NAME, + }); + expect(operation).toBeDefined(); + expect(operation?.span.parentIds).toEqual([root?.span.id ?? ""]); + + expect(workflow).toBeDefined(); + expect(workflow?.span.type).toBe("task"); + expect(workflow?.span.parentIds).toEqual([operation?.span.id ?? ""]); + + expect(agent).toBeDefined(); + expect(agent?.span.type).toBe("task"); + expect(agent?.row.metadata).toMatchObject({ + tools: [TOOL_NAME], + output_type: "text", + }); + + expect(modelSpans.length).toBeGreaterThanOrEqual(1); + for (const modelSpan of modelSpans) { + expect(modelSpan.span.type).toBe("llm"); + expect(String(modelSpan.row.metadata?.model)).toContain(MODEL_NAME); + expect(modelSpan.metrics).toMatchObject({ + completion_tokens: expect.any(Number), + prompt_tokens: expect.any(Number), + tokens: expect.any(Number), + }); + expect(modelSpan.input).toEqual( + expect.arrayContaining([expect.anything()]), + ); + expect(modelSpan.output).toBeDefined(); + } + + expect(toolSpan).toBeDefined(); + expect(toolSpan?.span.type).toBe("tool"); + expect(toolSpan?.input).toBe(JSON.stringify({ city: "Vienna" })); + expect(toolSpan?.output).toBe(FINAL_OUTPUT); + }, + ); + }); +} diff --git a/e2e/scenarios/openai-agents-instrumentation/constants.mjs b/e2e/scenarios/openai-agents-instrumentation/constants.mjs new file mode 100644 index 000000000..75dc8643f --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/constants.mjs @@ -0,0 +1,7 @@ +export const ROOT_NAME = "openai-agents-auto-instrumentation-root"; +export const SCENARIO_NAME = "openai-agents-instrumentation"; +export const OPERATION_NAME = "openai-agents-run-operation"; +export const AGENT_NAME = "Weather Agent"; +export const MODEL_NAME = "gpt-4o-mini"; +export const TOOL_NAME = "lookup_weather"; +export const FINAL_OUTPUT = "Sunny in Vienna"; diff --git a/e2e/scenarios/openai-agents-instrumentation/package.json b/e2e/scenarios/openai-agents-instrumentation/package.json new file mode 100644 index 000000000..31ed6e458 --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/package.json @@ -0,0 +1,18 @@ +{ + "name": "openai-agents-instrumentation-scenario", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@openai/agents": "0.0.14", + "zod": "3.25.67" + }, + "braintrustScenario": { + "canary": { + "dependencies": { + "@openai/agents": "latest", + "zod": "zod@^4.0.0" + } + } + } +} diff --git a/e2e/scenarios/openai-agents-instrumentation/pnpm-lock.yaml b/e2e/scenarios/openai-agents-instrumentation/pnpm-lock.yaml new file mode 100644 index 000000000..6326ec5ee --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/pnpm-lock.yaml @@ -0,0 +1,986 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@openai/agents': + specifier: 0.0.14 + version: 0.0.14(ws@8.20.0)(zod@3.25.67) + zod: + specifier: 3.25.67 + version: 3.25.67 + +packages: + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@openai/agents-core@0.0.14': + resolution: {integrity: sha512-enCk5ucz+xxwPgh0zBQoJi5c1RukSc60neRUmlW4eQRgj9p5hVFQaBQNapZ4RysagHCLm2scYRwKgaP6nPDuNQ==} + peerDependencies: + zod: 3.25.40 - 3.25.67 + peerDependenciesMeta: + zod: + optional: true + + '@openai/agents-openai@0.0.14': + resolution: {integrity: sha512-qSGBictwfJ3dMhC3QvqOLMm8RVZ/eIYNcFNLHps7hWeB1xeDGJFDZ/X7dDicejOeEXbi/nGe1ry6LbXDYSo3uQ==} + + '@openai/agents-realtime@0.0.14': + resolution: {integrity: sha512-gfSuWEDKZREWi0DJDf3F8fT/xvLL9R0cydfgriL0kPkWOlTMuZ0KZKI6D90pc2VAWIescA8BuqCcWkgWFq55Uw==} + + '@openai/agents@0.0.14': + resolution: {integrity: sha512-67FwkSxlid8/fFzIDMBuIvDQJ2Egf7PCpI7zp2JAlIlsz4UZVSlptNcN63RCG2xP6X2XqsdyjPke8ZDEKVrePw==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.3.2: + resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@5.23.2: + resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + +snapshots: + + '@hono/node-server@1.19.14(hono@4.12.14)': + dependencies: + hono: 4.12.14 + optional: true + + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.67)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.14) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.3.2(express@5.2.1) + hono: 4.12.14 + jose: 6.2.2 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.67 + zod-to-json-schema: 3.25.2(zod@3.25.67) + transitivePeerDependencies: + - supports-color + optional: true + + '@openai/agents-core@0.0.14(ws@8.20.0)(zod@3.25.67)': + dependencies: + '@openai/zod': zod@3.25.67 + debug: 4.4.3 + openai: 5.23.2(ws@8.20.0)(zod@3.25.67) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.67) + zod: 3.25.67 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + + '@openai/agents-openai@0.0.14(ws@8.20.0)(zod@3.25.67)': + dependencies: + '@openai/agents-core': 0.0.14(ws@8.20.0)(zod@3.25.67) + '@openai/zod': zod@3.25.67 + debug: 4.4.3 + openai: 5.23.2(ws@8.20.0)(zod@3.25.67) + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + - ws + - zod + + '@openai/agents-realtime@0.0.14(zod@3.25.67)': + dependencies: + '@openai/agents-core': 0.0.14(ws@8.20.0)(zod@3.25.67) + '@openai/zod': zod@3.25.67 + '@types/ws': 8.18.1 + debug: 4.4.3 + ws: 8.20.0 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - zod + + '@openai/agents@0.0.14(ws@8.20.0)(zod@3.25.67)': + dependencies: + '@openai/agents-core': 0.0.14(ws@8.20.0)(zod@3.25.67) + '@openai/agents-openai': 0.0.14(ws@8.20.0)(zod@3.25.67) + '@openai/agents-realtime': 0.0.14(zod@3.25.67) + debug: 4.4.3 + openai: 5.23.2(ws@8.20.0)(zod@3.25.67) + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.6.0 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + optional: true + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + optional: true + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + optional: true + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + optional: true + + bytes@3.1.2: + optional: true + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + optional: true + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + optional: true + + content-disposition@1.1.0: + optional: true + + content-type@1.0.5: + optional: true + + cookie-signature@1.2.2: + optional: true + + cookie@0.7.2: + optional: true + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + optional: true + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + ee-first@1.1.1: + optional: true + + encodeurl@2.0.0: + optional: true + + es-define-property@1.0.1: + optional: true + + es-errors@1.3.0: + optional: true + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + optional: true + + escape-html@1.0.3: + optional: true + + etag@1.8.1: + optional: true + + eventsource-parser@3.0.8: + optional: true + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + optional: true + + express-rate-limit@8.3.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + optional: true + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + optional: true + + fast-deep-equal@3.1.3: + optional: true + + fast-uri@3.1.0: + optional: true + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + + forwarded@0.2.0: + optional: true + + fresh@2.0.0: + optional: true + + function-bind@1.1.2: + optional: true + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + optional: true + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + optional: true + + gopd@1.2.0: + optional: true + + has-symbols@1.1.0: + optional: true + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + optional: true + + hono@4.12.14: + optional: true + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + optional: true + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + optional: true + + inherits@2.0.4: + optional: true + + ip-address@10.1.0: + optional: true + + ipaddr.js@1.9.1: + optional: true + + is-promise@4.0.0: + optional: true + + isexe@2.0.0: + optional: true + + jose@6.2.2: + optional: true + + json-schema-traverse@1.0.0: + optional: true + + json-schema-typed@8.0.2: + optional: true + + math-intrinsics@1.1.0: + optional: true + + media-typer@1.1.0: + optional: true + + merge-descriptors@2.0.0: + optional: true + + mime-db@1.54.0: + optional: true + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + optional: true + + ms@2.1.3: {} + + negotiator@1.0.0: + optional: true + + object-assign@4.1.1: + optional: true + + object-inspect@1.13.4: + optional: true + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + optional: true + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + openai@5.23.2(ws@8.20.0)(zod@3.25.67): + optionalDependencies: + ws: 8.20.0 + zod: 3.25.67 + + parseurl@1.3.3: + optional: true + + path-key@3.1.1: + optional: true + + path-to-regexp@8.4.2: + optional: true + + pkce-challenge@5.0.1: + optional: true + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + optional: true + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + optional: true + + range-parser@1.2.1: + optional: true + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + optional: true + + require-from-string@2.0.2: + optional: true + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + optional: true + + safer-buffer@2.1.2: + optional: true + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + optional: true + + setprototypeof@1.2.0: + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + optional: true + + shebang-regex@3.0.0: + optional: true + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + optional: true + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + optional: true + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + optional: true + + statuses@2.0.2: + optional: true + + toidentifier@1.0.1: + optional: true + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + optional: true + + undici-types@7.19.2: {} + + unpipe@1.0.0: + optional: true + + vary@1.1.2: + optional: true + + which@2.0.2: + dependencies: + isexe: 2.0.0 + optional: true + + wrappy@1.0.2: + optional: true + + ws@8.20.0: {} + + zod-to-json-schema@3.25.2(zod@3.25.67): + dependencies: + zod: 3.25.67 + optional: true + + zod@3.25.67: {} diff --git a/e2e/scenarios/openai-agents-instrumentation/scenario.impl.mjs b/e2e/scenarios/openai-agents-instrumentation/scenario.impl.mjs new file mode 100644 index 000000000..bf76f87a3 --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/scenario.impl.mjs @@ -0,0 +1,70 @@ +import { Agent, run, tool } from "@openai/agents"; +import { + runMain, + runOperation, + runTracedScenario, +} from "../../helpers/provider-runtime.mjs"; +import { + AGENT_NAME, + FINAL_OUTPUT, + MODEL_NAME, + OPERATION_NAME, + ROOT_NAME, + SCENARIO_NAME, + TOOL_NAME, +} from "./constants.mjs"; + +const lookupWeather = tool({ + name: TOOL_NAME, + description: "Look up the weather forecast for a city.", + parameters: { + type: "object", + properties: { + city: { type: "string" }, + }, + required: ["city"], + additionalProperties: false, + }, + strict: false, + execute: async ({ city }) => `Sunny in ${city}`, +}); + +export async function runOpenAIAgentsAutoInstrumentationScenario() { + await runTracedScenario({ + callback: async () => { + await runOperation(OPERATION_NAME, "openai-agents-run", async () => { + const agent = new Agent({ + name: AGENT_NAME, + instructions: + "Use the lookup_weather tool exactly once, then answer only with the forecast.", + model: MODEL_NAME, + modelSettings: { + temperature: 0, + toolChoice: "required", + }, + tools: [lookupWeather], + }); + + const result = await run( + agent, + "What is the weather in Vienna? Answer only with the forecast.", + ); + + if (!String(result.finalOutput).includes(FINAL_OUTPUT)) { + throw new Error( + `Unexpected OpenAI Agents final output: ${result.finalOutput}`, + ); + } + }); + }, + flushCount: 2, + flushDelayMs: 10, + metadata: { + scenario: SCENARIO_NAME, + }, + projectNameBase: "e2e-openai-agents-instrumentation", + rootName: ROOT_NAME, + }); +} + +export { runMain }; diff --git a/e2e/scenarios/openai-agents-instrumentation/scenario.mjs b/e2e/scenarios/openai-agents-instrumentation/scenario.mjs new file mode 100644 index 000000000..921b2a337 --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/scenario.mjs @@ -0,0 +1,6 @@ +import { + runMain, + runOpenAIAgentsAutoInstrumentationScenario, +} from "./scenario.impl.mjs"; + +runMain(runOpenAIAgentsAutoInstrumentationScenario); diff --git a/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts b/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts new file mode 100644 index 000000000..2f0681f99 --- /dev/null +++ b/e2e/scenarios/openai-agents-instrumentation/scenario.test.ts @@ -0,0 +1,36 @@ +import { describe } from "vitest"; +import { + prepareScenarioDir, + readInstalledPackageVersion, + resolveScenarioDir, +} from "../../helpers/scenario-harness"; +import { defineOpenAIAgentsAutoInstrumentationAssertions } from "./assertions"; + +const scenarioDir = await prepareScenarioDir({ + scenarioDir: resolveScenarioDir(import.meta.url), +}); +const OPENAI_AGENTS_VARIANT_KEY = "openai-agents-auto-hook"; +const openAIAgentsVersion = await readInstalledPackageVersion( + scenarioDir, + "@openai/agents", +); +const TIMEOUT_MS = 60_000; + +describe(`openai agents sdk ${openAIAgentsVersion}`, () => { + defineOpenAIAgentsAutoInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { + NODE_ENV: "development", + }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { variantKey: OPENAI_AGENTS_VARIANT_KEY }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + timeoutMs: TIMEOUT_MS, + }); +}); diff --git a/js/src/auto-instrumentations/bundler/plugin.ts b/js/src/auto-instrumentations/bundler/plugin.ts index 3202d3dbc..e41d72711 100644 --- a/js/src/auto-instrumentations/bundler/plugin.ts +++ b/js/src/auto-instrumentations/bundler/plugin.ts @@ -24,6 +24,7 @@ import { openaiConfigs } from "../configs/openai"; import { anthropicConfigs } from "../configs/anthropic"; import { aiSDKConfigs } from "../configs/ai-sdk"; import { claudeAgentSDKConfigs } from "../configs/claude-agent-sdk"; +import { openAIAgentsCoreConfigs } from "../configs/openai-agents"; import { googleGenAIConfigs } from "../configs/google-genai"; import { huggingFaceConfigs } from "../configs/huggingface"; import { openRouterAgentConfigs } from "../configs/openrouter-agent"; @@ -76,6 +77,7 @@ export const unplugin = createUnplugin((options = {}) => { ...anthropicConfigs, ...aiSDKConfigs, ...claudeAgentSDKConfigs, + ...openAIAgentsCoreConfigs, ...googleGenAIConfigs, ...huggingFaceConfigs, ...openRouterConfigs, diff --git a/js/src/auto-instrumentations/bundler/webpack-loader.ts b/js/src/auto-instrumentations/bundler/webpack-loader.ts index 5d6c7e20b..46c094660 100644 --- a/js/src/auto-instrumentations/bundler/webpack-loader.ts +++ b/js/src/auto-instrumentations/bundler/webpack-loader.ts @@ -33,6 +33,7 @@ import { openaiConfigs } from "../configs/openai"; import { anthropicConfigs } from "../configs/anthropic"; import { aiSDKConfigs } from "../configs/ai-sdk"; import { claudeAgentSDKConfigs } from "../configs/claude-agent-sdk"; +import { openAIAgentsCoreConfigs } from "../configs/openai-agents"; import { googleGenAIConfigs } from "../configs/google-genai"; import { huggingFaceConfigs } from "../configs/huggingface"; import { openRouterAgentConfigs } from "../configs/openrouter-agent"; @@ -71,6 +72,7 @@ function getMatcher(options: BundlerPluginOptions): InstrumentationMatcher { ...anthropicConfigs, ...aiSDKConfigs, ...claudeAgentSDKConfigs, + ...openAIAgentsCoreConfigs, ...googleGenAIConfigs, ...huggingFaceConfigs, ...openRouterConfigs, diff --git a/js/src/auto-instrumentations/configs/openai-agents.test.ts b/js/src/auto-instrumentations/configs/openai-agents.test.ts new file mode 100644 index 000000000..f6d53edff --- /dev/null +++ b/js/src/auto-instrumentations/configs/openai-agents.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { openAIAgentsCoreConfigs } from "./openai-agents"; +import { openAIAgentsCoreChannels } from "../../instrumentation/plugins/openai-agents-channels"; + +describe("openAIAgentsCoreConfigs", () => { + it("registers auto-instrumentation for OpenAI Agents trace processor lifecycle methods", () => { + const lifecycleMethods = [ + ["onTraceStart", openAIAgentsCoreChannels.onTraceStart.channelName], + ["onTraceEnd", openAIAgentsCoreChannels.onTraceEnd.channelName], + ["onSpanStart", openAIAgentsCoreChannels.onSpanStart.channelName], + ["onSpanEnd", openAIAgentsCoreChannels.onSpanEnd.channelName], + ] as const; + const expectedConfigs = [ + "dist/tracing/processor.mjs", + "dist/tracing/processor.js", + ] + .flatMap((filePath) => + lifecycleMethods.map(([methodName, channelName]) => ({ + channelName, + module: { + name: "@openai/agents-core", + versionRange: ">=0.0.14", + filePath, + }, + functionQuery: { + className: "MultiTracingProcessor", + methodName, + kind: "Async", + }, + })), + ) + .sort((left, right) => + `${left.module.filePath}:${left.functionQuery.methodName}`.localeCompare( + `${right.module.filePath}:${right.functionQuery.methodName}`, + ), + ); + + expect( + [...openAIAgentsCoreConfigs].sort((left, right) => + `${left.module.filePath}:${left.functionQuery.methodName}`.localeCompare( + `${right.module.filePath}:${right.functionQuery.methodName}`, + ), + ), + ).toEqual(expectedConfigs); + }); +}); diff --git a/js/src/auto-instrumentations/configs/openai-agents.ts b/js/src/auto-instrumentations/configs/openai-agents.ts new file mode 100644 index 000000000..6c9bf1f7e --- /dev/null +++ b/js/src/auto-instrumentations/configs/openai-agents.ts @@ -0,0 +1,28 @@ +import type { InstrumentationConfig } from "@apm-js-collab/code-transformer"; +import { openAIAgentsCoreChannels } from "../../instrumentation/plugins/openai-agents-channels"; + +const lifecycleMethods = [ + ["onTraceStart", openAIAgentsCoreChannels.onTraceStart.channelName], + ["onTraceEnd", openAIAgentsCoreChannels.onTraceEnd.channelName], + ["onSpanStart", openAIAgentsCoreChannels.onSpanStart.channelName], + ["onSpanEnd", openAIAgentsCoreChannels.onSpanEnd.channelName], +] as const; + +export const openAIAgentsCoreConfigs: InstrumentationConfig[] = + lifecycleMethods.flatMap(([methodName, channelName]) => + ["dist/tracing/processor.mjs", "dist/tracing/processor.js"].map( + (filePath) => ({ + channelName, + module: { + name: "@openai/agents-core", + versionRange: ">=0.0.14", + filePath, + }, + functionQuery: { + className: "MultiTracingProcessor", + methodName, + kind: "Async", + }, + }), + ), + ); diff --git a/js/src/auto-instrumentations/hook.mts b/js/src/auto-instrumentations/hook.mts index 2c3622600..ed16c9bae 100644 --- a/js/src/auto-instrumentations/hook.mts +++ b/js/src/auto-instrumentations/hook.mts @@ -18,6 +18,7 @@ import { openaiConfigs } from "./configs/openai.js"; import { anthropicConfigs } from "./configs/anthropic.js"; import { aiSDKConfigs } from "./configs/ai-sdk.js"; import { claudeAgentSDKConfigs } from "./configs/claude-agent-sdk.js"; +import { openAIAgentsCoreConfigs } from "./configs/openai-agents.js"; import { googleGenAIConfigs } from "./configs/google-genai.js"; import { huggingFaceConfigs } from "./configs/huggingface.js"; import { openRouterAgentConfigs } from "./configs/openrouter-agent.js"; @@ -68,6 +69,9 @@ const allConfigs = [ ...(isDisabled(disabledIntegrations, "claudeagentsdk", "claude-agent-sdk") ? [] : claudeAgentSDKConfigs), + ...(isDisabled(disabledIntegrations, "openaiagents", "openai-agents") + ? [] + : openAIAgentsCoreConfigs), ...(isDisabled(disabledIntegrations, "google", "google-genai") ? [] : googleGenAIConfigs), diff --git a/js/src/auto-instrumentations/index.ts b/js/src/auto-instrumentations/index.ts index 03fda75a1..88307da72 100644 --- a/js/src/auto-instrumentations/index.ts +++ b/js/src/auto-instrumentations/index.ts @@ -32,6 +32,7 @@ export { openaiConfigs } from "./configs/openai"; export { anthropicConfigs } from "./configs/anthropic"; export { aiSDKConfigs } from "./configs/ai-sdk"; export { claudeAgentSDKConfigs } from "./configs/claude-agent-sdk"; +export { openAIAgentsCoreConfigs } from "./configs/openai-agents"; export { googleGenAIConfigs } from "./configs/google-genai"; export { huggingFaceConfigs } from "./configs/huggingface"; export { openRouterAgentConfigs } from "./configs/openrouter-agent"; diff --git a/js/src/auto-instrumentations/loader/esm-hook.mts b/js/src/auto-instrumentations/loader/esm-hook.mts index c5af87183..fbb1d9c06 100644 --- a/js/src/auto-instrumentations/loader/esm-hook.mts +++ b/js/src/auto-instrumentations/loader/esm-hook.mts @@ -5,7 +5,7 @@ import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; -import { sep } from "node:path"; +import { extname, sep } from "node:path"; import { create, type InstrumentationConfig, @@ -17,6 +17,26 @@ let instrumentator: any; let packages: Set; let transformers: Map = new Map(); +function getModuleType(url: string, format: string | undefined) { + if (format === "module") { + return "esm"; + } + if (format === "commonjs") { + return "cjs"; + } + + const pathname = url.startsWith("file:") ? fileURLToPath(url) : url; + const ext = extname(pathname); + if (ext === ".mjs") { + return "esm"; + } + if (ext === ".cjs") { + return "cjs"; + } + + return "unknown"; +} + export async function initialize( data: { instrumentations?: InstrumentationConfig[] } = {}, ) { @@ -86,12 +106,7 @@ export async function load(url: string, context: any, nextLoad: Function) { if (code) { const transformer = transformers.get(url); try { - const moduleType = - result.format === "module" - ? "esm" - : result.format === "commonjs" - ? "cjs" - : "unknown"; + const moduleType = getModuleType(url, result.format); const transformedCode = transformer.transform( code.toString("utf8"), moduleType, diff --git a/js/src/instrumentation/braintrust-plugin.test.ts b/js/src/instrumentation/braintrust-plugin.test.ts index aec836b66..6954c1f36 100644 --- a/js/src/instrumentation/braintrust-plugin.test.ts +++ b/js/src/instrumentation/braintrust-plugin.test.ts @@ -1,9 +1,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { BraintrustPlugin } from "./braintrust-plugin"; +import { + BraintrustPlugin, + type BraintrustPluginConfig, +} from "./braintrust-plugin"; import { OpenAIPlugin } from "./plugins/openai-plugin"; import { AnthropicPlugin } from "./plugins/anthropic-plugin"; import { AISDKPlugin } from "./plugins/ai-sdk-plugin"; import { ClaudeAgentSDKPlugin } from "./plugins/claude-agent-sdk-plugin"; +import { OpenAIAgentsPlugin } from "./plugins/openai-agents-plugin"; import { GoogleGenAIPlugin } from "./plugins/google-genai-plugin"; import { HuggingFacePlugin } from "./plugins/huggingface-plugin"; import { OpenRouterAgentPlugin } from "./plugins/openrouter-agent-plugin"; @@ -22,6 +26,12 @@ function createPluginClassMock() { }); } +function pluginConfigWithIntegrations( + integrations: Record, +): BraintrustPluginConfig { + return { integrations }; +} + // Mock all sub-plugins but preserve the utility functions vi.mock("./plugins/openai-plugin", async () => { const actual = await vi.importActual< @@ -45,6 +55,10 @@ vi.mock("./plugins/claude-agent-sdk-plugin", () => ({ ClaudeAgentSDKPlugin: createPluginClassMock(), })); +vi.mock("./plugins/openai-agents-plugin", () => ({ + OpenAIAgentsPlugin: createPluginClassMock(), +})); + vi.mock("./plugins/google-genai-plugin", () => ({ GoogleGenAIPlugin: createPluginClassMock(), })); @@ -116,6 +130,15 @@ describe("BraintrustPlugin", () => { expect(mockInstance.enable).toHaveBeenCalledTimes(1); }); + it("should create and enable OpenAI Agents plugin by default", () => { + const plugin = new BraintrustPlugin(); + plugin.enable(); + + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); + const mockInstance = vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; + expect(mockInstance.enable).toHaveBeenCalledTimes(1); + }); + it("should create and enable Google GenAI plugin by default", () => { const plugin = new BraintrustPlugin(); plugin.enable(); @@ -189,6 +212,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -206,6 +230,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -223,6 +248,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -245,6 +271,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -262,6 +289,7 @@ describe("BraintrustPlugin", () => { expect(OpenAIPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -298,6 +326,25 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); + expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); + expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); + expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); + expect(MistralPlugin).toHaveBeenCalledTimes(1); + }); + + it("should not create OpenAI Agents plugin when openAIAgents: false", () => { + const plugin = new BraintrustPlugin( + pluginConfigWithIntegrations({ openAIAgents: false }), + ); + plugin.enable(); + + expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); + expect(OpenAIPlugin).toHaveBeenCalledTimes(1); + expect(AnthropicPlugin).toHaveBeenCalledTimes(1); + expect(AISDKPlugin).toHaveBeenCalledTimes(1); + expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); @@ -414,12 +461,13 @@ describe("BraintrustPlugin", () => { }); it("should not create any plugins when all are disabled", () => { - const plugin = new BraintrustPlugin({ - integrations: { + const plugin = new BraintrustPlugin( + pluginConfigWithIntegrations({ openai: false, anthropic: false, aisdk: false, claudeAgentSDK: false, + openAIAgents: false, googleGenAI: false, huggingface: false, openrouter: false, @@ -427,14 +475,15 @@ describe("BraintrustPlugin", () => { mistral: false, cohere: false, groq: false, - }, - }); + }), + ); plugin.enable(); expect(OpenAIPlugin).not.toHaveBeenCalled(); expect(AnthropicPlugin).not.toHaveBeenCalled(); expect(AISDKPlugin).not.toHaveBeenCalled(); expect(ClaudeAgentSDKPlugin).not.toHaveBeenCalled(); + expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); expect(HuggingFacePlugin).not.toHaveBeenCalled(); expect(OpenRouterPlugin).not.toHaveBeenCalled(); @@ -445,22 +494,24 @@ describe("BraintrustPlugin", () => { }); it("should allow selective enabling of plugins", () => { - const plugin = new BraintrustPlugin({ - integrations: { + const plugin = new BraintrustPlugin( + pluginConfigWithIntegrations({ openai: true, anthropic: false, aisdk: false, claudeAgentSDK: true, + openAIAgents: true, googleGenAI: false, huggingface: true, openrouter: true, mistral: false, - }, - }); + }), + ); plugin.enable(); expect(OpenAIPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); expect(OpenRouterAgentPlugin).toHaveBeenCalledTimes(1); @@ -483,6 +534,7 @@ describe("BraintrustPlugin", () => { expect(OpenAIPlugin).toHaveBeenCalledTimes(1); expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -571,6 +623,8 @@ describe("BraintrustPlugin", () => { const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; const claudeAgentSDKMock = vi.mocked(ClaudeAgentSDKPlugin).mock.results[0].value; + const openAIAgentsMock = + vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; const googleGenAIMock = vi.mocked(GoogleGenAIPlugin).mock.results[0].value; const huggingFaceMock = @@ -586,6 +640,7 @@ describe("BraintrustPlugin", () => { expect(anthropicMock.enable).toHaveBeenCalledTimes(1); expect(aiSDKMock.enable).toHaveBeenCalledTimes(1); expect(claudeAgentSDKMock.enable).toHaveBeenCalledTimes(1); + expect(openAIAgentsMock.enable).toHaveBeenCalledTimes(1); expect(googleGenAIMock.enable).toHaveBeenCalledTimes(1); expect(huggingFaceMock.enable).toHaveBeenCalledTimes(1); expect(openRouterMock.enable).toHaveBeenCalledTimes(1); @@ -604,6 +659,8 @@ describe("BraintrustPlugin", () => { const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; const claudeAgentSDKMock = vi.mocked(ClaudeAgentSDKPlugin).mock.results[0].value; + const openAIAgentsMock = + vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; const googleGenAIMock = vi.mocked(GoogleGenAIPlugin).mock.results[0].value; const huggingFaceMock = @@ -621,6 +678,7 @@ describe("BraintrustPlugin", () => { expect(anthropicMock.disable).toHaveBeenCalledTimes(1); expect(aiSDKMock.disable).toHaveBeenCalledTimes(1); expect(claudeAgentSDKMock.disable).toHaveBeenCalledTimes(1); + expect(openAIAgentsMock.disable).toHaveBeenCalledTimes(1); expect(googleGenAIMock.disable).toHaveBeenCalledTimes(1); expect(huggingFaceMock.disable).toHaveBeenCalledTimes(1); expect(openRouterMock.disable).toHaveBeenCalledTimes(1); @@ -665,6 +723,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).not.toHaveBeenCalled(); expect(AISDKPlugin).not.toHaveBeenCalled(); expect(ClaudeAgentSDKPlugin).not.toHaveBeenCalled(); + expect(OpenAIAgentsPlugin).not.toHaveBeenCalled(); expect(GoogleGenAIPlugin).not.toHaveBeenCalled(); expect(HuggingFacePlugin).not.toHaveBeenCalled(); expect(OpenRouterPlugin).not.toHaveBeenCalled(); @@ -687,6 +746,7 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); expect(AISDKPlugin).toHaveBeenCalledTimes(1); expect(ClaudeAgentSDKPlugin).toHaveBeenCalledTimes(1); + expect(OpenAIAgentsPlugin).toHaveBeenCalledTimes(1); expect(GoogleGenAIPlugin).toHaveBeenCalledTimes(1); expect(HuggingFacePlugin).toHaveBeenCalledTimes(1); expect(OpenRouterPlugin).toHaveBeenCalledTimes(1); @@ -697,12 +757,13 @@ describe("BraintrustPlugin", () => { }); it("should only disable plugins that were enabled", () => { - const plugin = new BraintrustPlugin({ - integrations: { + const plugin = new BraintrustPlugin( + pluginConfigWithIntegrations({ openai: true, anthropic: false, aisdk: true, claudeAgentSDK: false, + openAIAgents: true, googleGenAI: true, huggingface: true, openrouter: true, @@ -710,12 +771,14 @@ describe("BraintrustPlugin", () => { mistral: false, cohere: false, groq: true, - }, - }); + }), + ); plugin.enable(); const openaiMock = vi.mocked(OpenAIPlugin).mock.results[0].value; const aiSDKMock = vi.mocked(AISDKPlugin).mock.results[0].value; + const openAIAgentsMock = + vi.mocked(OpenAIAgentsPlugin).mock.results[0].value; const googleGenAIMock = vi.mocked(GoogleGenAIPlugin).mock.results[0].value; const huggingFaceMock = @@ -729,6 +792,7 @@ describe("BraintrustPlugin", () => { expect(openaiMock.disable).toHaveBeenCalledTimes(1); expect(aiSDKMock.disable).toHaveBeenCalledTimes(1); + expect(openAIAgentsMock.disable).toHaveBeenCalledTimes(1); expect(googleGenAIMock.disable).toHaveBeenCalledTimes(1); expect(huggingFaceMock.disable).toHaveBeenCalledTimes(1); expect(openRouterMock.disable).toHaveBeenCalledTimes(1); diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts index 485d2d9bc..8d811e224 100644 --- a/js/src/instrumentation/braintrust-plugin.ts +++ b/js/src/instrumentation/braintrust-plugin.ts @@ -3,6 +3,7 @@ import { OpenAIPlugin } from "./plugins/openai-plugin"; import { AnthropicPlugin } from "./plugins/anthropic-plugin"; import { AISDKPlugin } from "./plugins/ai-sdk-plugin"; import { ClaudeAgentSDKPlugin } from "./plugins/claude-agent-sdk-plugin"; +import { OpenAIAgentsPlugin } from "./plugins/openai-agents-plugin"; import { GoogleGenAIPlugin } from "./plugins/google-genai-plugin"; import { HuggingFacePlugin } from "./plugins/huggingface-plugin"; import { OpenRouterAgentPlugin } from "./plugins/openrouter-agent-plugin"; @@ -31,6 +32,13 @@ export interface BraintrustPluginConfig { }; } +function getIntegrationConfig( + integrations: NonNullable, + key: string, +): boolean | undefined { + return (integrations as Record)[key]; +} + /** * Default Braintrust plugin that manages all AI provider instrumentation plugins. * @@ -53,6 +61,7 @@ export class BraintrustPlugin extends BasePlugin { private anthropicPlugin: AnthropicPlugin | null = null; private aiSDKPlugin: AISDKPlugin | null = null; private claudeAgentSDKPlugin: ClaudeAgentSDKPlugin | null = null; + private openAIAgentsPlugin: OpenAIAgentsPlugin | null = null; private googleGenAIPlugin: GoogleGenAIPlugin | null = null; private huggingFacePlugin: HuggingFacePlugin | null = null; private openRouterPlugin: OpenRouterPlugin | null = null; @@ -95,6 +104,12 @@ export class BraintrustPlugin extends BasePlugin { this.claudeAgentSDKPlugin.enable(); } + // Enable OpenAI Agents SDK integration (default: true) + if (getIntegrationConfig(integrations, "openAIAgents") !== false) { + this.openAIAgentsPlugin = new OpenAIAgentsPlugin(); + this.openAIAgentsPlugin.enable(); + } + // Enable Google GenAI integration (default: true) // Support both 'googleGenAI' and legacy 'google' config keys if (integrations.googleGenAI !== false && integrations.google !== false) { @@ -160,6 +175,11 @@ export class BraintrustPlugin extends BasePlugin { this.claudeAgentSDKPlugin = null; } + if (this.openAIAgentsPlugin) { + this.openAIAgentsPlugin.disable(); + this.openAIAgentsPlugin = null; + } + if (this.googleGenAIPlugin) { this.googleGenAIPlugin.disable(); this.googleGenAIPlugin = null; diff --git a/js/src/instrumentation/index.ts b/js/src/instrumentation/index.ts index 0f3e21977..ccce32b2c 100644 --- a/js/src/instrumentation/index.ts +++ b/js/src/instrumentation/index.ts @@ -17,6 +17,8 @@ export { BasePlugin } from "./core"; export { BraintrustPlugin } from "./braintrust-plugin"; export type { BraintrustPluginConfig } from "./braintrust-plugin"; +export { OpenAIAgentsTraceProcessor } from "./plugins/openai-agents-trace-processor"; +export type { OpenAIAgentsTraceProcessorOptions } from "./plugins/openai-agents-trace-processor"; // Re-export core types for external instrumentation packages export type { diff --git a/js/src/instrumentation/plugins/openai-agents-channels.ts b/js/src/instrumentation/plugins/openai-agents-channels.ts new file mode 100644 index 000000000..3b5472cec --- /dev/null +++ b/js/src/instrumentation/plugins/openai-agents-channels.ts @@ -0,0 +1,24 @@ +import { channel, defineChannels } from "../core/channel-definitions"; +import type { + OpenAIAgentsSpan, + OpenAIAgentsTrace, +} from "../../vendor-sdk-types/openai-agents"; + +export const openAIAgentsCoreChannels = defineChannels("@openai/agents-core", { + onTraceStart: channel<[OpenAIAgentsTrace], void>({ + channelName: "tracing.processor.onTraceStart", + kind: "async", + }), + onTraceEnd: channel<[OpenAIAgentsTrace], void>({ + channelName: "tracing.processor.onTraceEnd", + kind: "async", + }), + onSpanStart: channel<[OpenAIAgentsSpan], void>({ + channelName: "tracing.processor.onSpanStart", + kind: "async", + }), + onSpanEnd: channel<[OpenAIAgentsSpan], void>({ + channelName: "tracing.processor.onSpanEnd", + kind: "async", + }), +}); diff --git a/js/src/instrumentation/plugins/openai-agents-plugin.test.ts b/js/src/instrumentation/plugins/openai-agents-plugin.test.ts new file mode 100644 index 000000000..354c1bad7 --- /dev/null +++ b/js/src/instrumentation/plugins/openai-agents-plugin.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { configureNode } from "../../node/config"; +import { _exportsForTestingOnly, initLogger } from "../../logger"; +import { openAIAgentsCoreChannels } from "./openai-agents-channels"; +import { OpenAIAgentsPlugin } from "./openai-agents-plugin"; + +try { + configureNode(); +} catch { + // Best-effort initialization for test environments. +} + +describe("OpenAIAgentsPlugin", () => { + let backgroundLogger: ReturnType< + typeof _exportsForTestingOnly.useTestBackgroundLogger + >; + let plugin: OpenAIAgentsPlugin; + + beforeAll(async () => { + await _exportsForTestingOnly.simulateLoginForTests(); + }); + + beforeEach(() => { + backgroundLogger = _exportsForTestingOnly.useTestBackgroundLogger(); + initLogger({ + projectName: "openai-agents-plugin.test.ts", + projectId: "test-project-id", + }); + plugin = new OpenAIAgentsPlugin(); + plugin.enable(); + }); + + afterEach(() => { + plugin.disable(); + _exportsForTestingOnly.clearTestBackgroundLogger(); + }); + + it("records Braintrust spans from OpenAI Agents trace processor lifecycle events", async () => { + const trace = { + type: "trace", + traceId: "trace-openai-agents-auto", + name: "Agent workflow", + groupId: "group-openai-agents-auto", + metadata: { workflow: "test" }, + }; + const startedAt = new Date(Date.now() - 100).toISOString(); + const endedAt = new Date().toISOString(); + const span = { + type: "trace.span", + traceId: trace.traceId, + spanId: "span-generation", + parentId: null, + startedAt, + endedAt, + error: null, + spanData: { + type: "generation", + input: [{ role: "user", content: "What is 2+2?" }], + output: [{ role: "assistant", content: "4" }], + model: "gpt-4.1-mini", + usage: { + prompt_tokens: 5, + completion_tokens: 1, + total_tokens: 6, + }, + }, + }; + + await openAIAgentsCoreChannels.onTraceStart.tracePromise( + async () => undefined, + { arguments: [trace] }, + ); + await openAIAgentsCoreChannels.onSpanStart.tracePromise( + async () => undefined, + { arguments: [span] }, + ); + await openAIAgentsCoreChannels.onSpanEnd.tracePromise( + async () => undefined, + { arguments: [span] }, + ); + await openAIAgentsCoreChannels.onTraceEnd.tracePromise( + async () => undefined, + { arguments: [trace] }, + ); + + const spans = await backgroundLogger.drain(); + const taskSpan = spans.find( + (s) => s.span_attributes?.name === "Agent workflow", + ); + const generationSpan = spans.find( + (s) => s.span_attributes?.name === "Generation", + ); + + expect(taskSpan?.span_attributes?.type).toBe("task"); + expect(generationSpan?.span_attributes?.type).toBe("llm"); + expect(generationSpan?.metrics).toMatchObject({ + prompt_tokens: 5, + completion_tokens: 1, + tokens: 6, + }); + }); +}); diff --git a/js/src/instrumentation/plugins/openai-agents-plugin.ts b/js/src/instrumentation/plugins/openai-agents-plugin.ts new file mode 100644 index 000000000..4cf8af1a0 --- /dev/null +++ b/js/src/instrumentation/plugins/openai-agents-plugin.ts @@ -0,0 +1,114 @@ +import { BasePlugin } from "../core"; +import { unsubscribeAll } from "../core/channel-tracing"; +import { isObject } from "../../../util/index"; +import { openAIAgentsCoreChannels } from "./openai-agents-channels"; +import { OpenAIAgentsTraceProcessor } from "./openai-agents-trace-processor"; +import type { + OpenAIAgentsSpan, + OpenAIAgentsTrace, +} from "../../vendor-sdk-types/openai-agents"; + +function firstArgument(args: unknown): unknown { + if (Array.isArray(args)) { + return args[0]; + } + if ( + isObject(args) && + "length" in args && + typeof args.length === "number" && + Number.isInteger(args.length) && + args.length >= 0 + ) { + return Array.from(args as unknown as ArrayLike)[0]; + } + return undefined; +} + +function isOpenAIAgentsTrace(value: unknown): value is OpenAIAgentsTrace { + return ( + isObject(value) && + value.type === "trace" && + typeof value.traceId === "string" + ); +} + +function isOpenAIAgentsSpan(value: unknown): value is OpenAIAgentsSpan { + return ( + isObject(value) && + value.type === "trace.span" && + typeof value.traceId === "string" && + typeof value.spanId === "string" + ); +} + +export class OpenAIAgentsPlugin extends BasePlugin { + private processor = new OpenAIAgentsTraceProcessor(); + + protected onEnable(): void { + this.subscribeToTraceLifecycle(); + } + + protected onDisable(): void { + this.unsubscribers = unsubscribeAll(this.unsubscribers); + void this.processor.shutdown(); + } + + private subscribeToTraceLifecycle(): void { + const traceStartChannel = + openAIAgentsCoreChannels.onTraceStart.tracingChannel(); + const traceStartHandlers = { + start: (event: { arguments: unknown }) => { + const trace = firstArgument(event.arguments); + if (isOpenAIAgentsTrace(trace)) { + void this.processor.onTraceStart(trace); + } + }, + }; + traceStartChannel.subscribe(traceStartHandlers); + this.unsubscribers.push(() => + traceStartChannel.unsubscribe(traceStartHandlers), + ); + + const traceEndChannel = + openAIAgentsCoreChannels.onTraceEnd.tracingChannel(); + const traceEndHandlers = { + start: (event: { arguments: unknown }) => { + const trace = firstArgument(event.arguments); + if (isOpenAIAgentsTrace(trace)) { + void this.processor.onTraceEnd(trace); + } + }, + }; + traceEndChannel.subscribe(traceEndHandlers); + this.unsubscribers.push(() => + traceEndChannel.unsubscribe(traceEndHandlers), + ); + + const spanStartChannel = + openAIAgentsCoreChannels.onSpanStart.tracingChannel(); + const spanStartHandlers = { + start: (event: { arguments: unknown }) => { + const span = firstArgument(event.arguments); + if (isOpenAIAgentsSpan(span)) { + void this.processor.onSpanStart(span); + } + }, + }; + spanStartChannel.subscribe(spanStartHandlers); + this.unsubscribers.push(() => + spanStartChannel.unsubscribe(spanStartHandlers), + ); + + const spanEndChannel = openAIAgentsCoreChannels.onSpanEnd.tracingChannel(); + const spanEndHandlers = { + start: (event: { arguments: unknown }) => { + const span = firstArgument(event.arguments); + if (isOpenAIAgentsSpan(span)) { + void this.processor.onSpanEnd(span); + } + }, + }; + spanEndChannel.subscribe(spanEndHandlers); + this.unsubscribers.push(() => spanEndChannel.unsubscribe(spanEndHandlers)); + } +} diff --git a/js/src/instrumentation/plugins/openai-agents-trace-processor.ts b/js/src/instrumentation/plugins/openai-agents-trace-processor.ts new file mode 100644 index 000000000..f2526f759 --- /dev/null +++ b/js/src/instrumentation/plugins/openai-agents-trace-processor.ts @@ -0,0 +1,536 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { SpanTypeAttribute, isObject } from "../../../util/index"; +import { + type Logger, + type Span as BraintrustSpan, + NOOP_SPAN, + currentSpan, + startSpan, +} from "../../logger"; +import type { + OpenAIAgentsAgentSpanData, + OpenAIAgentsCustomSpanData, + OpenAIAgentsFunctionSpanData, + OpenAIAgentsGenerationSpanData, + OpenAIAgentsGuardrailSpanData, + OpenAIAgentsHandoffSpanData, + OpenAIAgentsMCPListToolsSpanData, + OpenAIAgentsResponseSpanData, + OpenAIAgentsSpan, + OpenAIAgentsSpanData, + OpenAIAgentsSpeechGroupSpanData, + OpenAIAgentsSpeechSpanData, + OpenAIAgentsTrace, + OpenAIAgentsTranscriptionSpanData, +} from "../../vendor-sdk-types/openai-agents"; + +type SpanInput = + | string + | Array> + | Record[]; + +type SpanOutput = + | string + | Array> + | Record; + +type TraceMetadata = { + firstInput: SpanInput | null; + lastOutput: SpanOutput | null; +}; + +export interface OpenAIAgentsTraceProcessorOptions { + logger?: Logger; + maxTraces?: number; +} + +function isSpanData( + spanData: OpenAIAgentsSpanData, + type: T, +): spanData is Extract { + return spanData.type === type; +} + +function spanTypeFromAgents(span: OpenAIAgentsSpan): SpanTypeAttribute { + const spanType = span.spanData.type; + + if ( + spanType === "function" || + spanType === "guardrail" || + spanType === "mcp_tools" + ) { + return SpanTypeAttribute.TOOL; + } + + if ( + spanType === "generation" || + spanType === "response" || + spanType === "transcription" || + spanType === "speech" + ) { + return SpanTypeAttribute.LLM; + } + + return SpanTypeAttribute.TASK; +} + +function spanNameFromAgents(span: OpenAIAgentsSpan): string { + const spanData = span.spanData; + if ("name" in spanData && spanData.name) { + return spanData.name; + } + + switch (spanData.type) { + case "generation": + return "Generation"; + case "response": + return "Response"; + case "handoff": + return "Handoff"; + case "mcp_tools": + return isSpanData(spanData, "mcp_tools") && spanData.server + ? `List Tools (${spanData.server})` + : "MCP List Tools"; + case "transcription": + return "Transcription"; + case "speech": + return "Speech"; + case "speech_group": + return "Speech Group"; + default: + return "Unknown"; + } +} + +function getTimeElapsed(end?: string, start?: string): number | undefined { + if (!start || !end) { + return undefined; + } + const startTime = new Date(start).getTime(); + const endTime = new Date(end).getTime(); + if (Number.isNaN(startTime) || Number.isNaN(endTime)) { + return undefined; + } + return (endTime - startTime) / 1000; +} + +function getNumberProperty(obj: unknown, key: string): number | undefined { + if (!isObject(obj) || !(key in obj)) { + return undefined; + } + const value = obj[key]; + return typeof value === "number" ? value : undefined; +} + +function parseUsageMetrics(usage: unknown): Record { + const metrics: Record = {}; + if (!isObject(usage)) { + return metrics; + } + + const promptTokens = + getNumberProperty(usage, "prompt_tokens") ?? + getNumberProperty(usage, "input_tokens") ?? + getNumberProperty(usage, "promptTokens") ?? + getNumberProperty(usage, "inputTokens"); + const completionTokens = + getNumberProperty(usage, "completion_tokens") ?? + getNumberProperty(usage, "output_tokens") ?? + getNumberProperty(usage, "completionTokens") ?? + getNumberProperty(usage, "outputTokens"); + const totalTokens = + getNumberProperty(usage, "total_tokens") ?? + getNumberProperty(usage, "totalTokens"); + + if (promptTokens !== undefined) { + metrics.prompt_tokens = promptTokens; + } + if (completionTokens !== undefined) { + metrics.completion_tokens = completionTokens; + } + if (totalTokens !== undefined) { + metrics.tokens = totalTokens; + } else if (promptTokens !== undefined && completionTokens !== undefined) { + metrics.tokens = promptTokens + completionTokens; + } + + const inputDetails = usage.input_tokens_details; + const cachedTokens = getNumberProperty(inputDetails, "cached_tokens"); + const cacheWriteTokens = getNumberProperty( + inputDetails, + "cache_write_tokens", + ); + if (cachedTokens !== undefined) { + metrics.prompt_cached_tokens = cachedTokens; + } + if (cacheWriteTokens !== undefined) { + metrics.prompt_cache_creation_tokens = cacheWriteTokens; + } + + return metrics; +} + +/** + * Converts OpenAI Agents SDK trace processor lifecycle events into Braintrust spans. + */ +export class OpenAIAgentsTraceProcessor { + private static readonly DEFAULT_MAX_TRACES = 10000; + + private logger?: Logger; + private maxTraces: number; + private traceSpans = new Map< + string, + { + rootSpan: BraintrustSpan; + childSpans: Map; + metadata: TraceMetadata; + } + >(); + private traceOrder: string[] = []; + + public readonly _traceSpans = this.traceSpans; + + constructor(options: OpenAIAgentsTraceProcessorOptions = {}) { + this.logger = options.logger; + this.maxTraces = + options.maxTraces ?? OpenAIAgentsTraceProcessor.DEFAULT_MAX_TRACES; + } + + private evictOldestTrace(): void { + const oldestTraceId = this.traceOrder.shift(); + if (oldestTraceId) { + this.traceSpans.delete(oldestTraceId); + } + } + + onTraceStart(trace: OpenAIAgentsTrace): Promise { + if (!trace?.traceId) { + return Promise.resolve(); + } + + if (this.traceOrder.length >= this.maxTraces) { + this.evictOldestTrace(); + } + + const current = currentSpan(); + const span = + current && current !== NOOP_SPAN + ? current.startSpan({ + name: trace.name, + type: SpanTypeAttribute.TASK, + }) + : this.logger + ? this.logger.startSpan({ + name: trace.name, + type: SpanTypeAttribute.TASK, + }) + : startSpan({ + name: trace.name, + type: SpanTypeAttribute.TASK, + }); + + span.log({ + input: "Agent workflow started", + metadata: { + group_id: trace.groupId, + ...(trace.metadata || {}), + }, + }); + + this.traceSpans.set(trace.traceId, { + rootSpan: span, + childSpans: new Map(), + metadata: { + firstInput: null, + lastOutput: null, + }, + }); + this.traceOrder.push(trace.traceId); + + return Promise.resolve(); + } + + async onTraceEnd(trace: OpenAIAgentsTrace): Promise { + const traceData = this.traceSpans.get(trace?.traceId); + if (!traceData) { + return; + } + + try { + traceData.rootSpan.log({ + input: traceData.metadata.firstInput, + output: traceData.metadata.lastOutput, + }); + traceData.rootSpan.end(); + await traceData.rootSpan.flush(); + } finally { + this.traceSpans.delete(trace.traceId); + const orderIndex = this.traceOrder.indexOf(trace.traceId); + if (orderIndex > -1) { + this.traceOrder.splice(orderIndex, 1); + } + } + } + + onSpanStart(span: OpenAIAgentsSpan): Promise { + if (!span?.spanId || !span.traceId) { + return Promise.resolve(); + } + + const traceData = this.traceSpans.get(span.traceId); + if (!traceData) { + return Promise.resolve(); + } + + const parentSpan = span.parentId + ? traceData.childSpans.get(span.parentId) + : traceData.rootSpan; + if (!parentSpan) { + return Promise.resolve(); + } + + const childSpan = parentSpan.startSpan({ + name: spanNameFromAgents(span), + type: spanTypeFromAgents(span), + }); + traceData.childSpans.set(span.spanId, childSpan); + + return Promise.resolve(); + } + + onSpanEnd(span: OpenAIAgentsSpan): Promise { + if (!span?.spanId || !span.traceId) { + return Promise.resolve(); + } + + const traceData = this.traceSpans.get(span.traceId); + if (!traceData) { + return Promise.resolve(); + } + + const braintrustSpan = traceData.childSpans.get(span.spanId); + if (!braintrustSpan) { + return Promise.resolve(); + } + + const logData = this.extractLogData(span); + braintrustSpan.log({ + error: span.error, + ...logData, + }); + braintrustSpan.end(); + traceData.childSpans.delete(span.spanId); + + const input = logData.input as SpanInput; + const output = logData.output as SpanOutput; + if (traceData.metadata.firstInput === null && input != null) { + traceData.metadata.firstInput = input; + } + if (output != null) { + traceData.metadata.lastOutput = output; + } + + return Promise.resolve(); + } + + async shutdown(): Promise { + if (this.logger && typeof this.logger.flush === "function") { + await this.logger.flush(); + } + } + + async forceFlush(): Promise { + if (this.logger && typeof this.logger.flush === "function") { + await this.logger.flush(); + } + } + + private extractLogData( + span: OpenAIAgentsSpan, + ): Record & { input?: unknown; output?: unknown } { + const spanData = span.spanData; + + switch (spanData.type) { + case "agent": + return this.extractAgentLogData(spanData); + case "response": + return this.extractResponseLogData(spanData, span); + case "function": + return this.extractFunctionLogData(spanData); + case "handoff": + return this.extractHandoffLogData(spanData); + case "guardrail": + return this.extractGuardrailLogData(spanData); + case "generation": + return this.extractGenerationLogData(spanData, span); + case "custom": + return this.extractCustomLogData(spanData); + case "mcp_tools": + return this.extractMCPListToolsLogData(spanData); + case "transcription": + return this.extractTranscriptionLogData(spanData); + case "speech": + return this.extractSpeechLogData(spanData); + case "speech_group": + return this.extractSpeechGroupLogData(spanData); + default: + return {}; + } + } + + private extractAgentLogData( + spanData: OpenAIAgentsAgentSpanData, + ): Record { + return { + metadata: { + tools: spanData.tools, + handoffs: spanData.handoffs, + output_type: spanData.output_type, + }, + }; + } + + private extractResponseLogData( + spanData: OpenAIAgentsResponseSpanData, + span: OpenAIAgentsSpan, + ): Record { + const response = spanData._response; + const output = isObject(response) ? response.output : undefined; + const usage = isObject(response) ? response.usage : undefined; + const metrics = { + ...this.extractTimingMetrics(span), + ...parseUsageMetrics(usage), + }; + + return { + input: spanData._input, + output, + metadata: isObject(response) + ? this.omitKeys(response, ["output", "usage"]) + : {}, + metrics, + }; + } + + private extractFunctionLogData( + spanData: OpenAIAgentsFunctionSpanData, + ): Record { + return { + input: spanData.input, + output: spanData.output, + }; + } + + private extractHandoffLogData( + spanData: OpenAIAgentsHandoffSpanData, + ): Record { + return { + metadata: { + from_agent: spanData.from_agent, + to_agent: spanData.to_agent, + }, + }; + } + + private extractGuardrailLogData( + spanData: OpenAIAgentsGuardrailSpanData, + ): Record { + return { + metadata: { + triggered: spanData.triggered, + }, + }; + } + + private extractGenerationLogData( + spanData: OpenAIAgentsGenerationSpanData, + span: OpenAIAgentsSpan, + ): Record { + return { + input: spanData.input, + output: spanData.output, + metadata: { + model: spanData.model, + model_config: spanData.model_config, + }, + metrics: { + ...this.extractTimingMetrics(span), + ...parseUsageMetrics(spanData.usage), + }, + }; + } + + private extractCustomLogData( + spanData: OpenAIAgentsCustomSpanData, + ): Record { + return spanData.data || {}; + } + + private extractMCPListToolsLogData( + spanData: OpenAIAgentsMCPListToolsSpanData, + ): Record { + return { + output: spanData.result, + metadata: { + server: spanData.server, + }, + }; + } + + private extractTranscriptionLogData( + spanData: OpenAIAgentsTranscriptionSpanData, + ): Record { + return { + input: spanData.input, + output: spanData.output, + metadata: { + model: spanData.model, + model_config: spanData.model_config, + }, + }; + } + + private extractSpeechLogData( + spanData: OpenAIAgentsSpeechSpanData, + ): Record { + return { + input: spanData.input, + output: spanData.output, + metadata: { + model: spanData.model, + model_config: spanData.model_config, + }, + }; + } + + private extractSpeechGroupLogData( + spanData: OpenAIAgentsSpeechGroupSpanData, + ): Record { + return { + input: spanData.input, + }; + } + + private extractTimingMetrics(span: OpenAIAgentsSpan): Record { + const timeToFirstToken = getTimeElapsed( + span.endedAt ?? undefined, + span.startedAt ?? undefined, + ); + return timeToFirstToken === undefined + ? {} + : { time_to_first_token: timeToFirstToken }; + } + + private omitKeys( + value: Record, + keys: string[], + ): Record { + const result: Record = {}; + for (const [key, fieldValue] of Object.entries(value)) { + if (!keys.includes(key)) { + result[key] = fieldValue; + } + } + return result; + } +} diff --git a/js/src/instrumentation/registry.ts b/js/src/instrumentation/registry.ts index 0d3111db4..18732f8d1 100644 --- a/js/src/instrumentation/registry.ts +++ b/js/src/instrumentation/registry.ts @@ -111,6 +111,7 @@ class PluginRegistry { google: true, huggingface: true, claudeAgentSDK: true, + openAIAgents: true, openrouter: true, openrouterAgent: true, mistral: true, diff --git a/js/src/vendor-sdk-types/openai-agents.ts b/js/src/vendor-sdk-types/openai-agents.ts new file mode 100644 index 000000000..81d3782f3 --- /dev/null +++ b/js/src/vendor-sdk-types/openai-agents.ts @@ -0,0 +1,127 @@ +/* + * Minimal OpenAI Agents SDK tracing types used by Braintrust auto-instrumentation. + * + * Original source: https://github.com/openai/openai-agents-js + * License: MIT + */ + +export type OpenAIAgentsSpanDataBase = { + type: string; +}; + +export type OpenAIAgentsAgentSpanData = OpenAIAgentsSpanDataBase & { + type: "agent"; + name: string; + handoffs?: string[]; + tools?: string[]; + output_type?: string; +}; + +export type OpenAIAgentsFunctionSpanData = OpenAIAgentsSpanDataBase & { + type: "function"; + name: string; + input: string; + output: string; + mcp_data?: string; +}; + +export type OpenAIAgentsGenerationSpanData = OpenAIAgentsSpanDataBase & { + type: "generation"; + input?: Array>; + output?: Array>; + model?: string; + model_config?: Record; + usage?: Record; +}; + +export type OpenAIAgentsResponseSpanData = OpenAIAgentsSpanDataBase & { + type: "response"; + response_id?: string; + _input?: string | Record[]; + _response?: Record; +}; + +export type OpenAIAgentsHandoffSpanData = OpenAIAgentsSpanDataBase & { + type: "handoff"; + from_agent?: string; + to_agent?: string; +}; + +export type OpenAIAgentsCustomSpanData = OpenAIAgentsSpanDataBase & { + type: "custom"; + name: string; + data: Record; +}; + +export type OpenAIAgentsGuardrailSpanData = OpenAIAgentsSpanDataBase & { + type: "guardrail"; + name: string; + triggered: boolean; +}; + +export type OpenAIAgentsTranscriptionSpanData = OpenAIAgentsSpanDataBase & { + type: "transcription"; + input: { + data: string; + format: "pcm" | string; + }; + output?: string; + model?: string; + model_config?: Record; +}; + +export type OpenAIAgentsSpeechSpanData = OpenAIAgentsSpanDataBase & { + type: "speech"; + input?: string; + output: { + data: string; + format: "pcm" | string; + }; + model?: string; + model_config?: Record; +}; + +export type OpenAIAgentsSpeechGroupSpanData = OpenAIAgentsSpanDataBase & { + type: "speech_group"; + input?: string; +}; + +export type OpenAIAgentsMCPListToolsSpanData = OpenAIAgentsSpanDataBase & { + type: "mcp_tools"; + server?: string; + result?: string[]; +}; + +export type OpenAIAgentsSpanData = + | OpenAIAgentsAgentSpanData + | OpenAIAgentsFunctionSpanData + | OpenAIAgentsGenerationSpanData + | OpenAIAgentsResponseSpanData + | OpenAIAgentsHandoffSpanData + | OpenAIAgentsCustomSpanData + | OpenAIAgentsGuardrailSpanData + | OpenAIAgentsTranscriptionSpanData + | OpenAIAgentsSpeechSpanData + | OpenAIAgentsSpeechGroupSpanData + | OpenAIAgentsMCPListToolsSpanData; + +export type OpenAIAgentsTrace = { + type: "trace"; + traceId: string; + name: string; + groupId: string | null; + metadata?: Record; +}; + +export type OpenAIAgentsSpan< + TData extends OpenAIAgentsSpanData = OpenAIAgentsSpanData, +> = { + type: "trace.span"; + traceId: string; + spanData: TData; + spanId: string; + parentId: string | null; + startedAt: string | null; + endedAt: string | null; + error: { message: string; data?: Record } | null; +};