diff --git a/CHANGELOG.md b/CHANGELOG.md index 39311199..672c3e2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History +## [0.1.0-alpha.2] - Unreleased + +### Features Added +- Add OpenAI Agents SDK instrumentation. ([#14](https://github.com/microsoft/opentelemetry-distro-javascript/pull/14)) + + ## [0.1.0-alpha.1] - 2026-04-10 ### Features Added diff --git a/package-lock.json b/package-lock.json index 09561c75..003a4d5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "devDependencies": { "@azure/functions": "^4.9.0", "@langchain/core": "^1.1.39", + "@openai/agents": "^0.8.3", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -1412,6 +1413,20 @@ "node": ">=6" } }, + "node_modules/@hono/node-server": { + "version": "1.19.13", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", + "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1679,6 +1694,144 @@ "integrity": "sha512-59ex4x1/PabGQIg+o0GKG5olqAJYBvMOiXec/9HCD3hK2y36YMWT0ivq5mequvtS5+21kco3SOnMB6QyScLPIA==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@openai/agents": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@openai/agents/-/agents-0.8.3.tgz", + "integrity": "sha512-VmUywYNVaOuBOOv5hdH6tj3UjU3ZOkvM5WyRyEgRXEVkYbISkZuVWlnzEtiZkQ3lWjp0M8GGtKfSq2Dbr5DaQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.8.3", + "@openai/agents-openai": "0.8.3", + "@openai/agents-realtime": "0.8.3", + "debug": "^4.4.0", + "openai": "^6.26.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-core": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@openai/agents-core/-/agents-core-0.8.3.tgz", + "integrity": "sha512-UWRwtGF4t+MkT2xMWTHuEUI2xgp58bKvMYN0rQu6rB9zWtiP0aY5tCXlWUdPKhpPlM/X68u6eUN7qDitqrf50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "openai": "^6.26.0" + }, + "optionalDependencies": { + "@modelcontextprotocol/sdk": "^1.26.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-openai": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@openai/agents-openai/-/agents-openai-0.8.3.tgz", + "integrity": "sha512-n5bsRfVDINupkeSh/E/lRoGWUi9xVSRpf6muRjnEckIO0pmCG3NzKO+FQVhiyFA3c8i2vrn28SXF/ZdGcG4+vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.8.3", + "debug": "^4.4.0", + "openai": "^6.26.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-realtime": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.8.3.tgz", + "integrity": "sha512-no5AdAfYM5V/1u32OEQpP+VOJP/Y/JyTCVOvcgAdSKIV43ZG82f72pxbB0B2aFRdLZwC7C6sDgCJz6pbp+TjRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.8.3", + "@types/ws": "^8.18.1", + "debug": "^4.4.0", + "ws": "^8.18.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -3605,6 +3758,16 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.58.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", @@ -3992,6 +4155,21 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -4049,6 +4227,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4134,6 +4357,32 @@ "node": ">=6.0.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -4181,6 +4430,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4191,6 +4451,39 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4324,6 +4617,32 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4341,6 +4660,36 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4400,6 +4749,17 @@ "dev": true, "license": "MIT" }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -4413,6 +4773,22 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4420,6 +4796,14 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/electron-to-chromium": { "version": "1.5.331", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", @@ -4433,6 +4817,39 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -4440,6 +4857,20 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -4491,6 +4922,14 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4751,13 +5190,49 @@ "node": ">=0.10.0" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT" }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -4768,6 +5243,71 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4789,6 +5329,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4826,6 +5384,29 @@ "node": ">=16.0.0" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4881,12 +5462,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", "license": "MIT" }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4930,6 +5533,47 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "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.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -4974,6 +5618,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4984,6 +5642,20 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4996,6 +5668,17 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", + "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5003,6 +5686,28 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -5029,6 +5734,24 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -5084,6 +5807,28 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -5131,6 +5876,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5225,6 +5978,17 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tiktoken": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", @@ -5282,6 +6046,14 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -5482,6 +6254,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -5556,6 +6393,17 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", @@ -5563,6 +6411,78 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "6.34.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.34.0.tgz", + "integrity": "sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5673,6 +6593,17 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5726,6 +6657,18 @@ "node": "20 || >=22" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5794,6 +6737,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -5912,6 +6866,21 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5922,6 +6891,51 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5945,6 +6959,17 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -6053,6 +7078,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6082,6 +7125,14 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -6094,6 +7145,63 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC", + "optional": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6123,6 +7231,86 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", "license": "BSD-2-Clause" }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -6160,6 +7348,17 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -6445,6 +7644,17 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -6486,6 +7696,22 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -6530,6 +7756,17 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6591,6 +7828,17 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -6855,6 +8103,36 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "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 + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -6929,6 +8207,17 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "optional": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index bac21a00..cc94c549 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "devDependencies": { "@azure/functions": "^4.9.0", "@langchain/core": "^1.1.39", + "@openai/agents": "^0.8.3", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/samples/README.md b/samples/README.md index c05102ff..5b8d5bac 100644 --- a/samples/README.md +++ b/samples/README.md @@ -23,6 +23,7 @@ These sample programs show how to use the `@microsoft/opentelemetry` distributio | [redactQueryStrings.ts][redactquerystrings] | Demonstrates how to redact URL query strings from telemetry to protect sensitive information. | | [sampling.ts][sampling] | Demonstrates how to enable sampling to reduce data ingestion volume and control costs. | | [langchainInstrumentation.ts][langchaininstrumentation] | Demonstrates how to enable LangChain instrumentation to trace GenAI operations. | +| [openaiInstrumentation.ts][openaiinstrumentation] | Demonstrates how to enable OpenAI Agents SDK instrumentation to trace GenAI operations. | ## Prerequisites @@ -72,3 +73,4 @@ APPLICATIONINSIGHTS_CONNECTION_STRING="" node dist/basic [redactquerystrings]: https://github.com/Azure/opentelemetry-distro-javascript/blob/main/samples/src/redactQueryStrings.ts [sampling]: https://github.com/Azure/opentelemetry-distro-javascript/blob/main/samples/src/sampling.ts [langchaininstrumentation]: https://github.com/Azure/opentelemetry-distro-javascript/blob/main/samples/src/langchainInstrumentation.ts +[openaiinstrumentation]: https://github.com/Azure/opentelemetry-distro-javascript/blob/main/samples/src/openaiInstrumentation.ts diff --git a/samples/package.json b/samples/package.json index 72982afd..18fc2f7b 100644 --- a/samples/package.json +++ b/samples/package.json @@ -25,7 +25,8 @@ "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/exporter-trace-otlp-http": "^0.208.0", "@langchain/openai": "^1.4.4", - "@langchain/core": "^1.1.39" + "@langchain/core": "^1.1.39", + "@openai/agents": "^0.8.3" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/samples/src/openaiInstrumentation.ts b/samples/src/openaiInstrumentation.ts new file mode 100644 index 00000000..0635974b --- /dev/null +++ b/samples/src/openaiInstrumentation.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @summary Demonstrates how to enable OpenAI Agents SDK instrumentation to trace GenAI operations. + */ + +import { + useMicrosoftOpenTelemetry, + shutdownMicrosoftOpenTelemetry, +} from "@microsoft/opentelemetry"; +import { Agent, run } from "@openai/agents"; +import "dotenv/config"; + +async function main(): Promise { + useMicrosoftOpenTelemetry({ + azureMonitor: { + azureMonitorExporterOptions: { + connectionString: + process.env.APPLICATIONINSIGHTS_CONNECTION_STRING || "", + }, + }, + instrumentationOptions: { + openaiAgents: { + isContentRecordingEnabled: true, + }, + }, + }); + + // Create an OpenAI agent + const agent = new Agent({ + name: "Assistant", + instructions: "You are a helpful assistant that answers questions concisely.", + model: process.env.OPENAI_MODEL || "gpt-4o", + }); + + // Run the agent — this call is automatically traced + const result = await run(agent, "What is OpenTelemetry?"); + + console.log("Response:", result.finalOutput); + console.log("OpenAI Agents traces sent to Azure Monitor"); + + await shutdownMicrosoftOpenTelemetry(); +} + +main().catch(console.error); diff --git a/src/genai/instrumentations/openai/index.ts b/src/genai/instrumentations/openai/index.ts new file mode 100644 index 00000000..a6367746 --- /dev/null +++ b/src/genai/instrumentations/openai/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export { OpenAIAgentsTraceInstrumentor } from "./openAIAgentsTraceInstrumentor.js"; +export { OpenAIAgentsTraceProcessor } from "./openAIAgentsTraceProcessor.js"; diff --git a/src/genai/instrumentations/openai/openAIAgentsTraceInstrumentor.ts b/src/genai/instrumentations/openai/openAIAgentsTraceInstrumentor.ts new file mode 100644 index 00000000..7be586fa --- /dev/null +++ b/src/genai/instrumentations/openai/openAIAgentsTraceInstrumentor.ts @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// Vendored from microsoft/Agent365-nodejs packages/agents-a365-observability-extensions-openai +// Adapted: removed ObservabilityManager dependency, uses diag logger instead of A365 logger + +import { diag, trace, Tracer } from "@opentelemetry/api"; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationModuleDefinition, +} from "@opentelemetry/instrumentation"; +import { setTraceProcessors, setTracingDisabled, TracingProcessor } from "@openai/agents"; +import { OpenAIAgentsTraceProcessor } from "./openAIAgentsTraceProcessor.js"; + +/** + * Configuration options for the OpenAI Agents instrumentor. + */ +export interface OpenAIAgentsInstrumentationConfig extends InstrumentationConfig { + /** + * When true, the gen_ai.input.messages attribute containing LLM input + * messages will be suppressed and not attached to spans in InvokeAgent scopes. + * @default false + */ + suppressInvokeAgentInput?: boolean; + /** + * Whether to enable content recording (input/output messages, tool args, etc.). + * @default false + */ + isContentRecordingEnabled?: boolean; +} + +/** + * Internal singleton implementation. + */ +class OpenAIAgentsTraceInstrumentorImpl extends InstrumentationBase { + private static _instance: OpenAIAgentsTraceInstrumentorImpl | null = null; + private _hasBeenEnabled = false; + private processor?: OpenAIAgentsTraceProcessor; + protected otelTracer: Tracer; + + private constructor(config: OpenAIAgentsInstrumentationConfig = {}) { + if (OpenAIAgentsTraceInstrumentorImpl._instance !== null) { + throw new Error("OpenAIAgentsTraceInstrumentor can only be instantiated once."); + } + + super("microsoft-otel-openai-agents-instrumentor", "1.0.0", { + enabled: true, + ...config, + }); + + this.otelTracer = trace.getTracer("microsoft-otel-openai-agents", "1.0.0"); + + OpenAIAgentsTraceInstrumentorImpl._instance = this; + diag.info("[OpenAIAgentsTraceInstrumentor] Initialized"); + } + + static getInstance( + config?: OpenAIAgentsInstrumentationConfig, + ): OpenAIAgentsTraceInstrumentorImpl { + if (!OpenAIAgentsTraceInstrumentorImpl._instance) { + OpenAIAgentsTraceInstrumentorImpl._instance = new OpenAIAgentsTraceInstrumentorImpl(config); + } + return OpenAIAgentsTraceInstrumentorImpl._instance; + } + + static hasInstance(): boolean { + return OpenAIAgentsTraceInstrumentorImpl._instance !== null; + } + + static resetInstance(): void { + const inst = OpenAIAgentsTraceInstrumentorImpl._instance; + if (inst) { + void inst.processor?.shutdown(); + inst.processor = undefined; + } + OpenAIAgentsTraceInstrumentorImpl._instance = null; + } + + protected init(): InstrumentationModuleDefinition { + return { + name: "@openai/agents", + supportedVersions: [">=0.1.5"], + files: [], + }; + } + + public instrumentationDependencies(): readonly string[] { + return ["@openai/agents >= 0.1.5"] as const; + } + + /** + * Enable instrumentation. + * Sets up the trace processor and registers it with the OpenAI Agents SDK. + */ + public override enable(): void { + if (this._hasBeenEnabled) { + return; + } + this._hasBeenEnabled = true; + + // Enable tracing in the OpenAI Agents SDK + setTracingDisabled(false); + + this.processor = new OpenAIAgentsTraceProcessor(this.otelTracer, { + suppressInvokeAgentInput: this._config.suppressInvokeAgentInput ?? false, + isContentRecordingEnabled: this._config.isContentRecordingEnabled ?? false, + }); + + try { + setTraceProcessors([this.processor as TracingProcessor]); + } catch (error) { + diag.error( + `[OpenAIAgentsTraceInstrumentor] Failed to register trace processor: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + diag.info("[OpenAIAgentsTraceInstrumentor] Enabled OpenAI Agents instrumentation"); + super.enable(); + } + + /** + * Disable instrumentation. + */ + public override disable(): void { + if (this.processor) { + void this.processor.shutdown(); + this.processor = undefined; + } + + this._hasBeenEnabled = false; + + try { + setTraceProcessors([]); + } catch (error) { + diag.error( + `[OpenAIAgentsTraceInstrumentor] Failed to clear trace processors: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + diag.info("[OpenAIAgentsTraceInstrumentor] Disabled OpenAI Agents instrumentation"); + super.disable(); + } + + public getProcessor(): OpenAIAgentsTraceProcessor | undefined { + return this.processor; + } +} + +/** + * Static wrapper for OpenAI Agents SDK tracing instrumentation. + * + * Usage: + * ```ts + * OpenAIAgentsTraceInstrumentor.instrument({ isContentRecordingEnabled: true }); + * ``` + */ +export class OpenAIAgentsTraceInstrumentor { + private static throwNotInitialized(): never { + throw new Error( + "OpenAIAgentsTraceInstrumentor must be initialized first. " + + "Call OpenAIAgentsTraceInstrumentor.instrument() before using enable/disable.", + ); + } + + /** + * Initialize and auto-enable the OpenAI Agents instrumentation. + */ + static instrument(options?: OpenAIAgentsInstrumentationConfig): void { + OpenAIAgentsTraceInstrumentorImpl.getInstance(options).enable(); + } + + /** + * Enable OpenAI Agents instrumentation. + */ + static enable(): void { + if (!OpenAIAgentsTraceInstrumentorImpl.hasInstance()) { + this.throwNotInitialized(); + } + OpenAIAgentsTraceInstrumentorImpl.getInstance().enable(); + } + + /** + * Disable OpenAI Agents instrumentation. + */ + static disable(): void { + if (!OpenAIAgentsTraceInstrumentorImpl.hasInstance()) { + this.throwNotInitialized(); + } + OpenAIAgentsTraceInstrumentorImpl.getInstance().disable(); + } + + /** + * Reset the instrumentor instance (for testing). + */ + static resetInstance(): void { + OpenAIAgentsTraceInstrumentorImpl.resetInstance(); + } +} diff --git a/src/genai/instrumentations/openai/openAIAgentsTraceProcessor.ts b/src/genai/instrumentations/openai/openAIAgentsTraceProcessor.ts new file mode 100644 index 00000000..68bcbf72 --- /dev/null +++ b/src/genai/instrumentations/openai/openAIAgentsTraceProcessor.ts @@ -0,0 +1,439 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// Vendored from microsoft/Agent365-nodejs packages/agents-a365-observability-extensions-openai +// Adapted: removed A365 ObservabilityManager dependency, uses local semconv + utils + +import { + context, + trace as OtelTrace, + Span as OtelSpan, + Tracer as OtelTracer, + diag, +} from "@opentelemetry/api"; +import type { + Span as AgentsSpan, + SpanData, + MCPListToolsSpanData, + Trace as AgentTrace, + TracingProcessor, +} from "@openai/agents-core"; +import { + ATTR_GEN_AI_INPUT_MESSAGES, + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_OUTPUT_MESSAGES, + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_TOOL_TYPE, + ATTR_GEN_AI_TOOL_NAME, + GEN_AI_OPERATION_CHAT, + GEN_AI_OPERATION_EXECUTE_TOOL, + GEN_AI_OPERATION_INVOKE_AGENT, +} from "../../index.js"; +import { truncateValue } from "../../utils.js"; +import { + GEN_AI_GRAPH_NODE_ID, + GEN_AI_GRAPH_NODE_PARENT_ID, + GEN_AI_RESPONSE_CONTENT_KEY, + GEN_AI_EXECUTION_PAYLOAD_KEY, +} from "./semconv.js"; +import * as Utils from "./utils.js"; + +type ContextToken = unknown; + +/** + * OpenTelemetry-based trace processor for the OpenAI Agents SDK. + * + * Implements the `TracingProcessor` interface from `@openai/agents-core` so it + * can be registered via `setTraceProcessors()`. Every OpenAI agent span + * (agent invocation, function/tool call, LLM generation, response, handoff, + * MCP list-tools) is mapped to an OTel span with GenAI semantic convention + * attributes. + * + * Key behaviors: + * - Creates an OTel span on `onSpanStart` and ends it on `onSpanEnd`. + * - Maintains parent–child span relationships via trace/span ID maps. + * - Guards against unbounded memory with a hard cap of {@link MAX_SPANS_IN_FLIGHT}. + * - Content-sensitive attributes (messages, tool args) are only recorded + * when `isContentRecordingEnabled` is true. + */ +export class OpenAIAgentsTraceProcessor implements TracingProcessor { + private static readonly MAX_HANDOFFS_IN_FLIGHT = 1_000; + private static readonly MAX_SPANS_IN_FLIGHT = 10_000; + + private readonly tracer: OtelTracer; + private readonly suppressInvokeAgentInput: boolean; + private readonly isContentRecordingEnabled: boolean; + + /** Root spans keyed by trace ID. */ + private readonly rootSpans: Map = new Map(); + /** Active OTel spans keyed by OpenAI span ID. */ + private readonly otelSpans: Map = new Map(); + /** Context tokens for span activation. */ + private readonly tokens: Map = new Map(); + /** Reverse handoff map: `${toAgent}:${traceId}` → fromAgent */ + private readonly reverseHandoffsDict: Map = new Map(); + /** Span names for lookup (OTel Span doesn't expose name). */ + private readonly spanNames: Map = new Map(); + + constructor( + tracer: OtelTracer, + options?: { + suppressInvokeAgentInput?: boolean; + isContentRecordingEnabled?: boolean; + }, + ) { + this.tracer = tracer; + this.suppressInvokeAgentInput = options?.suppressInvokeAgentInput ?? false; + this.isContentRecordingEnabled = options?.isContentRecordingEnabled ?? false; + } + + private isContentKey(key: string): boolean { + return Utils.CONTENT_KEYS.has(key); + } + + private getNewKey(spanType: string, key: string): string | null { + return Utils.KEY_MAPPINGS.get(`${spanType}${key}`) ?? null; + } + + // -- TracingProcessor interface -- + + public async start(): Promise { + // no-op + } + + public async onTraceStart(_trace: AgentTrace): Promise { + // no-op + } + + public async onTraceEnd(trace: AgentTrace): Promise { + const rootSpan = this.rootSpans.get(trace.traceId); + if (rootSpan) { + this.rootSpans.delete(trace.traceId); + rootSpan.end(); + } + } + + public async onSpanStart(span: AgentsSpan): Promise { + const spanId = span.spanId; + const parentId = span.parentId; + const traceId = span.traceId; + const startedAt = span.startedAt; + const spanData = span.spanData; + + if (!startedAt || !spanId || !traceId) { + return; + } + + if (this.otelSpans.size >= OpenAIAgentsTraceProcessor.MAX_SPANS_IN_FLIGHT) { + diag.warn( + `[OpenAIAgentsTraceProcessor] Max spans in flight (${OpenAIAgentsTraceProcessor.MAX_SPANS_IN_FLIGHT}) reached, skipping span`, + ); + return; + } + + const startTime = new Date(startedAt).getTime(); + + // Find parent span + const parentSpan = parentId ? this.otelSpans.get(parentId) : this.rootSpans.get(traceId); + + // Create context with parent + const parentContext = parentSpan + ? OtelTrace.setSpan(context.active(), parentSpan) + : context.active(); + + const spanName = Utils.getSpanName(span); + + // Start OpenTelemetry span + const otelSpan = this.tracer.startSpan( + spanName, + { + startTime, + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: Utils.getOperationName(spanData), + [ATTR_GEN_AI_PROVIDER_NAME]: "openai", + }, + }, + parentContext, + ); + + if (!parentSpan) { + this.rootSpans.set(traceId, otelSpan); + } + + this.otelSpans.set(spanId, otelSpan); + this.spanNames.set(otelSpan, spanName); + const newContext = OtelTrace.setSpan(context.active(), otelSpan); + const token = context.with(newContext, () => context.active()); + this.tokens.set(spanId, token); + } + + public async onSpanEnd(span: AgentsSpan): Promise { + const spanId = span.spanId; + const traceId = span.traceId; + const endedAt = span.endedAt; + const spanData = span.spanData; + + if (!spanId || !traceId) { + return; + } + + // Cleanup context token + const token = this.tokens.get(spanId); + if (token) { + this.tokens.delete(spanId); + } + + const otelSpan = this.otelSpans.get(spanId); + if (!otelSpan) { + return; + } + this.otelSpans.delete(spanId); + this.spanNames.delete(otelSpan); + + // Clean up root span reference if this is the root + const rootSpan = this.rootSpans.get(traceId); + if (rootSpan === otelSpan) { + this.rootSpans.delete(traceId); + } + + // Update span name + otelSpan.updateName(Utils.getSpanName(span)); + + // Process based on span data type + if (spanData) { + this.processSpanData(otelSpan, spanData, traceId); + } + + // Set end time and status + const endTime = endedAt ? new Date(endedAt).getTime() : undefined; + const status = Utils.getSpanStatus(span); + otelSpan.setStatus(status); + if (endTime) { + otelSpan.end(endTime); + } else { + otelSpan.end(); + } + } + + public async forceFlush(): Promise { + // no-op + } + + public async shutdown(_timeout?: number): Promise { + this.rootSpans.clear(); + this.otelSpans.clear(); + this.tokens.clear(); + this.reverseHandoffsDict.clear(); + } + + // -- Span data processors -- + + private processSpanData(otelSpan: OtelSpan, data: SpanData, traceId: string): void { + const type = data.type; + const contentRecording = this.isContentRecordingEnabled; + + switch (type) { + case "response": + this.processResponseSpanData(otelSpan, data, contentRecording); + break; + + case "generation": + this.processGenerationSpanData(otelSpan, data, traceId, contentRecording); + break; + + case "function": + this.processFunctionSpanData(otelSpan, data, traceId, contentRecording); + break; + + case "mcp_tools": + this.processMCPListToolsSpanData(otelSpan, data); + break; + + case "handoff": + this.processHandoffSpanData(otelSpan, data, traceId); + break; + + case "agent": + this.processAgentSpanData(otelSpan, data, traceId); + break; + } + } + + private processResponseSpanData( + otelSpan: OtelSpan, + data: SpanData, + contentRecording: boolean, + ): void { + const responseData = data as Record; + const responseObj = responseData._response || responseData.response; + const inputObj = responseData._input || responseData.input; + + if (responseObj) { + const resp = responseObj as Record; + + if (resp.output && contentRecording) { + if (typeof resp.output === "string") { + otelSpan.setAttribute(ATTR_GEN_AI_OUTPUT_MESSAGES, truncateValue(resp.output)); + } else { + otelSpan.setAttribute( + ATTR_GEN_AI_OUTPUT_MESSAGES, + truncateValue( + Utils.buildOutputMessages( + resp.output as Array<{ + role: string; + content: Array<{ type: string; text: string }>; + }>, + ), + ), + ); + } + } + + // Get attributes but filter out response content key (handled above) + const attrs = Utils.getAttributesFromResponse(responseObj); + Object.entries(attrs).forEach(([key, value]) => { + if (value !== null && value !== undefined && key !== GEN_AI_RESPONSE_CONTENT_KEY) { + otelSpan.setAttribute(key, value as string | number | boolean); + } + }); + + const modelName = attrs[ATTR_GEN_AI_REQUEST_MODEL] ?? ""; + otelSpan.updateName(`${GEN_AI_OPERATION_CHAT} ${modelName}`); + } + + if (inputObj && !this.suppressInvokeAgentInput && contentRecording) { + if (typeof inputObj === "string") { + try { + const parsed = JSON.parse(inputObj as string); + if (Array.isArray(parsed)) { + otelSpan.setAttribute( + ATTR_GEN_AI_INPUT_MESSAGES, + truncateValue(Utils.buildInputMessages(parsed)), + ); + return; + } + } catch { + // Fall back to raw string + } + otelSpan.setAttribute(ATTR_GEN_AI_INPUT_MESSAGES, truncateValue(inputObj as string)); + } else if (Array.isArray(inputObj)) { + otelSpan.setAttribute( + ATTR_GEN_AI_INPUT_MESSAGES, + truncateValue(Utils.buildInputMessages(inputObj)), + ); + } + } + } + + private processGenerationSpanData( + otelSpan: OtelSpan, + data: SpanData, + traceId: string, + contentRecording: boolean, + ): void { + const attrs = Utils.getAttributesFromGenerationSpanData(data); + Object.entries(attrs).forEach(([key, value]) => { + const shouldExcludeKey = key === GEN_AI_EXECUTION_PAYLOAD_KEY; + if (value !== null && value !== undefined && !shouldExcludeKey) { + const newKey = this.getNewKey(data.type, key); + const resolvedKey = newKey || key; + if (!this.isContentKey(resolvedKey) || contentRecording) { + otelSpan.setAttribute(resolvedKey, value as string | number | boolean); + } + } + }); + + this.stampCustomParent(otelSpan, traceId); + + const operationName = attrs[ATTR_GEN_AI_OPERATION_NAME]; + const modelName = attrs[ATTR_GEN_AI_REQUEST_MODEL]; + if (operationName && modelName) { + otelSpan.updateName(`${operationName} ${modelName}`); + } + } + + private processFunctionSpanData( + otelSpan: OtelSpan, + data: SpanData, + traceId: string, + contentRecording: boolean, + ): void { + const functionData = data as Record; + const attrs = Utils.getAttributesFromFunctionSpanData(data); + Object.entries(attrs).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + const newKey = this.getNewKey(data.type, key); + const resolvedKey = newKey || key; + if (!this.isContentKey(resolvedKey) || contentRecording) { + otelSpan.setAttribute(resolvedKey, value as string | number | boolean); + } + } + }); + otelSpan.setAttribute(ATTR_GEN_AI_TOOL_TYPE, "function"); + + this.stampCustomParent(otelSpan, traceId); + otelSpan.updateName(`${GEN_AI_OPERATION_EXECUTE_TOOL} ${functionData.name ?? ""}`); + otelSpan.setAttribute(ATTR_GEN_AI_OPERATION_NAME, GEN_AI_OPERATION_EXECUTE_TOOL); + } + + private processMCPListToolsSpanData(otelSpan: OtelSpan, data: SpanData): void { + const attrs = Utils.getAttributesFromMCPListToolsSpanData(data); + Object.entries(attrs).forEach(([key, value]) => { + if (value !== null && value !== undefined) { + const newKey = this.getNewKey(data.type, key); + otelSpan.setAttribute(newKey || key, value as string | number | boolean); + } + }); + + otelSpan.setAttribute(ATTR_GEN_AI_OPERATION_NAME, GEN_AI_OPERATION_EXECUTE_TOOL); + const serverName = (data as MCPListToolsSpanData).server ?? "unknown"; + otelSpan.updateName(`${GEN_AI_OPERATION_EXECUTE_TOOL} ${serverName}`); + if (serverName) { + otelSpan.setAttribute(ATTR_GEN_AI_TOOL_NAME, serverName); + } + otelSpan.setAttribute(ATTR_GEN_AI_TOOL_TYPE, "extension"); + } + + private processHandoffSpanData(_otelSpan: OtelSpan, data: SpanData, traceId: string): void { + const handoffData = data as Record; + if (handoffData.to_agent && handoffData.from_agent) { + const key = `${handoffData.to_agent}:${traceId}`; + this.reverseHandoffsDict.set(key, handoffData.from_agent as string); + + while (this.reverseHandoffsDict.size > OpenAIAgentsTraceProcessor.MAX_HANDOFFS_IN_FLIGHT) { + const firstKey = this.reverseHandoffsDict.keys().next().value; + if (firstKey) { + this.reverseHandoffsDict.delete(firstKey); + } + } + } + } + + private processAgentSpanData(otelSpan: OtelSpan, data: SpanData, traceId: string): void { + const agentData = data as Record; + if (agentData.name) { + otelSpan.setAttribute(GEN_AI_GRAPH_NODE_ID, agentData.name as string); + otelSpan.setAttribute(ATTR_GEN_AI_OPERATION_NAME, GEN_AI_OPERATION_INVOKE_AGENT); + + // Lookup parent node from handoff + const key = `${agentData.name}:${traceId}`; + const parentNode = this.reverseHandoffsDict.get(key); + if (parentNode) { + this.reverseHandoffsDict.delete(key); + otelSpan.setAttribute(GEN_AI_GRAPH_NODE_PARENT_ID, parentNode); + } + + otelSpan.updateName(`${GEN_AI_OPERATION_INVOKE_AGENT} ${agentData.name}`); + } + } + + private stampCustomParent(otelSpan: OtelSpan, traceId: string): void { + const root = this.rootSpans.get(traceId); + if (!root) { + return; + } + const spanContext = root.spanContext(); + const pidHex = `0x${spanContext.spanId}`; + otelSpan.setAttribute("custom.parent.span.id", pidHex); + } +} diff --git a/src/genai/instrumentations/openai/semconv.ts b/src/genai/instrumentations/openai/semconv.ts new file mode 100644 index 00000000..74aea46d --- /dev/null +++ b/src/genai/instrumentations/openai/semconv.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// Vendored from microsoft/Agent365-nodejs packages/agents-a365-observability-extensions-openai +// Adapted: uses local semconv constants instead of A365 imports + +// Span kind classifiers (map OpenAI span data types to GenAI operation kinds) +export const GEN_AI_SPAN_KIND_AGENT = "agent" as const; +export const GEN_AI_SPAN_KIND_TOOL = "tool" as const; +export const GEN_AI_SPAN_KIND_CHAIN = "chain" as const; +export const GEN_AI_SPAN_KIND_CHAT = "chat" as const; + +// OpenAI-specific span attribute keys (not in OTel semconv) +export const GEN_AI_REQUEST_CONTENT_KEY = "gen_ai.request.content" as const; +export const GEN_AI_RESPONSE_CONTENT_KEY = "gen_ai.response.content" as const; +export const GEN_AI_EXECUTION_PAYLOAD_KEY = "gen_ai.execution.payload" as const; +export const GEN_AI_GRAPH_NODE_ID = "graph_node_id" as const; +export const GEN_AI_GRAPH_NODE_PARENT_ID = "graph_node_parent_id" as const; diff --git a/src/genai/instrumentations/openai/utils.ts b/src/genai/instrumentations/openai/utils.ts new file mode 100644 index 00000000..cf8503d0 --- /dev/null +++ b/src/genai/instrumentations/openai/utils.ts @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// Vendored from microsoft/Agent365-nodejs packages/agents-a365-observability-extensions-openai +// Adapted: removed A365 observability imports, uses local semconv + truncateValue + +import { SpanStatusCode } from "@opentelemetry/api"; +import type { Span as AgentsSpan, SpanData } from "@openai/agents-core"; +import { + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_TOOL_NAME, + ATTR_GEN_AI_USAGE_INPUT_TOKENS, + ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_OPERATION_CHAT, + GEN_AI_OPERATION_EXECUTE_TOOL, + GEN_AI_OPERATION_INVOKE_AGENT, +} from "../../index.js"; +import { truncateValue } from "../../utils.js"; +import { + GEN_AI_EXECUTION_PAYLOAD_KEY, + GEN_AI_REQUEST_CONTENT_KEY, + GEN_AI_RESPONSE_CONTENT_KEY, + GEN_AI_SPAN_KIND_AGENT, + GEN_AI_SPAN_KIND_CHAIN, + GEN_AI_SPAN_KIND_CHAT, + GEN_AI_SPAN_KIND_TOOL, +} from "./semconv.js"; + +/** + * Safely stringify an object to JSON, truncating to the attribute limit. + */ +export function safeJsonDumps(obj: unknown): string { + try { + return truncateValue(JSON.stringify(obj)); + } catch { + return truncateValue(String(obj)); + } +} + +/** + * Get span name from OpenAI Agents SDK span. + */ +export function getSpanName(span: AgentsSpan): string { + const data = span.spanData; + + const dataWithName = data as { name?: string }; + if (dataWithName?.name && typeof dataWithName.name === "string") { + return dataWithName.name; + } + + if (data?.type === "handoff") { + const handoffData = data as Record; + if (handoffData.to_agent) { + return `handoff to ${handoffData.to_agent}`; + } + } + + return data?.type || "unknown"; +} + +/** + * Map OpenAI span data type to a GenAI operation kind string. + */ +export function getSpanKind(spanData: SpanData | undefined): string { + if (!spanData?.type) { + return GEN_AI_SPAN_KIND_CHAIN; + } + + switch (spanData.type) { + case "agent": + return GEN_AI_SPAN_KIND_AGENT; + case "function": + return GEN_AI_SPAN_KIND_TOOL; + case "generation": + case "response": + return GEN_AI_SPAN_KIND_CHAT; + case "handoff": + case "custom": + case "guardrail": + default: + return GEN_AI_SPAN_KIND_CHAIN; + } +} + +/** + * Map OpenAI span data type to a GenAI semantic convention operation name. + */ +export function getOperationName(spanData: SpanData | undefined): string { + if (!spanData?.type) { + return GEN_AI_SPAN_KIND_CHAIN; + } + + switch (spanData.type) { + case "agent": + return GEN_AI_OPERATION_INVOKE_AGENT; + case "function": + return GEN_AI_OPERATION_EXECUTE_TOOL; + case "generation": + case "response": + return GEN_AI_OPERATION_CHAT; + case "handoff": + case "custom": + case "guardrail": + default: + return GEN_AI_SPAN_KIND_CHAIN; + } +} + +/** + * Derive OTel span status from an OpenAI Agents SDK span. + */ +export function getSpanStatus(span: AgentsSpan): { + code: SpanStatusCode; + message?: string; +} { + if (span.error) { + const message = span.error.message || span.error.data || "Unknown error"; + return { code: SpanStatusCode.ERROR, message: String(message) }; + } + return { code: SpanStatusCode.OK }; +} + +/** + * Extract attributes from a generation span. + */ +export function getAttributesFromGenerationSpanData(data: SpanData): Record { + const attributes: Record = { + [ATTR_GEN_AI_PROVIDER_NAME]: "openai", + }; + + const genData = data as Record; + + if (typeof genData.model === "string") { + attributes[ATTR_GEN_AI_REQUEST_MODEL] = genData.model; + } + + if (genData.model_config || genData.modelConfig) { + const config = genData.model_config || genData.modelConfig; + attributes[GEN_AI_EXECUTION_PAYLOAD_KEY] = safeJsonDumps(config); + } + + if (genData.input) { + attributes[GEN_AI_REQUEST_CONTENT_KEY] = safeJsonDumps(genData.input); + } + + if (genData.output) { + attributes[GEN_AI_RESPONSE_CONTENT_KEY] = safeJsonDumps(genData.output); + } + + if (genData.usage) { + const usage = genData.usage as Record; + if (usage.input_tokens !== undefined) { + attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens; + } + if (usage.output_tokens !== undefined) { + attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens; + } + } + + // Map operation name for generation spans + attributes[ATTR_GEN_AI_OPERATION_NAME] = GEN_AI_OPERATION_CHAT; + + return attributes; +} + +/** + * Extract attributes from a function/tool span. + */ +export function getAttributesFromFunctionSpanData(data: SpanData): Record { + const attributes: Record = {}; + + const funcData = data as Record; + + if (funcData.name) { + attributes[ATTR_GEN_AI_TOOL_NAME] = funcData.name; + } + + if (funcData.input) { + attributes[GEN_AI_REQUEST_CONTENT_KEY] = + typeof funcData.input === "string" + ? truncateValue(funcData.input) + : safeJsonDumps(funcData.input); + } + + if (funcData.output !== undefined && funcData.output !== null) { + const output = + typeof funcData.output === "object" + ? safeJsonDumps(funcData.output) + : truncateValue(String(funcData.output)); + attributes[GEN_AI_RESPONSE_CONTENT_KEY] = output; + } + + return attributes; +} + +/** + * Extract attributes from an MCP list-tools span. + */ +export function getAttributesFromMCPListToolsSpanData(data: SpanData): Record { + const attributes: Record = {}; + + const mcpData = data as Record; + if (mcpData.result) { + attributes[GEN_AI_RESPONSE_CONTENT_KEY] = safeJsonDumps(mcpData.result); + } + return attributes; +} + +/** + * Extract attributes from a response span. + */ +export function getAttributesFromResponse(response: unknown): Record { + const attributes: Record = {}; + + const resp = response as Record; + + if (resp.model) { + attributes[ATTR_GEN_AI_REQUEST_MODEL] = resp.model; + } + + if (resp.usage) { + const usage = resp.usage as Record; + if (usage.input_tokens !== undefined) { + attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS] = usage.input_tokens; + } + if (usage.output_tokens !== undefined) { + attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = usage.output_tokens; + } + } + + return attributes; +} + +/** Content-sensitive attribute keys that should only be recorded when content recording is enabled. */ +export const CONTENT_KEYS = new Set([ + "gen_ai.input.messages", + "gen_ai.output.messages", + "gen_ai.tool.call.arguments", + "gen_ai.tool.call.result", + GEN_AI_REQUEST_CONTENT_KEY, + GEN_AI_RESPONSE_CONTENT_KEY, +]); + +/** Key remapping table: `${spanType}${originalKey}` → target semconv key */ +export const KEY_MAPPINGS = new Map([ + [`mcp_tools${GEN_AI_RESPONSE_CONTENT_KEY}`, "gen_ai.tool.call.result"], + [`mcp_tools${GEN_AI_REQUEST_CONTENT_KEY}`, "gen_ai.tool.call.arguments"], + [`function${GEN_AI_RESPONSE_CONTENT_KEY}`, "gen_ai.tool.call.result"], + [`function${GEN_AI_REQUEST_CONTENT_KEY}`, "gen_ai.tool.call.arguments"], + [`generation${GEN_AI_RESPONSE_CONTENT_KEY}`, "gen_ai.output.messages"], + [`generation${GEN_AI_REQUEST_CONTENT_KEY}`, "gen_ai.input.messages"], +]); + +/** + * Build a JSON string of input messages, extracting user-role content. + */ +export function buildInputMessages(arr: Array<{ role: string; content: string }>): string { + const userTexts = arr + .filter((m) => m && m.role === "user" && typeof m.content === "string") + .map((m) => m.content); + return JSON.stringify(userTexts.length ? userTexts : arr); +} + +/** + * Build a JSON string of output messages, extracting output_text content. + */ +export function buildOutputMessages( + arr: Array<{ role: string; content: Array<{ type: string; text: string }> }>, +): string { + const userTexts: string[] = []; + for (const { content } of arr) { + if (!Array.isArray(content)) { + continue; + } + for (const { type, text } of content) { + if (type === "output_text" && typeof text === "string") { + userTexts.push(text); + } + } + } + return JSON.stringify(userTexts.length ? userTexts : arr); +} diff --git a/test/internal/functional/openai.test.ts b/test/internal/functional/openai.test.ts new file mode 100644 index 00000000..98e23a00 --- /dev/null +++ b/test/internal/functional/openai.test.ts @@ -0,0 +1,415 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Functional test for OpenAI Agents instrumentation. + * + * Exercises the full pipeline: OpenAIAgentsTraceProcessor receives OpenAI + * Agents SDK span callbacks and produces OTel spans captured by an + * InMemorySpanExporter. Validates that the correct spans, attributes, and + * parent-child relationships are emitted. + */ + +import { afterEach, assert, describe, it } from "vitest"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import type { Span as AgentsSpan, SpanData } from "@openai/agents-core"; +import { OpenAIAgentsTraceProcessor } from "../../../src/genai/instrumentations/openai/openAIAgentsTraceProcessor.js"; +import { + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_TOOL_NAME, + ATTR_GEN_AI_TOOL_TYPE, + ATTR_GEN_AI_USAGE_INPUT_TOKENS, + ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, + GEN_AI_OPERATION_CHAT, + GEN_AI_OPERATION_EXECUTE_TOOL, + GEN_AI_OPERATION_INVOKE_AGENT, +} from "../../../src/genai/index.js"; + +let provider: BasicTracerProvider; +let exporter: InMemorySpanExporter; +let processor: OpenAIAgentsTraceProcessor; + +function setup(options?: { + isContentRecordingEnabled?: boolean; + suppressInvokeAgentInput?: boolean; +}) { + exporter = new InMemorySpanExporter(); + provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + + const tracer = provider.getTracer("test-openai-agents", "1.0.0"); + processor = new OpenAIAgentsTraceProcessor(tracer, options); +} + +let spanCounter = 0; +function makeSpan(overrides: Partial> = {}): AgentsSpan { + const id = `span-${++spanCounter}`; + return { + spanId: id, + traceId: "trace-1", + parentId: undefined, + startedAt: new Date().toISOString(), + endedAt: new Date(Date.now() + 100).toISOString(), + spanData: { type: "agent", name: "TestAgent" }, + error: undefined, + ...overrides, + } as unknown as AgentsSpan; +} + +afterEach(async () => { + spanCounter = 0; + if (provider) { + await provider.forceFlush(); + await provider.shutdown(); + } + exporter?.reset(); +}); + +describe("OpenAI Agents Instrumentation Functional Tests", () => { + describe("single generation (LLM call)", () => { + it("produces a span with chat operation and model attributes", async () => { + setup({ isContentRecordingEnabled: true }); + + const span = makeSpan({ + spanId: "gen-1", + spanData: { + type: "generation", + model: "gpt-4o", + input: [{ role: "user", content: "Hello" }], + output: [{ role: "assistant", content: "Hi there!" }], + usage: { input_tokens: 50, output_tokens: 20 }, + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + + const otelSpan = spans[0]; + assert.ok( + otelSpan.name.includes("gpt-4o"), + `span name "${otelSpan.name}" should contain model name`, + ); + assert.strictEqual(otelSpan.status.code, SpanStatusCode.OK); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_PROVIDER_NAME], "openai"); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o"); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS], 50); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 20); + }); + }); + + describe("response span", () => { + it("extracts model and usage from response data", async () => { + setup({ isContentRecordingEnabled: true }); + + const span = makeSpan({ + spanId: "resp-1", + spanData: { + type: "response", + _response: { + model: "gpt-4o-mini", + usage: { input_tokens: 10, output_tokens: 5 }, + output: "Response text here", + }, + _input: [{ role: "user", content: "What is 2+2?" }], + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + + const otelSpan = spans[0]; + assert.ok(otelSpan.name.includes(GEN_AI_OPERATION_CHAT)); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o-mini"); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); + assert.strictEqual(otelSpan.attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 5); + }); + }); + + describe("agent with function tool and generation children", () => { + it("produces parent-child spans matching the agent execution flow", async () => { + setup({ isContentRecordingEnabled: true }); + + // 1. Agent span (root) + const agentSpan = makeSpan({ + spanId: "agent-1", + spanData: { type: "agent", name: "ResearchAgent" }, + }); + + // 2. Function/tool span (child of agent) + const toolSpan = makeSpan({ + spanId: "tool-1", + parentId: "agent-1", + spanData: { + type: "function", + name: "web_search", + input: "opentelemetry agents", + output: "search results...", + }, + }); + + // 3. Generation span (child of agent) + const genSpan = makeSpan({ + spanId: "gen-1", + parentId: "agent-1", + spanData: { + type: "generation", + model: "gpt-4o", + usage: { input_tokens: 200, output_tokens: 80 }, + }, + }); + + // Start in order: agent, then tool, then generation + await processor.onSpanStart(agentSpan); + await processor.onSpanStart(toolSpan); + await processor.onSpanStart(genSpan); + + // End in reverse order (children end before parent) + await processor.onSpanEnd(genSpan); + await processor.onSpanEnd(toolSpan); + await processor.onSpanEnd(agentSpan); + + // End root trace to emit the root span + await processor.onTraceEnd({ traceId: "trace-1" } as any); + + const spans = exporter.getFinishedSpans(); + // agent + tool + generation + root = 4 spans (root created by first span without parent) + assert.ok(spans.length >= 3, `expected at least 3 spans, got ${spans.length}`); + + // Find spans by operation + const agentOtel = spans.find( + (s) => s.attributes[ATTR_GEN_AI_OPERATION_NAME] === GEN_AI_OPERATION_INVOKE_AGENT, + ); + const toolOtel = spans.find( + (s) => s.attributes[ATTR_GEN_AI_OPERATION_NAME] === GEN_AI_OPERATION_EXECUTE_TOOL, + ); + const genOtel = spans.find( + (s) => s.attributes[ATTR_GEN_AI_OPERATION_NAME] === GEN_AI_OPERATION_CHAT, + ); + + assert.ok(agentOtel, "agent span should exist"); + assert.ok(toolOtel, "tool span should exist"); + assert.ok(genOtel, "generation span should exist"); + + // Verify agent span + assert.ok(agentOtel!.name.includes(GEN_AI_OPERATION_INVOKE_AGENT)); + assert.strictEqual(agentOtel!.attributes["graph_node_id"], "ResearchAgent"); + + // Verify tool span + assert.ok(toolOtel!.name.includes(GEN_AI_OPERATION_EXECUTE_TOOL)); + assert.strictEqual(toolOtel!.attributes[ATTR_GEN_AI_TOOL_NAME], "web_search"); + assert.strictEqual(toolOtel!.attributes[ATTR_GEN_AI_TOOL_TYPE], "function"); + + // Verify generation span + assert.strictEqual(genOtel!.attributes[ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o"); + assert.strictEqual(genOtel!.attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS], 200); + assert.strictEqual(genOtel!.attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 80); + + // Verify parent-child: tool and gen should be children of the root span + // (in OpenAI SDK, the first span without a parent becomes the root) + const rootSpan = spans.find((s) => !s.parentSpanContext?.spanId); + assert.ok(rootSpan, "root span should exist"); + + // Agent, tool, and gen should all be descendants of root + assert.strictEqual( + toolOtel!.parentSpanContext?.spanId, + rootSpan!.spanContext().spanId, + "tool span should be child of root span", + ); + assert.strictEqual( + genOtel!.parentSpanContext?.spanId, + rootSpan!.spanContext().spanId, + "gen span should be child of root span", + ); + }); + }); + + describe("handoff between agents", () => { + it("records handoff parent node in target agent span", async () => { + setup(); + + // Handoff span + const handoff = makeSpan({ + spanId: "handoff-1", + spanData: { + type: "handoff", + from_agent: "AgentA", + to_agent: "AgentB", + }, + }); + + await processor.onSpanStart(handoff); + await processor.onSpanEnd(handoff); + + // Target agent span + const agentB = makeSpan({ + spanId: "agent-b", + spanData: { type: "agent", name: "AgentB" }, + }); + + await processor.onSpanStart(agentB); + await processor.onSpanEnd(agentB); + + const spans = exporter.getFinishedSpans(); + const agentBSpan = spans.find((s) => s.attributes["graph_node_id"] === "AgentB"); + assert.ok(agentBSpan); + assert.strictEqual(agentBSpan!.attributes["graph_node_parent_id"], "AgentA"); + }); + }); + + describe("MCP tools span", () => { + it("produces a span with tool attributes for MCP server", async () => { + setup(); + + const span = makeSpan({ + spanId: "mcp-1", + spanData: { + type: "mcp_tools", + server: "my-mcp-server", + result: [{ name: "tool1" }, { name: "tool2" }], + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + // End root trace to emit the root span + await processor.onTraceEnd({ traceId: "trace-1" } as any); + + const spans = exporter.getFinishedSpans(); + assert.ok(spans.length >= 1, `expected at least 1 span, got ${spans.length}`); + + const mcpSpan = spans.find((s) => s.attributes[ATTR_GEN_AI_TOOL_NAME] === "my-mcp-server"); + assert.ok(mcpSpan, "MCP span should exist"); + assert.strictEqual( + mcpSpan!.attributes[ATTR_GEN_AI_OPERATION_NAME], + GEN_AI_OPERATION_EXECUTE_TOOL, + ); + assert.strictEqual(mcpSpan!.attributes[ATTR_GEN_AI_TOOL_TYPE], "extension"); + }); + }); + + describe("content recording gating", () => { + it("does not record content when disabled", async () => { + setup({ isContentRecordingEnabled: false }); + + const span = makeSpan({ + spanId: "gen-no-content", + spanData: { + type: "generation", + model: "gpt-4o", + input: "secret input", + output: "secret output", + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + // Root span is also created since no parent + const genSpan = spans.find((s) => s.attributes[ATTR_GEN_AI_REQUEST_MODEL] === "gpt-4o"); + assert.ok(genSpan); + + // Content keys should be absent + assert.strictEqual( + genSpan!.attributes["gen_ai.input.messages"], + undefined, + "should not record input", + ); + assert.strictEqual( + genSpan!.attributes["gen_ai.output.messages"], + undefined, + "should not record output", + ); + }); + + it("records content when enabled", async () => { + setup({ isContentRecordingEnabled: true }); + + const span = makeSpan({ + spanId: "gen-with-content", + spanData: { + type: "generation", + model: "gpt-4o", + input: "What is 2+2?", + output: "4", + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + const genSpan = spans.find((s) => s.attributes[ATTR_GEN_AI_REQUEST_MODEL] === "gpt-4o"); + assert.ok(genSpan); + + assert.ok(genSpan!.attributes["gen_ai.input.messages"], "should record input"); + assert.ok(genSpan!.attributes["gen_ai.output.messages"], "should record output"); + }); + }); + + describe("error handling", () => { + it("sets error status on failed spans", async () => { + setup(); + + const span = makeSpan({ + spanId: "err-1", + spanData: { type: "generation", model: "gpt-4o" }, + error: { message: "Rate limit exceeded" }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + const genSpan = spans.find((s) => s.attributes[ATTR_GEN_AI_REQUEST_MODEL] === "gpt-4o"); + assert.ok(genSpan); + assert.strictEqual(genSpan!.status.code, SpanStatusCode.ERROR); + assert.strictEqual(genSpan!.status.message, "Rate limit exceeded"); + }); + }); + + describe("suppressInvokeAgentInput", () => { + it("suppresses input messages when flag is set", async () => { + setup({ isContentRecordingEnabled: true, suppressInvokeAgentInput: true }); + + const span = makeSpan({ + spanId: "resp-suppress", + spanData: { + type: "response", + _response: { + model: "gpt-4o", + output: "Response text", + }, + _input: [{ role: "user", content: "Secret instructions" }], + }, + }); + + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + const spans = exporter.getFinishedSpans(); + const respSpan = spans.find((s) => s.attributes[ATTR_GEN_AI_REQUEST_MODEL] === "gpt-4o"); + assert.ok(respSpan); + assert.strictEqual( + respSpan!.attributes["gen_ai.input.messages"], + undefined, + "input should be suppressed", + ); + }); + }); +}); diff --git a/test/internal/unit/genai/openai/openAIAgentsTraceInstrumentor.test.ts b/test/internal/unit/genai/openai/openAIAgentsTraceInstrumentor.test.ts new file mode 100644 index 00000000..7dc3372a --- /dev/null +++ b/test/internal/unit/genai/openai/openAIAgentsTraceInstrumentor.test.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { afterEach, assert, describe, it, vi } from "vitest"; +import { OpenAIAgentsTraceInstrumentor } from "../../../../../src/genai/instrumentations/openai/openAIAgentsTraceInstrumentor.js"; + +// vi.mock is hoisted above imports by Vitest +vi.mock("@openai/agents", () => ({ + setTraceProcessors: vi.fn(), + setTracingDisabled: vi.fn(), +})); + +afterEach(() => { + OpenAIAgentsTraceInstrumentor.resetInstance(); + vi.restoreAllMocks(); +}); + +describe("OpenAIAgentsTraceInstrumentor", () => { + describe("instrument", () => { + it("does not throw when called", () => { + // Should not throw + OpenAIAgentsTraceInstrumentor.instrument(); + }); + + it("accepts configuration options", () => { + OpenAIAgentsTraceInstrumentor.instrument({ + isContentRecordingEnabled: true, + suppressInvokeAgentInput: true, + }); + }); + }); + + describe("enable / disable", () => { + it("throws when not initialized", () => { + assert.throws(() => OpenAIAgentsTraceInstrumentor.enable(), /must be initialized first/); + }); + + it("throws disable when not initialized", () => { + assert.throws(() => OpenAIAgentsTraceInstrumentor.disable(), /must be initialized first/); + }); + + it("enable and disable do not throw after initialization", () => { + OpenAIAgentsTraceInstrumentor.instrument(); + + // Should not throw + OpenAIAgentsTraceInstrumentor.enable(); + OpenAIAgentsTraceInstrumentor.disable(); + }); + }); + + describe("resetInstance", () => { + it("allows re-initialization after reset", () => { + OpenAIAgentsTraceInstrumentor.instrument(); + OpenAIAgentsTraceInstrumentor.resetInstance(); + + // Should be able to instrument again + OpenAIAgentsTraceInstrumentor.instrument(); + }); + }); +}); diff --git a/test/internal/unit/genai/openai/openAIAgentsTraceProcessor.test.ts b/test/internal/unit/genai/openai/openAIAgentsTraceProcessor.test.ts new file mode 100644 index 00000000..3acb5323 --- /dev/null +++ b/test/internal/unit/genai/openai/openAIAgentsTraceProcessor.test.ts @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert, beforeEach, describe, it, vi } from "vitest"; +import type { Tracer, Span as OtelSpan } from "@opentelemetry/api"; +import { SpanStatusCode } from "@opentelemetry/api"; +import type { Span as AgentsSpan, SpanData } from "@openai/agents-core"; +import type { Trace as AgentTrace } from "@openai/agents-core"; +import { OpenAIAgentsTraceProcessor } from "../../../../../src/genai/instrumentations/openai/openAIAgentsTraceProcessor.js"; + +function makeMockOtelSpan(): OtelSpan & { attrs: Record; _name: string } { + const attrs: Record = {}; + let _name = ""; + return { + attrs, + get _name() { + return _name; + }, + setAttribute: vi.fn((key: string, value: unknown) => { + attrs[key] = value; + }), + setStatus: vi.fn(), + end: vi.fn(), + updateName: vi.fn((name: string) => { + _name = name; + }), + spanContext: vi.fn(() => ({ + traceId: "0000000000000001", + spanId: "00000001", + traceFlags: 1, + })), + isRecording: vi.fn(() => true), + recordException: vi.fn(), + addEvent: vi.fn(), + addLink: vi.fn(), + addLinks: vi.fn(), + } as unknown as OtelSpan & { attrs: Record; _name: string }; +} + +function makeMockTracer(mockSpan?: OtelSpan): Tracer & { spans: OtelSpan[] } { + const spans: OtelSpan[] = []; + let hasReturnedInjectedSpan = false; + return { + spans, + startSpan: vi.fn(() => { + const span = + mockSpan && !hasReturnedInjectedSpan + ? ((hasReturnedInjectedSpan = true), mockSpan) + : makeMockOtelSpan(); + spans.push(span); + return span; + }), + startActiveSpan: vi.fn(), + } as unknown as Tracer; +} + +function makeAgentsSpan(overrides: Partial> = {}): AgentsSpan { + return { + spanId: "span-1", + traceId: "trace-1", + parentId: undefined, + startedAt: new Date().toISOString(), + endedAt: new Date().toISOString(), + spanData: { type: "agent", name: "TestAgent" }, + error: undefined, + ...overrides, + } as unknown as AgentsSpan; +} + +function makeTrace(traceId = "trace-1"): AgentTrace { + return { traceId } as unknown as AgentTrace; +} + +describe("OpenAIAgentsTraceProcessor", () => { + let processor: OpenAIAgentsTraceProcessor; + let mockSpan: OtelSpan & { attrs: Record }; + let tracer: Tracer & { spans: OtelSpan[] }; + + beforeEach(() => { + mockSpan = makeMockOtelSpan(); + tracer = makeMockTracer(mockSpan); + processor = new OpenAIAgentsTraceProcessor(tracer, { + isContentRecordingEnabled: true, + }); + }); + + describe("start / shutdown", () => { + it("start is a no-op", async () => { + await processor.start(); + }); + + it("shutdown clears internal state", async () => { + await processor.shutdown(); + }); + }); + + describe("onSpanStart / onSpanEnd", () => { + it("creates and ends an OTel span for an agent span", async () => { + const span = makeAgentsSpan(); + await processor.onSpanStart(span); + assert.ok((tracer.startSpan as ReturnType).mock.calls.length > 0); + + await processor.onSpanEnd(span); + assert.ok((mockSpan.setStatus as ReturnType).mock.calls.length > 0); + assert.ok((mockSpan.end as ReturnType).mock.calls.length > 0); + }); + + it("skips span start when missing required fields", async () => { + const span = makeAgentsSpan({ spanId: undefined }); + await processor.onSpanStart(span); + assert.strictEqual((tracer.startSpan as ReturnType).mock.calls.length, 0); + }); + + it("skips span end when no matching OTel span", async () => { + const span = makeAgentsSpan({ spanId: "nonexistent" }); + await processor.onSpanEnd(span); + // Should not throw + }); + + it("sets ERROR status for failed spans", async () => { + const span = makeAgentsSpan({ + error: { message: "something broke" }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + const statusCalls = (mockSpan.setStatus as ReturnType).mock.calls; + assert.ok(statusCalls.length > 0); + assert.strictEqual(statusCalls[0][0].code, SpanStatusCode.ERROR); + }); + }); + + describe("onTraceEnd", () => { + it("ends root span on trace end", async () => { + // Start a span that becomes the root (no parent) + const span = makeAgentsSpan(); + await processor.onSpanStart(span); + + const trace = makeTrace("trace-1"); + await processor.onTraceEnd(trace); + // Root span should be ended + }); + + it("does nothing for unknown trace", async () => { + const trace = makeTrace("unknown-trace"); + await processor.onTraceEnd(trace); + // Should not throw + }); + }); + + describe("agent span processing", () => { + it("sets agent attributes", async () => { + const span = makeAgentsSpan({ + spanData: { type: "agent", name: "MyAgent" }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + assert.strictEqual(mockSpan.attrs["graph_node_id"], "MyAgent"); + assert.strictEqual(mockSpan.attrs["gen_ai.operation.name"], "invoke_agent"); + }); + }); + + describe("generation span processing", () => { + it("sets model and usage attributes", async () => { + const span = makeAgentsSpan({ + spanData: { + type: "generation", + model: "gpt-4o", + usage: { input_tokens: 10, output_tokens: 20 }, + }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + assert.strictEqual(mockSpan.attrs["gen_ai.request.model"], "gpt-4o"); + assert.strictEqual(mockSpan.attrs["gen_ai.usage.input_tokens"], 10); + assert.strictEqual(mockSpan.attrs["gen_ai.usage.output_tokens"], 20); + }); + }); + + describe("function span processing", () => { + it("sets tool attributes and operation name", async () => { + const span = makeAgentsSpan({ + spanData: { + type: "function", + name: "get_weather", + input: "Seattle", + output: "Sunny", + }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + assert.strictEqual(mockSpan.attrs["gen_ai.tool.name"], "get_weather"); + assert.strictEqual(mockSpan.attrs["gen_ai.operation.name"], "execute_tool"); + assert.strictEqual(mockSpan.attrs["gen_ai.tool.type"], "function"); + }); + }); + + describe("response span processing", () => { + it("sets model and usage from response", async () => { + const span = makeAgentsSpan({ + spanData: { + type: "response", + _response: { + model: "gpt-4o-mini", + usage: { input_tokens: 3, output_tokens: 7 }, + }, + }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + assert.strictEqual(mockSpan.attrs["gen_ai.request.model"], "gpt-4o-mini"); + assert.strictEqual(mockSpan.attrs["gen_ai.usage.input_tokens"], 3); + }); + }); + + describe("handoff span processing", () => { + it("records handoff and resolves in agent span", async () => { + // Create handoff span + const handoffSpan = makeAgentsSpan({ + spanId: "span-handoff", + spanData: { + type: "handoff", + from_agent: "AgentA", + to_agent: "AgentB", + }, + }); + await processor.onSpanStart(handoffSpan); + await processor.onSpanEnd(handoffSpan); + + // Create agent span for the target agent + const agentSpan = makeAgentsSpan({ + spanId: "span-agent", + spanData: { type: "agent", name: "AgentB" }, + }); + await processor.onSpanStart(agentSpan); + await processor.onSpanEnd(agentSpan); + + const agentOtelSpan = tracer.spans[tracer.spans.length - 1] as OtelSpan & { + attrs: Record; + }; + assert.strictEqual(agentOtelSpan.attrs["graph_node_parent_id"], "AgentA"); + }); + }); + + describe("content recording", () => { + it("does not record content when disabled", async () => { + const noContentProcessor = new OpenAIAgentsTraceProcessor(tracer, { + isContentRecordingEnabled: false, + }); + + const span = makeAgentsSpan({ + spanData: { + type: "generation", + model: "gpt-4o", + input: "secret input", + output: "secret output", + }, + }); + await noContentProcessor.onSpanStart(span); + await noContentProcessor.onSpanEnd(span); + + // Model should still be set + assert.strictEqual(mockSpan.attrs["gen_ai.request.model"], "gpt-4o"); + // Content should NOT be set + assert.strictEqual(mockSpan.attrs["gen_ai.input.messages"], undefined); + assert.strictEqual(mockSpan.attrs["gen_ai.output.messages"], undefined); + }); + }); + + describe("mcp_tools span processing", () => { + it("sets tool attributes for MCP tools", async () => { + const span = makeAgentsSpan({ + spanData: { + type: "mcp_tools", + server: "my-mcp-server", + result: [{ name: "tool1" }], + }, + }); + await processor.onSpanStart(span); + await processor.onSpanEnd(span); + + assert.strictEqual(mockSpan.attrs["gen_ai.operation.name"], "execute_tool"); + assert.strictEqual(mockSpan.attrs["gen_ai.tool.name"], "my-mcp-server"); + assert.strictEqual(mockSpan.attrs["gen_ai.tool.type"], "extension"); + }); + }); +}); diff --git a/test/internal/unit/genai/openai/utils.test.ts b/test/internal/unit/genai/openai/utils.test.ts new file mode 100644 index 00000000..85807fbf --- /dev/null +++ b/test/internal/unit/genai/openai/utils.test.ts @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert, describe, it } from "vitest"; +import { SpanStatusCode } from "@opentelemetry/api"; +import type { Span as AgentsSpan, SpanData } from "@openai/agents-core"; +import { + safeJsonDumps, + getSpanName, + getSpanKind, + getSpanStatus, + getAttributesFromGenerationSpanData, + getAttributesFromFunctionSpanData, + getAttributesFromMCPListToolsSpanData, + getAttributesFromResponse, + buildInputMessages, + buildOutputMessages, + CONTENT_KEYS, + KEY_MAPPINGS, +} from "../../../../../src/genai/instrumentations/openai/utils.js"; +import { + GEN_AI_SPAN_KIND_AGENT, + GEN_AI_SPAN_KIND_CHAIN, + GEN_AI_SPAN_KIND_CHAT, + GEN_AI_SPAN_KIND_TOOL, + GEN_AI_REQUEST_CONTENT_KEY, + GEN_AI_RESPONSE_CONTENT_KEY, +} from "../../../../../src/genai/instrumentations/openai/semconv.js"; +import { + ATTR_GEN_AI_PROVIDER_NAME, + ATTR_GEN_AI_REQUEST_MODEL, + ATTR_GEN_AI_TOOL_NAME, + ATTR_GEN_AI_USAGE_INPUT_TOKENS, + ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, +} from "../../../../../src/genai/index.js"; + +function makeAgentsSpan(overrides: Partial> = {}): AgentsSpan { + return { + spanId: "span-1", + traceId: "trace-1", + parentId: undefined, + startedAt: new Date().toISOString(), + endedAt: undefined, + spanData: { type: "agent" } as SpanData, + error: undefined, + ...overrides, + } as unknown as AgentsSpan; +} + +describe("OpenAI Utils", () => { + describe("safeJsonDumps", () => { + it("returns JSON string for an object", () => { + const result = safeJsonDumps({ hello: "world" }); + assert.strictEqual(result, '{"hello":"world"}'); + }); + + it("handles circular references gracefully", () => { + const obj: Record = {}; + obj.self = obj; + const result = safeJsonDumps(obj); + assert.ok(typeof result === "string"); + }); + }); + + describe("getSpanName", () => { + it("returns name from spanData if present", () => { + const span = makeAgentsSpan({ + spanData: { type: "agent", name: "MyAgent" } as unknown as SpanData, + }); + assert.strictEqual(getSpanName(span), "MyAgent"); + }); + + it("returns handoff name for handoff spans", () => { + const span = makeAgentsSpan({ + spanData: { type: "handoff", to_agent: "TargetAgent" } as unknown as SpanData, + }); + assert.strictEqual(getSpanName(span), "handoff to TargetAgent"); + }); + + it("returns type when no name is present", () => { + const span = makeAgentsSpan({ + spanData: { type: "generation" } as SpanData, + }); + assert.strictEqual(getSpanName(span), "generation"); + }); + + it("returns 'unknown' for missing data", () => { + const span = makeAgentsSpan({ spanData: undefined as unknown as SpanData }); + assert.strictEqual(getSpanName(span), "unknown"); + }); + }); + + describe("getSpanKind", () => { + it("maps 'agent' to GEN_AI_SPAN_KIND_AGENT", () => { + assert.strictEqual(getSpanKind({ type: "agent" } as SpanData), GEN_AI_SPAN_KIND_AGENT); + }); + + it("maps 'function' to GEN_AI_SPAN_KIND_TOOL", () => { + assert.strictEqual(getSpanKind({ type: "function" } as SpanData), GEN_AI_SPAN_KIND_TOOL); + }); + + it("maps 'generation' to GEN_AI_SPAN_KIND_CHAT", () => { + assert.strictEqual(getSpanKind({ type: "generation" } as SpanData), GEN_AI_SPAN_KIND_CHAT); + }); + + it("maps 'response' to GEN_AI_SPAN_KIND_CHAT", () => { + assert.strictEqual(getSpanKind({ type: "response" } as SpanData), GEN_AI_SPAN_KIND_CHAT); + }); + + it("maps 'handoff' to GEN_AI_SPAN_KIND_CHAIN", () => { + assert.strictEqual(getSpanKind({ type: "handoff" } as SpanData), GEN_AI_SPAN_KIND_CHAIN); + }); + + it("maps undefined to GEN_AI_SPAN_KIND_CHAIN", () => { + assert.strictEqual(getSpanKind(undefined), GEN_AI_SPAN_KIND_CHAIN); + }); + }); + + describe("getSpanStatus", () => { + it("returns OK for span without error", () => { + const span = makeAgentsSpan(); + const status = getSpanStatus(span); + assert.strictEqual(status.code, SpanStatusCode.OK); + }); + + it("returns ERROR for span with error", () => { + const span = makeAgentsSpan({ + error: { message: "something failed" } as unknown, + } as Partial>); + const status = getSpanStatus(span); + assert.strictEqual(status.code, SpanStatusCode.ERROR); + assert.strictEqual(status.message, "something failed"); + }); + + it("returns 'Unknown error' when error has no message", () => { + const span = makeAgentsSpan({ + error: {} as unknown, + } as Partial>); + const status = getSpanStatus(span); + assert.strictEqual(status.code, SpanStatusCode.ERROR); + assert.strictEqual(status.message, "Unknown error"); + }); + }); + + describe("getAttributesFromGenerationSpanData", () => { + it("extracts model and provider", () => { + const data = { type: "generation", model: "gpt-4o" } as unknown as SpanData; + const attrs = getAttributesFromGenerationSpanData(data); + assert.strictEqual(attrs[ATTR_GEN_AI_PROVIDER_NAME], "openai"); + assert.strictEqual(attrs[ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o"); + }); + + it("extracts usage tokens", () => { + const data = { + type: "generation", + usage: { input_tokens: 10, output_tokens: 20 }, + } as unknown as SpanData; + const attrs = getAttributesFromGenerationSpanData(data); + assert.strictEqual(attrs[ATTR_GEN_AI_USAGE_INPUT_TOKENS], 10); + assert.strictEqual(attrs[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 20); + }); + + it("extracts input and output content", () => { + const data = { + type: "generation", + input: "hello", + output: "world", + } as unknown as SpanData; + const attrs = getAttributesFromGenerationSpanData(data); + assert.strictEqual(attrs[GEN_AI_REQUEST_CONTENT_KEY], '"hello"'); + assert.strictEqual(attrs[GEN_AI_RESPONSE_CONTENT_KEY], '"world"'); + }); + }); + + describe("getAttributesFromFunctionSpanData", () => { + it("extracts tool name", () => { + const data = { type: "function", name: "get_weather" } as unknown as SpanData; + const attrs = getAttributesFromFunctionSpanData(data); + assert.strictEqual(attrs[ATTR_GEN_AI_TOOL_NAME], "get_weather"); + }); + + it("extracts input and output", () => { + const data = { + type: "function", + name: "my_tool", + input: "test input", + output: "test output", + } as unknown as SpanData; + const attrs = getAttributesFromFunctionSpanData(data); + assert.strictEqual(attrs[GEN_AI_REQUEST_CONTENT_KEY], "test input"); + assert.strictEqual(attrs[GEN_AI_RESPONSE_CONTENT_KEY], "test output"); + }); + + it("handles object input", () => { + const data = { + type: "function", + name: "my_tool", + input: { city: "Seattle" }, + } as unknown as SpanData; + const attrs = getAttributesFromFunctionSpanData(data); + assert.strictEqual(attrs[GEN_AI_REQUEST_CONTENT_KEY], '{"city":"Seattle"}'); + }); + }); + + describe("getAttributesFromMCPListToolsSpanData", () => { + it("extracts result", () => { + const data = { + type: "mcp_tools", + result: [{ name: "tool1" }], + } as unknown as SpanData; + const attrs = getAttributesFromMCPListToolsSpanData(data); + assert.strictEqual(attrs[GEN_AI_RESPONSE_CONTENT_KEY], '[{"name":"tool1"}]'); + }); + + it("returns empty when no result", () => { + const data = { type: "mcp_tools" } as unknown as SpanData; + const attrs = getAttributesFromMCPListToolsSpanData(data); + assert.strictEqual(Object.keys(attrs).length, 0); + }); + }); + + describe("getAttributesFromResponse", () => { + it("extracts model and usage", () => { + const response = { + model: "gpt-4o", + usage: { input_tokens: 5, output_tokens: 15 }, + }; + const attrs = getAttributesFromResponse(response); + assert.strictEqual(attrs[ATTR_GEN_AI_REQUEST_MODEL], "gpt-4o"); + assert.strictEqual(attrs[ATTR_GEN_AI_USAGE_INPUT_TOKENS], 5); + assert.strictEqual(attrs[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS], 15); + }); + + it("handles missing fields", () => { + const attrs = getAttributesFromResponse({}); + assert.strictEqual(Object.keys(attrs).length, 0); + }); + }); + + describe("buildInputMessages", () => { + it("extracts user role content", () => { + const messages = [ + { role: "system", content: "You are helpful" }, + { role: "user", content: "Hello" }, + ]; + const result = buildInputMessages(messages); + assert.strictEqual(result, '["Hello"]'); + }); + + it("falls back to full array when no user messages", () => { + const messages = [{ role: "system", content: "System prompt" }]; + const result = buildInputMessages(messages); + assert.strictEqual(result, JSON.stringify(messages)); + }); + }); + + describe("buildOutputMessages", () => { + it("extracts output_text content", () => { + const messages = [ + { + role: "assistant", + content: [ + { type: "output_text", text: "Hello there!" }, + { type: "other", text: "ignored" }, + ], + }, + ]; + const result = buildOutputMessages(messages); + assert.strictEqual(result, '["Hello there!"]'); + }); + + it("falls back to full array when no output_text", () => { + const messages = [ + { + role: "assistant", + content: [{ type: "tool_call", text: "" }], + }, + ]; + const result = buildOutputMessages(messages); + assert.strictEqual(result, JSON.stringify(messages)); + }); + }); + + describe("CONTENT_KEYS", () => { + it("contains expected content-sensitive keys", () => { + assert.ok(CONTENT_KEYS.has("gen_ai.input.messages")); + assert.ok(CONTENT_KEYS.has("gen_ai.output.messages")); + assert.ok(CONTENT_KEYS.has("gen_ai.tool.call.arguments")); + assert.ok(CONTENT_KEYS.has("gen_ai.tool.call.result")); + assert.ok(CONTENT_KEYS.has(GEN_AI_REQUEST_CONTENT_KEY)); + assert.ok(CONTENT_KEYS.has(GEN_AI_RESPONSE_CONTENT_KEY)); + }); + }); + + describe("KEY_MAPPINGS", () => { + it("maps function request content to tool args", () => { + assert.strictEqual( + KEY_MAPPINGS.get(`function${GEN_AI_REQUEST_CONTENT_KEY}`), + "gen_ai.tool.call.arguments", + ); + }); + + it("maps generation response content to output messages", () => { + assert.strictEqual( + KEY_MAPPINGS.get(`generation${GEN_AI_RESPONSE_CONTENT_KEY}`), + "gen_ai.output.messages", + ); + }); + }); +});