diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index 802de7ccb6..3324c301db 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -128,6 +128,9 @@ components:
- sharadraju
packages/instrumentation-runtime-node:
- d4nyll
+ packages/instrumentation-openai:
+ - trentm
+ - seemk
packages/instrumentation-pg:
- maryliag
packages/instrumentation-pino:
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f22b1b37b9..64982fd4ec 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -50,6 +50,7 @@
"packages/instrumentation-mysql2": "0.50.0",
"packages/instrumentation-nestjs-core": "0.49.0",
"packages/instrumentation-net": "0.47.0",
+ "packages/instrumentation-openai": "0.0.1",
"packages/instrumentation-oracledb": "0.29.0",
"packages/instrumentation-pg": "0.56.0",
"packages/instrumentation-pino": "0.50.0",
diff --git a/package-lock.json b/package-lock.json
index ce6cac21c8..78752d0c0a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7318,6 +7318,24 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.38.7",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz",
+ "integrity": "sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz",
@@ -8540,6 +8558,31 @@
"@octokit/openapi-types": "^24.2.0"
}
},
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -9031,6 +9074,10 @@
"resolved": "packages/instrumentation-net",
"link": true
},
+ "node_modules/@opentelemetry/instrumentation-openai": {
+ "resolved": "packages/instrumentation-openai",
+ "link": true
+ },
"node_modules/@opentelemetry/instrumentation-oracledb": {
"resolved": "packages/instrumentation-oracledb",
"link": true
@@ -11090,6 +11137,24 @@
"generic-pool": "*"
}
},
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/glob/node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/http-assert": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz",
@@ -11267,6 +11332,17 @@
"undici-types": "~7.8.0"
}
},
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@@ -11551,6 +11627,109 @@
}
}
},
+ "node_modules/@typescript-eslint/experimental-utils": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.1.tgz",
+ "integrity": "sha512-fbodVnjIDU4JpeXWRDsG5IfIjYBxEvs8EBO8W1+YVdtrc2B9ppfof5sZhVEDOtgTfFHnYQJDI8+qdqLYO4ceww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/typescript-estree": "5.8.1",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz",
+ "integrity": "sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.8.1.tgz",
+ "integrity": "sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz",
+ "integrity": "sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz",
+ "integrity": "sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "eslint-visitor-keys": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
@@ -17819,6 +17998,27 @@
"node": ">= 6"
}
},
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
"node_modules/formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
@@ -19692,6 +19892,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -24436,6 +24643,27 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -25432,6 +25660,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -29321,6 +29556,13 @@
"bare-events": "^2.2.0"
}
},
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
@@ -31353,6 +31595,16 @@
"defaults": "^1.0.3"
}
},
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -34144,6 +34396,282 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
+ "packages/instrumentation-openai": {
+ "name": "@opentelemetry/instrumentation-openai",
+ "version": "0.0.1",
+ "dependencies": {
+ "@opentelemetry/api-logs": "^0.203.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.36.0"
+ },
+ "devDependencies": {
+ "@opentelemetry/api": "^1.3.0",
+ "@opentelemetry/contrib-test-utils": "^0.49.0",
+ "@opentelemetry/sdk-logs": "^0.203.0",
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
+ "@types/glob": "^8.1.0",
+ "@types/node": "18.18.14",
+ "@typescript-eslint/eslint-plugin": "5.8.1",
+ "@typescript-eslint/parser": "5.8.1",
+ "cross-env": "7.0.3",
+ "expect": "29.2.0",
+ "nock": "^14.0.0",
+ "nyc": "17.1.0",
+ "openai": "4.104.0",
+ "rimraf": "5.0.10",
+ "safe-stable-stringify": "^2.4.1",
+ "sinon": "15.2.0",
+ "test-all-versions": "6.1.0",
+ "typescript": "5.0.4"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@types/node": {
+ "version": "18.18.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.14.tgz",
+ "integrity": "sha512-iSOeNeXYNYNLLOMDSVPvIFojclvMZ/HDY2dU17kUlcsOsSQETbWIslJbYLZgA+ox8g2XQwSHKTkght1a5X26lQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz",
+ "integrity": "sha512-wTZ5oEKrKj/8/366qTM366zqhIKAp6NCMweoRONtfuC07OAU9nVI2GZZdqQ1qD30WAAtcPdkH+npDwtRFdp4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "5.8.1",
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "debug": "^4.3.2",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.2.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/parser": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.8.1.tgz",
+ "integrity": "sha512-K1giKHAjHuyB421SoXMXFHHVI4NdNY603uKw92++D3qyxSeYvC10CBJ/GE5Thpo4WTUvu1mmJI2/FFkz38F2Gw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/typescript-estree": "5.8.1",
+ "debug": "^4.3.2"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz",
+ "integrity": "sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/types": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.8.1.tgz",
+ "integrity": "sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz",
+ "integrity": "sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "packages/instrumentation-openai/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz",
+ "integrity": "sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.8.1",
+ "eslint-visitor-keys": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/nock": {
+ "version": "14.0.5",
+ "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.5.tgz",
+ "integrity": "sha512-R49fALR9caB6vxuSWUIaK2eBYeTloZQUFBZ4rHO+TbhMGQHtwnhdqKLYki+o+8qMgLvoBYWrp/2KzGPhxL4S6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@mswjs/interceptors": "^0.38.7",
+ "json-stringify-safe": "^5.0.1",
+ "propagate": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18.20.0 <20 || >=20.12.1"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/openai": {
+ "version": "4.104.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ },
+ "bin": {
+ "openai": "bin/cli"
+ },
+ "peerDependencies": {
+ "ws": "^8.18.0",
+ "zod": "^3.23.8"
+ },
+ "peerDependenciesMeta": {
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "packages/instrumentation-openai/node_modules/typescript": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
+ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "packages/instrumentation-openai/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "packages/instrumentation-openai/node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "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
+ }
+ }
+ },
"packages/instrumentation-oracledb": {
"name": "@opentelemetry/instrumentation-oracledb",
"version": "0.29.0",
@@ -41734,6 +42262,20 @@
"sparse-bitfield": "^3.0.3"
}
},
+ "@mswjs/interceptors": {
+ "version": "0.38.7",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz",
+ "integrity": "sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==",
+ "dev": true,
+ "requires": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ }
+ },
"@napi-rs/wasm-runtime": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz",
@@ -42594,6 +43136,28 @@
"@octokit/openapi-types": "^24.2.0"
}
},
+ "@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true
+ },
+ "@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "requires": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true
+ },
"@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -44407,6 +44971,160 @@
}
}
},
+ "@opentelemetry/instrumentation-openai": {
+ "version": "file:packages/instrumentation-openai",
+ "requires": {
+ "@opentelemetry/api": "^1.3.0",
+ "@opentelemetry/api-logs": "^0.203.0",
+ "@opentelemetry/contrib-test-utils": "^0.49.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/sdk-logs": "^0.203.0",
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
+ "@opentelemetry/semantic-conventions": "^1.36.0",
+ "@types/glob": "^8.1.0",
+ "@types/node": "18.18.14",
+ "@typescript-eslint/eslint-plugin": "5.8.1",
+ "@typescript-eslint/parser": "5.8.1",
+ "cross-env": "7.0.3",
+ "expect": "29.2.0",
+ "nock": "^14.0.0",
+ "nyc": "17.1.0",
+ "openai": "4.104.0",
+ "rimraf": "5.0.10",
+ "safe-stable-stringify": "^2.4.1",
+ "sinon": "15.2.0",
+ "test-all-versions": "6.1.0",
+ "typescript": "5.0.4"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "18.18.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.14.tgz",
+ "integrity": "sha512-iSOeNeXYNYNLLOMDSVPvIFojclvMZ/HDY2dU17kUlcsOsSQETbWIslJbYLZgA+ox8g2XQwSHKTkght1a5X26lQ==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "@typescript-eslint/eslint-plugin": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz",
+ "integrity": "sha512-wTZ5oEKrKj/8/366qTM366zqhIKAp6NCMweoRONtfuC07OAU9nVI2GZZdqQ1qD30WAAtcPdkH+npDwtRFdp4Rw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "5.8.1",
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "debug": "^4.3.2",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.2.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.8.1.tgz",
+ "integrity": "sha512-K1giKHAjHuyB421SoXMXFHHVI4NdNY603uKw92++D3qyxSeYvC10CBJ/GE5Thpo4WTUvu1mmJI2/FFkz38F2Gw==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/typescript-estree": "5.8.1",
+ "debug": "^4.3.2"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz",
+ "integrity": "sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.8.1.tgz",
+ "integrity": "sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz",
+ "integrity": "sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz",
+ "integrity": "sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "eslint-visitor-keys": "^3.0.0"
+ }
+ },
+ "nock": {
+ "version": "14.0.5",
+ "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.5.tgz",
+ "integrity": "sha512-R49fALR9caB6vxuSWUIaK2eBYeTloZQUFBZ4rHO+TbhMGQHtwnhdqKLYki+o+8qMgLvoBYWrp/2KzGPhxL4S6w==",
+ "dev": true,
+ "requires": {
+ "@mswjs/interceptors": "^0.38.7",
+ "json-stringify-safe": "^5.0.1",
+ "propagate": "^2.0.0"
+ }
+ },
+ "openai": {
+ "version": "4.104.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz",
+ "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "typescript": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
+ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
+ "dev": true
+ },
+ "undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
+ "ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "requires": {}
+ }
+ }
+ },
"@opentelemetry/instrumentation-oracledb": {
"version": "file:packages/instrumentation-oracledb",
"requires": {
@@ -47461,6 +48179,24 @@
"generic-pool": "*"
}
},
+ "@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "dev": true,
+ "requires": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ },
+ "dependencies": {
+ "@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true
+ }
+ }
+ },
"@types/http-assert": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz",
@@ -47636,6 +48372,16 @@
"undici-types": "~7.8.0"
}
},
+ "@types/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "form-data": "^4.0.0"
+ }
+ },
"@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
@@ -47902,6 +48648,63 @@
"tsutils": "^3.21.0"
}
},
+ "@typescript-eslint/experimental-utils": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.1.tgz",
+ "integrity": "sha512-fbodVnjIDU4JpeXWRDsG5IfIjYBxEvs8EBO8W1+YVdtrc2B9ppfof5sZhVEDOtgTfFHnYQJDI8+qdqLYO4ceww==",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.9",
+ "@typescript-eslint/scope-manager": "5.8.1",
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/typescript-estree": "5.8.1",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.8.1.tgz",
+ "integrity": "sha512-DGxJkNyYruFH3NIZc3PwrzwOQAg7vvgsHsHCILOLvUpupgkwDZdNq/cXU3BjF4LNrCsVg0qxEyWasys5AiJ85Q==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.8.1.tgz",
+ "integrity": "sha512-L/FlWCCgnjKOLefdok90/pqInkomLnAcF9UAzNr+DSqMC3IffzumHTQTrINXhP1gVp9zlHiYYjvozVZDPleLcA==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.1.tgz",
+ "integrity": "sha512-26lQ8l8tTbG7ri7xEcCFT9ijU5Fk+sx/KRRyyzCv7MQ+rZZlqiDPtMKWLC8P7o+dtCnby4c+OlxuX1tp8WfafQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "@typescript-eslint/visitor-keys": "5.8.1",
+ "debug": "^4.3.2",
+ "globby": "^11.0.4",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.8.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.1.tgz",
+ "integrity": "sha512-SWgiWIwocK6NralrJarPZlWdr0hZnj5GXHIgfdm8hNkyKvpeQuFyLP6YjSIe9kf3YBIfU6OHSZLYkQ+smZwtNg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.8.1",
+ "eslint-visitor-keys": "^3.0.0"
+ }
+ }
+ }
+ },
"@typescript-eslint/parser": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
@@ -52672,6 +53475,22 @@
"mime-types": "^2.1.12"
}
},
+ "form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "dev": true
+ },
+ "formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "dev": true,
+ "requires": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ }
+ },
"formidable": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
@@ -54048,6 +54867,12 @@
"integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
"dev": true
},
+ "is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true
+ },
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -57767,6 +58592,12 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true
},
+ "node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "dev": true
+ },
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -58531,6 +59362,12 @@
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true
},
+ "outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true
+ },
"own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -61492,6 +62329,12 @@
"text-decoder": "^1.1.0"
}
},
+ "strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true
+ },
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
@@ -62935,6 +63778,12 @@
"defaults": "^1.0.3"
}
},
+ "web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "dev": true
+ },
"webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
diff --git a/packages/instrumentation-openai/.eslintignore b/packages/instrumentation-openai/.eslintignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/packages/instrumentation-openai/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/packages/instrumentation-openai/.eslintrc.js b/packages/instrumentation-openai/.eslintrc.js
new file mode 100644
index 0000000000..f726f3becb
--- /dev/null
+++ b/packages/instrumentation-openai/.eslintrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ "env": {
+ "mocha": true,
+ "node": true
+ },
+ ...require('../../eslint.config.js')
+}
diff --git a/packages/instrumentation-openai/.tav.yml b/packages/instrumentation-openai/.tav.yml
new file mode 100644
index 0000000000..ac264a8790
--- /dev/null
+++ b/packages/instrumentation-openai/.tav.yml
@@ -0,0 +1,5 @@
+openai:
+ versions:
+ include: '>=4.19.0 <6'
+ mode: max-5
+ commands: npm test
diff --git a/packages/instrumentation-openai/LICENSE b/packages/instrumentation-openai/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/packages/instrumentation-openai/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/instrumentation-openai/README.md b/packages/instrumentation-openai/README.md
new file mode 100644
index 0000000000..4f030da907
--- /dev/null
+++ b/packages/instrumentation-openai/README.md
@@ -0,0 +1,56 @@
+# OpenTelemetry OpenAI module Instrumentation for Node.js
+
+[![NPM Published Version][npm-img]][npm-url]
+[![Apache License][license-image]][license-image]
+
+This module provides automatic instrumentation for the [`openai`](https://www.npmjs.com/package/openai) module, which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package.
+
+Compatible with OpenTelemetry JS API and SDK `1.0+`.
+
+## Installation
+
+```bash
+npm install --save @opentelemetry/instrumentation-openai
+```
+
+## Supported Versions
+
+- openai `>=4.19.0 <6`
+
+## Usage
+
+```js
+const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
+const { OpenAIInstrumentation } = require('@opentelemetry/instrumentation-openai');
+const { registerInstrumentations } = require('@opentelemetry/instrumentation');
+
+const provider = new NodeTracerProvider();
+provider.register();
+
+registerInstrumentations({
+ instrumentations: [
+ new OpenAIInstrumentation(),
+ // other instrumentations
+ ],
+});
+```
+
+## Semantic Conventions
+
+This package implements Semantic Convention [Version 1.36.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/README.md).
+
+## Useful links
+
+- For more information on OpenTelemetry, visit:
+- For more about OpenTelemetry JavaScript:
+- For help or feedback on this project, join us in [GitHub Discussions][discussions-url]
+
+## License
+
+Apache 2.0 - See [LICENSE][license-url] for more information.
+
+[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions
+[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE
+[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
+[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-openai
+[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-openai.svg
diff --git a/packages/instrumentation-openai/package.json b/packages/instrumentation-openai/package.json
new file mode 100644
index 0000000000..05a42df71c
--- /dev/null
+++ b/packages/instrumentation-openai/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "@opentelemetry/instrumentation-openai",
+ "version": "0.0.1",
+ "description": "OpenTelemetry instrumentation for the `openai` OpenAI client library",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/open-telemetry/opentelemetry-js-contrib.git",
+ "directory": "packages/instrumentation-openai"
+ },
+ "scripts": {
+ "clean": "rimraf build/*",
+ "setup:dev": "nx run-many -t compile -p @opentelemetry/instrumentation-openai",
+ "compile": "tsc -p .",
+ "lint": "eslint . --ext .ts",
+ "lint:fix": "eslint . --ext .ts --fix",
+ "lint:readme": "node ../../scripts/lint-readme.js",
+ "prepublishOnly": "npm run compile",
+ "tdd": "npm run test -- --watch-extensions ts --watch",
+ "test": "nyc mocha --require '@opentelemetry/contrib-test-utils' 'test/**/*.test.ts'",
+ "test-all-versions": "tav",
+ "version:update": "node ../../scripts/version-update.js",
+ "watch": "tsc -w"
+ },
+ "keywords": [
+ "instrumentation",
+ "opentelemetry",
+ "observability",
+ "openai",
+ "llm",
+ "genai"
+ ],
+ "author": "OpenTelemetry Authors",
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "files": [
+ "build/src/**/*.js",
+ "build/src/**/*.js.map",
+ "build/src/**/*.d.ts"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "devDependencies": {
+ "@opentelemetry/api": "^1.3.0",
+ "@opentelemetry/contrib-test-utils": "^0.49.0",
+ "@opentelemetry/sdk-logs": "^0.203.0",
+ "@opentelemetry/sdk-trace-base": "^2.0.0",
+ "@opentelemetry/sdk-trace-node": "^2.0.0",
+ "@types/glob": "^8.1.0",
+ "@types/node": "18.18.14",
+ "@typescript-eslint/eslint-plugin": "5.8.1",
+ "@typescript-eslint/parser": "5.8.1",
+ "cross-env": "7.0.3",
+ "expect": "29.2.0",
+ "nock": "^14.0.0",
+ "nyc": "17.1.0",
+ "openai": "4.104.0",
+ "rimraf": "5.0.10",
+ "safe-stable-stringify": "^2.4.1",
+ "sinon": "15.2.0",
+ "test-all-versions": "6.1.0",
+ "typescript": "5.0.4"
+ },
+ "dependencies": {
+ "@opentelemetry/api-logs": "^0.203.0",
+ "@opentelemetry/instrumentation": "^0.203.0",
+ "@opentelemetry/semantic-conventions": "^1.36.0"
+ },
+ "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-openai#readme"
+}
diff --git a/packages/instrumentation-openai/src/index.ts b/packages/instrumentation-openai/src/index.ts
new file mode 100644
index 0000000000..da127f7b58
--- /dev/null
+++ b/packages/instrumentation-openai/src/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { OpenAIInstrumentation } from './instrumentation';
+export type { OpenAIInstrumentationConfig } from './types';
diff --git a/packages/instrumentation-openai/src/instrumentation.ts b/packages/instrumentation-openai/src/instrumentation.ts
new file mode 100644
index 0000000000..22277c711f
--- /dev/null
+++ b/packages/instrumentation-openai/src/instrumentation.ts
@@ -0,0 +1,842 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// avoids a dependency on @opentelemetry/core for hrTime utilities
+import { performance } from 'perf_hooks';
+
+import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
+import type { Attributes, Context, Histogram, Span } from '@opentelemetry/api';
+import {
+ InstrumentationBase,
+ InstrumentationNodeModuleDefinition,
+} from '@opentelemetry/instrumentation';
+import { SeverityNumber } from '@opentelemetry/api-logs';
+import type {
+ ChatCompletion,
+ ChatCompletionMessageToolCall,
+ ChatCompletionContentPart,
+ ChatCompletionContentPartRefusal,
+ ChatCompletionContentPartText,
+ ChatCompletionCreateParams,
+ ChatCompletionMessageParam,
+ ChatCompletionChunk,
+ Completions as ChatCompletions,
+} from 'openai/resources/chat/completions';
+import type { APIPromise } from 'openai/core';
+import type {
+ CreateEmbeddingResponse,
+ Embeddings,
+ EmbeddingCreateParams,
+} from 'openai/resources/embeddings';
+import type { Stream } from 'openai/streaming';
+
+import {
+ ATTR_EVENT_NAME,
+ ATTR_GEN_AI_OPERATION_NAME,
+ ATTR_GEN_AI_REQUEST_ENCODING_FORMATS,
+ ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY,
+ ATTR_GEN_AI_REQUEST_MAX_TOKENS,
+ ATTR_GEN_AI_REQUEST_MODEL,
+ ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY,
+ ATTR_GEN_AI_REQUEST_STOP_SEQUENCES,
+ ATTR_GEN_AI_REQUEST_TEMPERATURE,
+ ATTR_GEN_AI_REQUEST_TOP_P,
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
+ ATTR_GEN_AI_RESPONSE_ID,
+ ATTR_GEN_AI_RESPONSE_MODEL,
+ ATTR_GEN_AI_SYSTEM,
+ ATTR_GEN_AI_TOKEN_TYPE,
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
+ METRIC_GEN_AI_CLIENT_OPERATION_DURATION,
+ METRIC_GEN_AI_CLIENT_TOKEN_USAGE,
+} from './semconv';
+/** @knipignore */
+import { PACKAGE_NAME, PACKAGE_VERSION } from './version';
+import { getEnvBool, getAttrsFromBaseURL } from './utils';
+import { OpenAIInstrumentationConfig } from './types';
+import {
+ GenAIMessage,
+ GenAIChoiceEventBody,
+ GenAISystemMessageEventBody,
+ GenAIUserMessageEventBody,
+ GenAIAssistantMessageEventBody,
+ GenAIToolMessageEventBody,
+} from './internal-types';
+
+// The JS semconv package doesn't yet emit constants for event names.
+// TODO: otel-js issue for semconv pkg not including event names
+const EVENT_GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message';
+const EVENT_GEN_AI_USER_MESSAGE = 'gen_ai.user.message';
+const EVENT_GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message';
+const EVENT_GEN_AI_TOOL_MESSAGE = 'gen_ai.tool.message';
+const EVENT_GEN_AI_CHOICE = 'gen_ai.choice';
+
+export class OpenAIInstrumentation extends InstrumentationBase {
+ private _genaiClientOperationDuration!: Histogram;
+ private _genaiClientTokenUsage!: Histogram;
+
+ constructor(config: OpenAIInstrumentationConfig = {}) {
+ super(PACKAGE_NAME, PACKAGE_VERSION, config);
+
+ // Possible environment variable overrides for config.
+ const cfg = this.getConfig();
+ const envCC = getEnvBool(
+ 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT',
+ this._diag
+ );
+ if (envCC !== undefined) {
+ cfg.captureMessageContent = envCC;
+ }
+ }
+
+ // Override InstrumentationAbtract.setConfig so we can normalize config.
+ override setConfig(config: OpenAIInstrumentationConfig = {}) {
+ const { captureMessageContent, ...validConfig } = config;
+ (validConfig as OpenAIInstrumentationConfig).captureMessageContent =
+ !!captureMessageContent;
+ super.setConfig(validConfig);
+ }
+
+ protected init() {
+ return [
+ new InstrumentationNodeModuleDefinition(
+ 'openai',
+ ['>=4.19.0 <6'],
+ modExports => {
+ this._wrap(
+ modExports.OpenAI.Chat.Completions.prototype,
+ 'create',
+ this._getPatchedChatCompletionsCreate()
+ );
+ this._wrap(
+ modExports.OpenAI.Embeddings.prototype,
+ 'create',
+ this._getPatchedEmbeddingsCreate()
+ );
+
+ return modExports;
+ },
+ modExports => {
+ this._unwrap(modExports.OpenAI.Chat.Completions.prototype, 'create');
+ this._unwrap(modExports.OpenAI.Embeddings.prototype, 'create');
+ }
+ ),
+ ];
+ }
+
+ // This is a 'protected' method on class `InstrumentationAbstract`.
+ override _updateMetricInstruments() {
+ this._genaiClientOperationDuration = this.meter.createHistogram(
+ METRIC_GEN_AI_CLIENT_OPERATION_DURATION,
+ {
+ description: 'GenAI operation duration',
+ unit: 's',
+ advice: {
+ explicitBucketBoundaries: [
+ 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24,
+ 20.48, 40.96, 81.92,
+ ],
+ },
+ }
+ );
+ this._genaiClientTokenUsage = this.meter.createHistogram(
+ METRIC_GEN_AI_CLIENT_TOKEN_USAGE,
+ {
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ advice: {
+ explicitBucketBoundaries: [
+ 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576,
+ 4194304, 16777216, 67108864,
+ ],
+ },
+ }
+ );
+ }
+
+ _getPatchedChatCompletionsCreate() {
+ const self = this;
+ return (original: ChatCompletions['create']) => {
+ // https://platform.openai.com/docs/api-reference/chat/create
+ return function patchedCreate(
+ this: ChatCompletions,
+ ...args: Parameters
+ ) {
+ if (!self.isEnabled) {
+ return original.apply(this, args);
+ }
+
+ self._diag.debug('OpenAI.Chat.Completions.create args: %O', args);
+ const params = args[0];
+ const config = self.getConfig();
+ const startNow = performance.now();
+
+ let startInfo;
+ try {
+ startInfo = self._startChatCompletionsSpan(
+ params,
+ config,
+ this?._client?.baseURL
+ );
+ } catch (err) {
+ self._diag.error('unexpected error starting span:', err);
+ return original.apply(this, args);
+ }
+ const { span, ctx, commonAttrs } = startInfo;
+
+ const apiPromise = context.with(ctx, () => original.apply(this, args));
+
+ // Streaming.
+ if (isStreamPromise(params, apiPromise)) {
+ // When streaming, `apiPromise` resolves to `Stream`,
+ // an async iterable (i.e. has a `Symbol.asyncIterator` method). We
+ // want to wrap that iteration to gather telemetry. Instead of wrapping
+ // `Symbol.asyncIterator`, which would be nice, we wrap the `iterator`
+ // method because it is used internally by `Stream#tee()`.
+ return apiPromise.then(stream => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ self._wrap(stream as any, 'iterator', origIterator => {
+ return () => {
+ return self._onChatCompletionsStreamIterator(
+ origIterator(),
+ span,
+ startNow,
+ config,
+ commonAttrs,
+ ctx
+ );
+ };
+ });
+ return stream;
+ });
+ }
+
+ // Non-streaming.
+ apiPromise
+ .then(result => {
+ self._onChatCompletionsCreateResult(
+ span,
+ startNow,
+ commonAttrs,
+ result as ChatCompletion,
+ config,
+ ctx
+ );
+ })
+ .catch(
+ self._createAPIPromiseRejectionHandler(startNow, span, commonAttrs)
+ );
+
+ return apiPromise;
+ };
+ };
+ }
+
+ /**
+ * Start a span for this chat-completion API call. This also emits log events
+ * as appropriate for the request params.
+ */
+ _startChatCompletionsSpan(
+ params: ChatCompletionCreateParams,
+ config: OpenAIInstrumentationConfig,
+ baseURL: string | undefined
+ ) {
+ // Attributes common to span, metrics, log events.
+ const commonAttrs: Attributes = {
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: params.model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ };
+ Object.assign(commonAttrs, getAttrsFromBaseURL(baseURL, this._diag));
+
+ // Span attributes.
+ const attrs: Attributes = {
+ ...commonAttrs,
+ };
+ if (params.frequency_penalty != null) {
+ attrs[ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY] = params.frequency_penalty;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((params as any).max_completion_tokens != null) {
+ attrs[ATTR_GEN_AI_REQUEST_MAX_TOKENS] =
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (params as any).max_completion_tokens;
+ } else if (params.max_tokens != null) {
+ // `max_tokens` is deprecated in favour of `max_completion_tokens`.
+ attrs[ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens;
+ }
+ if (params.presence_penalty != null) {
+ attrs[ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY] = params.presence_penalty;
+ }
+ if (params.stop != null) {
+ if (Array.isArray(params.stop)) {
+ attrs[ATTR_GEN_AI_REQUEST_STOP_SEQUENCES] = params.stop;
+ } else {
+ attrs[ATTR_GEN_AI_REQUEST_STOP_SEQUENCES] = [params.stop];
+ }
+ }
+ if (params.temperature != null) {
+ attrs[ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature;
+ }
+ if (params.top_p != null) {
+ attrs[ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p;
+ }
+
+ const span: Span = this.tracer.startSpan(
+ `${attrs[ATTR_GEN_AI_OPERATION_NAME]} ${attrs[ATTR_GEN_AI_REQUEST_MODEL]}`,
+ {
+ kind: SpanKind.CLIENT,
+ attributes: attrs,
+ }
+ );
+ const ctx: Context = trace.setSpan(context.active(), span);
+
+ // Capture prompts as log events.
+ const timestamp = Date.now();
+ params.messages.forEach((msg: ChatCompletionMessageParam) => {
+ switch (msg.role) {
+ case 'system': {
+ const body: GenAISystemMessageEventBody = {};
+ if (config.captureMessageContent) {
+ if (Array.isArray(msg.content)) {
+ body.content = msg.content.map(p => p.text).join('');
+ } else {
+ body.content = msg.content;
+ }
+ }
+ this.logger.emit({
+ timestamp,
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_SYSTEM_MESSAGE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body,
+ });
+ break;
+ }
+ case 'user': {
+ const body: GenAIUserMessageEventBody = {};
+ if (config.captureMessageContent) {
+ if (Array.isArray(msg.content)) {
+ body.content = msg.content
+ .filter(isTextContent)
+ .map(p => p.text)
+ .join('');
+ } else {
+ body.content = msg.content;
+ }
+ }
+ this.logger.emit({
+ timestamp,
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body,
+ });
+ break;
+ }
+ case 'assistant': {
+ const body: GenAIAssistantMessageEventBody = {};
+ if (config.captureMessageContent) {
+ if (msg.content) {
+ if (Array.isArray(msg.content)) {
+ body.content = msg.content
+ .filter(isTextContent)
+ .map(p => p.text)
+ .join('');
+ } else {
+ body.content = msg.content;
+ }
+ }
+ body.tool_calls = msg.tool_calls?.map(tc => {
+ return {
+ id: tc.id,
+ type: tc.type,
+ function: {
+ name: tc.function.name,
+ arguments: tc.function.arguments,
+ },
+ };
+ });
+ } else {
+ body.tool_calls = msg.tool_calls?.map(tc => {
+ return {
+ id: tc.id,
+ type: tc.type,
+ function: { name: tc.function.name },
+ };
+ });
+ }
+ this.logger.emit({
+ timestamp,
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_ASSISTANT_MESSAGE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body,
+ });
+ break;
+ }
+ case 'tool': {
+ const body: GenAIToolMessageEventBody = {
+ id: msg.tool_call_id,
+ };
+ if (config.captureMessageContent) {
+ if (Array.isArray(msg.content)) {
+ body.content = msg.content.map(p => p.text).join('');
+ } else {
+ body.content = msg.content;
+ }
+ }
+ this.logger.emit({
+ timestamp,
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_TOOL_MESSAGE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body,
+ });
+ break;
+ }
+ default:
+ this._diag.debug(
+ `unknown message role in OpenAI.Chat.Completions.create: ${msg.role}`
+ );
+ }
+ });
+
+ return { span, ctx, commonAttrs };
+ }
+
+ /**
+ * This wraps an instance of a `openai/streaming.Stream.iterator()`, an
+ * async iterator. It should yield the chunks unchanged, and gather telemetry
+ * data from those chunks, then end the span.
+ */
+ async *_onChatCompletionsStreamIterator(
+ streamIter: AsyncIterator,
+ span: Span,
+ startNow: number,
+ config: OpenAIInstrumentationConfig,
+ commonAttrs: Attributes,
+ ctx: Context
+ ) {
+ let id;
+ let model;
+ const finishReasons: string[] = [];
+ const choices = [];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for await (const chunk of streamIter as any) {
+ yield chunk;
+
+ // Gather telemetry from this chunk.
+ this._diag.debug(
+ 'OpenAI.Chat.Completions.create stream chunk: %O',
+ chunk
+ );
+ const idx = chunk.choices[0]?.index ?? 0;
+ if (!choices[idx]) {
+ choices[idx] = {} as {
+ content: string;
+ toolCalls: ChatCompletionMessageToolCall[];
+ };
+ }
+ if (config.captureMessageContent) {
+ const contentPart = chunk.choices[0]?.delta?.content;
+ if (contentPart) {
+ if (!choices[idx].content) {
+ choices[idx].content = '';
+ }
+ choices[idx].content += contentPart;
+ }
+ }
+ // Assume delta.tool_calls, if exists, is an array of length 1.
+ const toolCallPart = chunk.choices[0]?.delta?.tool_calls?.[0];
+ if (toolCallPart) {
+ if (!choices[idx].toolCalls) {
+ choices[idx].toolCalls = [];
+ }
+ const toolCalls = choices[idx].toolCalls;
+ if (toolCallPart.id) {
+ // First chunk in a tool call.
+ toolCalls.push({
+ id: toolCallPart.id,
+ type: toolCallPart.type,
+ function: {
+ name: toolCallPart.function?.name,
+ arguments: toolCallPart.function?.arguments ?? '',
+ },
+ });
+ } else if (toolCalls.length > 0) {
+ // A tool call chunk with more of the `function.arguments`.
+ toolCalls[toolCalls.length - 1].function.arguments +=
+ toolCallPart.function?.arguments ?? '';
+ }
+ }
+ if (!id && chunk.id) {
+ id = chunk.id;
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, id);
+ }
+ if (!model && chunk.model) {
+ model = chunk.model;
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, model);
+ }
+ if (!finishReasons[idx]) {
+ const finishReason = chunk.choices[0]?.finish_reason;
+ if (finishReason) {
+ finishReasons[idx] = finishReason;
+ }
+ }
+ if (chunk.usage) {
+ // A final usage chunk if `stream_options.include_usage: true`.
+ span.setAttribute(
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
+ chunk.usage.prompt_tokens
+ );
+ span.setAttribute(
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
+ chunk.usage.completion_tokens
+ );
+ this._genaiClientTokenUsage.record(chunk.usage.prompt_tokens, {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ });
+ this._genaiClientTokenUsage.record(chunk.usage.completion_tokens, {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ });
+ }
+ }
+ span.setAttribute(
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
+ finishReasons.filter(f => f !== undefined)
+ );
+
+ // Capture choices as log events.
+ for (const [idx, choice] of choices.entries()) {
+ if (!choice) {
+ continue;
+ }
+ const message: Partial = {};
+ if (
+ config.captureMessageContent &&
+ choice.content &&
+ choice.content.length > 0
+ ) {
+ message.content = choice.content;
+ }
+ if (choice.toolCalls && choice.toolCalls.length > 0) {
+ message.tool_calls = choice.toolCalls;
+ if (!config.captureMessageContent) {
+ message.tool_calls.forEach(tc => {
+ delete tc.function?.arguments;
+ });
+ }
+ }
+ this.logger.emit({
+ timestamp: Date.now(),
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body: {
+ finish_reason: finishReasons[idx],
+ index: idx,
+ message,
+ } as GenAIChoiceEventBody,
+ });
+ }
+
+ this._genaiClientOperationDuration.record(
+ (performance.now() - startNow) / 1000,
+ {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: model,
+ }
+ );
+
+ span.end();
+ }
+
+ _onChatCompletionsCreateResult(
+ span: Span,
+ startNow: number,
+ commonAttrs: Attributes,
+ result: ChatCompletion,
+ config: OpenAIInstrumentationConfig,
+ ctx: Context
+ ) {
+ this._diag.debug('OpenAI.Chat.Completions.create result: %O', result);
+ try {
+ span.setAttribute(
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
+ result.choices.map(c => c.finish_reason)
+ );
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, result.id);
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, result.model);
+ if (result.usage) {
+ span.setAttribute(
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
+ result.usage.prompt_tokens
+ );
+ span.setAttribute(
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
+ result.usage.completion_tokens
+ );
+ }
+
+ // Capture choices as log events.
+ result.choices.forEach(choice => {
+ const message: Partial = {};
+ if (config.captureMessageContent) {
+ // TODO: telemetry diff with streaming case: content=null, no 'role: assistant', 'tool calls (enableCaptureContent=true)' test case
+ if (choice.message.content) {
+ message.content = choice.message.content;
+ }
+ if (choice.message.tool_calls) {
+ message.tool_calls = choice.message.tool_calls;
+ }
+ } else {
+ if (choice.message.tool_calls) {
+ message.tool_calls = choice.message.tool_calls.map(tc => {
+ return {
+ id: tc.id,
+ type: tc.type,
+ function: { name: tc.function.name },
+ };
+ });
+ }
+ }
+ this.logger.emit({
+ timestamp: Date.now(),
+ context: ctx,
+ severityNumber: SeverityNumber.INFO,
+ attributes: {
+ [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ },
+ body: {
+ finish_reason: choice.finish_reason,
+ index: choice.index,
+ message,
+ } as GenAIChoiceEventBody,
+ });
+ });
+
+ this._genaiClientOperationDuration.record(
+ (performance.now() - startNow) / 1000,
+ {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: result.model,
+ }
+ );
+
+ if (result.usage) {
+ this._genaiClientTokenUsage.record(result.usage.prompt_tokens, {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: result.model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ });
+
+ this._genaiClientTokenUsage.record(result.usage.completion_tokens, {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: result.model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ });
+ }
+ } catch (err) {
+ this._diag.error(
+ 'unexpected error getting telemetry from chat result:',
+ err
+ );
+ }
+ span.end();
+ }
+
+ _createAPIPromiseRejectionHandler(
+ startNow: number,
+ span: Span,
+ commonAttrs: Attributes
+ ) {
+ return (err: Error) => {
+ this._diag.debug('OpenAI APIPromise rejection: %O', err);
+
+ // https://github.com/openai/openai-node/blob/master/src/error.ts
+ // The most reliable low cardinality string for errors seems to be
+ // the class name. See also:
+ // https://platform.openai.com/docs/guides/error-codes
+ const errorType = err?.constructor?.name;
+
+ this._genaiClientOperationDuration.record(
+ (performance.now() - startNow) / 1000,
+ {
+ ...commonAttrs,
+ 'error.type': errorType,
+ }
+ );
+
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: err.message,
+ });
+
+ span.setAttribute('error.type', errorType);
+ span.end();
+ };
+ }
+
+ _getPatchedEmbeddingsCreate() {
+ const self = this;
+ return (original: Embeddings['create']) => {
+ // https://platform.openai.com/docs/api-reference/embeddings/create
+ return function patchedCreate(
+ this: Embeddings,
+ ...args: Parameters
+ ) {
+ if (!self.isEnabled) {
+ return original.apply(this, args);
+ }
+
+ self._diag.debug('OpenAI.Chat.Embeddings.create args: %O', args);
+ const params = args[0];
+ const startNow = performance.now();
+
+ let startInfo;
+ try {
+ startInfo = self._startEmbeddingsSpan(params, this?._client?.baseURL);
+ } catch (err) {
+ self._diag.error('unexpected error starting span:', err);
+ return original.apply(this, args);
+ }
+ const { span, ctx, commonAttrs } = startInfo;
+
+ const apiPromise = context.with(ctx, () => original.apply(this, args));
+
+ apiPromise
+ .then(result => {
+ self._onEmbeddingsCreateResult(span, startNow, commonAttrs, result);
+ })
+ .catch(
+ self._createAPIPromiseRejectionHandler(startNow, span, commonAttrs)
+ );
+
+ return apiPromise;
+ };
+ };
+ }
+
+ /**
+ * Start a span for this chat-completion API call. This also emits log events
+ * as appropriate for the request params.
+ */
+ _startEmbeddingsSpan(
+ params: EmbeddingCreateParams,
+ baseURL: string | undefined
+ ) {
+ // Attributes common to span, metrics, log events.
+ const commonAttrs: Attributes = {
+ [ATTR_GEN_AI_OPERATION_NAME]: 'embeddings',
+ [ATTR_GEN_AI_REQUEST_MODEL]: params.model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ };
+ Object.assign(commonAttrs, getAttrsFromBaseURL(baseURL, this._diag));
+
+ // Span attributes.
+ const attrs: Attributes = {
+ ...commonAttrs,
+ };
+ if (params.encoding_format != null) {
+ attrs[ATTR_GEN_AI_REQUEST_ENCODING_FORMATS] = [params.encoding_format];
+ }
+
+ const span = this.tracer.startSpan(
+ `${attrs[ATTR_GEN_AI_OPERATION_NAME]} ${attrs[ATTR_GEN_AI_REQUEST_MODEL]}`,
+ {
+ kind: SpanKind.CLIENT,
+ attributes: attrs,
+ }
+ );
+ const ctx = trace.setSpan(context.active(), span);
+
+ return { span, ctx, commonAttrs };
+ }
+
+ _onEmbeddingsCreateResult(
+ span: Span,
+ startNow: number,
+ commonAttrs: Attributes,
+ result: CreateEmbeddingResponse
+ ) {
+ this._diag.debug('OpenAI.Embeddings.create result: %O', result);
+ try {
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, result.model);
+
+ this._genaiClientOperationDuration.record(
+ (performance.now() - startNow) / 1000,
+ {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: result.model,
+ }
+ );
+
+ span.setAttribute(
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
+ result.usage.prompt_tokens
+ );
+ this._genaiClientTokenUsage.record(result.usage.prompt_tokens, {
+ ...commonAttrs,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: result.model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ });
+ } catch (err) {
+ this._diag.error(
+ 'unexpected error getting telemetry from embeddings result:',
+ err
+ );
+ }
+ span.end();
+ }
+}
+
+function isTextContent(
+ value: ChatCompletionContentPart | ChatCompletionContentPartRefusal
+): value is ChatCompletionContentPartText {
+ return value.type === 'text';
+}
+
+function isStreamPromise(
+ params: ChatCompletionCreateParams | undefined,
+ value: APIPromise | ChatCompletion>
+): value is APIPromise> {
+ if (params && params.stream) {
+ return true;
+ }
+ return false;
+}
diff --git a/packages/instrumentation-openai/src/internal-types.ts b/packages/instrumentation-openai/src/internal-types.ts
new file mode 100644
index 0000000000..98476f1d20
--- /dev/null
+++ b/packages/instrumentation-openai/src/internal-types.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AnyValue } from '@opentelemetry/api-logs';
+
+export type GenAIFunction = {
+ name: string;
+ arguments?: AnyValue;
+};
+
+export type GenAIToolCall = {
+ id: string;
+ type: string;
+ function: GenAIFunction;
+};
+
+export type GenAIMessage = {
+ role?: string;
+ content?: AnyValue;
+ tool_calls?: GenAIToolCall[];
+};
+
+export type GenAIChoiceEventBody = {
+ finish_reason: string;
+ index: number;
+ message: GenAIMessage;
+};
+
+export type GenAISystemMessageEventBody = {
+ role?: string;
+ content?: AnyValue;
+};
+
+export type GenAIUserMessageEventBody = {
+ role?: string;
+ content?: AnyValue;
+};
+
+export type GenAIAssistantMessageEventBody = {
+ role?: string;
+ content?: AnyValue;
+ tool_calls?: GenAIToolCall[];
+};
+
+export type GenAIToolMessageEventBody = {
+ role?: string;
+ content?: AnyValue;
+ id: string;
+};
diff --git a/packages/instrumentation-openai/src/semconv.ts b/packages/instrumentation-openai/src/semconv.ts
new file mode 100644
index 0000000000..36591a0c74
--- /dev/null
+++ b/packages/instrumentation-openai/src/semconv.ts
@@ -0,0 +1,219 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * This file contains a copy of unstable semantic convention definitions
+ * used by this package.
+ * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
+ */
+
+/**
+ * Identifies the class / type of event.
+ *
+ * @example browser.mouse.click
+ * @example device.app.lifecycle
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ *
+ * @deprecated Replaced by EventName top-level field on the LogRecord.
+ */
+export const ATTR_EVENT_NAME = 'event.name' as const;
+
+/**
+ * The name of the operation being performed.
+ *
+ * @note If one of the predefined values applies, but specific system uses a different name it's **RECOMMENDED** to document it in the semantic conventions for specific GenAI system and use system-specific name in the instrumentation. If a different name is not documented, instrumentation libraries **SHOULD** use applicable predefined value.
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name' as const;
+
+/**
+ * The encoding formats requested in an embeddings operation, if specified.
+ *
+ * @example ["base64"]
+ * @example ["float", "binary"]
+ *
+ * @note In some GenAI systems the encoding formats are called embedding types. Also, some GenAI systems only accept a single format per request.
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_ENCODING_FORMATS =
+ 'gen_ai.request.encoding_formats' as const;
+
+/**
+ * The frequency penalty setting for the GenAI request.
+ *
+ * @example 0.1
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY =
+ 'gen_ai.request.frequency_penalty' as const;
+
+/**
+ * The maximum number of tokens the model generates for a request.
+ *
+ * @example 100
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_MAX_TOKENS =
+ 'gen_ai.request.max_tokens' as const;
+
+/**
+ * The name of the GenAI model a request is being made to.
+ *
+ * @example "gpt-4"
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model' as const;
+
+/**
+ * The presence penalty setting for the GenAI request.
+ *
+ * @example 0.1
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY =
+ 'gen_ai.request.presence_penalty' as const;
+
+/**
+ * List of sequences that the model will use to stop generating further tokens.
+ *
+ * @example ["forest", "lived"]
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_STOP_SEQUENCES =
+ 'gen_ai.request.stop_sequences' as const;
+
+/**
+ * The temperature setting for the GenAI request.
+ *
+ * @example 0.0
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_TEMPERATURE =
+ 'gen_ai.request.temperature' as const;
+
+/**
+ * The top_p sampling setting for the GenAI request.
+ *
+ * @example 1.0
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_REQUEST_TOP_P = 'gen_ai.request.top_p' as const;
+
+/**
+ * Array of reasons the model stopped generating tokens, corresponding to each generation received.
+ *
+ * @example ["stop"]
+ * @example ["stop", "length"]
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_RESPONSE_FINISH_REASONS =
+ 'gen_ai.response.finish_reasons' as const;
+
+/**
+ * The unique identifier for the completion.
+ *
+ * @example chatcmpl-123
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_RESPONSE_ID = 'gen_ai.response.id' as const;
+
+/**
+ * The name of the model that generated the response.
+ *
+ * @example gpt-4-0613
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_RESPONSE_MODEL = 'gen_ai.response.model' as const;
+
+/**
+ * The Generative AI product as identified by the client or server instrumentation.
+ *
+ * @example "openai"
+ *
+ * @note The `gen_ai.system` describes a family of GenAI models with specific model identified
+ * by `gen_ai.request.model` and `gen_ai.response.model` attributes.
+ *
+ * The actual GenAI product may differ from the one identified by the client.
+ * Multiple systems, including Azure OpenAI and Gemini, are accessible by OpenAI client
+ * libraries. In such cases, the `gen_ai.system` is set to `openai` based on the
+ * instrumentation's best knowledge, instead of the actual system. The `server.address`
+ * attribute may help identify the actual system in use for `openai`.
+ *
+ * For custom model, a custom friendly name **SHOULD** be used.
+ * If none of these options apply, the `gen_ai.system` **SHOULD** be set to `_OTHER`.
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_SYSTEM = 'gen_ai.system' as const;
+
+/**
+ * The type of token being counted.
+ *
+ * @example input
+ * @example output
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_TOKEN_TYPE = 'gen_ai.token.type' as const;
+
+/**
+ * The number of tokens used in the GenAI input (prompt).
+ *
+ * @example 100
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_USAGE_INPUT_TOKENS =
+ 'gen_ai.usage.input_tokens' as const;
+
+/**
+ * The number of tokens used in the GenAI response (completion).
+ *
+ * @example 180
+ *
+ * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS =
+ 'gen_ai.usage.output_tokens' as const;
+
+/**
+ * GenAI operation duration
+ *
+ * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const METRIC_GEN_AI_CLIENT_OPERATION_DURATION =
+ 'gen_ai.client.operation.duration' as const;
+
+/**
+ * Measures number of input and output tokens used
+ *
+ * @experimental This metric is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
+ */
+export const METRIC_GEN_AI_CLIENT_TOKEN_USAGE =
+ 'gen_ai.client.token.usage' as const;
diff --git a/packages/instrumentation-openai/src/types.ts b/packages/instrumentation-openai/src/types.ts
new file mode 100644
index 0000000000..7a8a11242e
--- /dev/null
+++ b/packages/instrumentation-openai/src/types.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { InstrumentationConfig } from '@opentelemetry/instrumentation';
+
+export interface OpenAIInstrumentationConfig extends InstrumentationConfig {
+ /**
+ * Set to true to enable capture of content data, such as prompt and
+ * completion content, tool call function arguments, etc. By default, this is
+ * `false` to avoid possible exposure of sensitive data. This can also be set
+ * via the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`
+ * environment variable.
+ */
+ captureMessageContent?: boolean;
+}
diff --git a/packages/instrumentation-openai/src/utils.ts b/packages/instrumentation-openai/src/utils.ts
new file mode 100644
index 0000000000..039ac7cec7
--- /dev/null
+++ b/packages/instrumentation-openai/src/utils.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { diag } from '@opentelemetry/api';
+import type { Attributes, DiagLogger } from '@opentelemetry/api';
+import {
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+} from '@opentelemetry/semantic-conventions';
+
+/**
+ * Read a boolean from an environment variable.
+ *
+ * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#boolean
+ *
+ * @param {string} name
+ * @returns {boolean | undefined}
+ * - Returns `undefined` if the envvar is not set on `process.env` or is
+ * the empty string. This indicates that no explicit value was given,
+ * which may be a useful distinction from an explicit `false` for callers.
+ * - Returns `true` iff the envvar value is the string "true" (case-insensitive).
+ * - Returns `false`, iff the envvar value is the string "false" (case-insensitive).
+ * - Otherwise, it `diag.warn()`s about the invalid value and returns
+ * `undefined` as a (falsey) fallback.
+ * @throws if the envvar value is set and is not a string
+ */
+export function getEnvBool(
+ name: string,
+ diag_: DiagLogger = diag
+): boolean | undefined {
+ const val = process.env[name];
+ if (val === undefined || val === '') {
+ return undefined;
+ } else if (typeof val !== 'string') {
+ throw new Error(
+ `invalid type for environment variable: ${typeof val} (${name}=${val})`
+ );
+ } else {
+ const valLower = val.toLowerCase();
+ if (valLower === 'true') {
+ return true;
+ } else if (valLower === 'false') {
+ return false;
+ } else {
+ diag_.warn(
+ `invalid boolean value for environment variable: ${name}=${val}; ignoring`
+ );
+ return undefined;
+ }
+ }
+}
+
+type PortFromProtocol = { [key: string]: number };
+const SERVER_PORT_FROM_URL_PROTOCOL: PortFromProtocol = {
+ 'https:': 443,
+ 'http:': 80,
+};
+
+/**
+ * Return span/metric attributes from the given OpenAI client baseURL.
+ */
+export function getAttrsFromBaseURL(
+ baseURL: string | undefined,
+ diag_: DiagLogger = diag
+): Attributes | undefined {
+ if (!baseURL) {
+ return;
+ }
+
+ // TODO: would be nice to LRU cache this, but probably not significant perf
+ let u;
+ try {
+ u = new URL(baseURL);
+ } catch (ex) {
+ // Note: We should never get to this point as openai should crash prior to this.
+ // Even if it did, instrumentation will still work except lacking these attributes.
+ diag_.debug(
+ `could not determine server.{address,port} from baseURL: ${ex}`
+ );
+ return;
+ }
+
+ return {
+ [ATTR_SERVER_ADDRESS]: u.hostname,
+ [ATTR_SERVER_PORT]: u.port
+ ? Number(u.port)
+ : SERVER_PORT_FROM_URL_PROTOCOL[u.protocol],
+ };
+}
diff --git a/packages/instrumentation-openai/test/load-instrumentation.ts b/packages/instrumentation-openai/test/load-instrumentation.ts
new file mode 100644
index 0000000000..d7f673b6db
--- /dev/null
+++ b/packages/instrumentation-openai/test/load-instrumentation.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Because all tests in this folder are run in the same process, if instantiating
+ * instrumentation within tests with different configurations such as metrics support,
+ * it can be difficult to ensure the correct instrumentation is applied during the
+ * specific test. We instead instantiate a single instrumentation instance here to
+ * use within all tests.
+ */
+import { registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils';
+import {
+ InMemoryLogRecordExporter,
+ LoggerProvider,
+ SimpleLogRecordProcessor,
+} from '@opentelemetry/sdk-logs';
+import {
+ AggregationTemporality,
+ InMemoryMetricExporter,
+ MeterProvider,
+ PeriodicExportingMetricReader,
+} from '@opentelemetry/sdk-metrics';
+import { OpenAIInstrumentation } from '../src';
+
+export const instrumentation = new OpenAIInstrumentation();
+export const metricExporter = new InMemoryMetricExporter(
+ AggregationTemporality.DELTA
+);
+export const meterProvider = new MeterProvider({
+ readers: [
+ new PeriodicExportingMetricReader({
+ exporter: metricExporter,
+ }),
+ ],
+});
+instrumentation.setMeterProvider(meterProvider);
+
+export const logsExporter = new InMemoryLogRecordExporter();
+export const loggerProvider = new LoggerProvider({
+ processors: [new SimpleLogRecordProcessor(logsExporter)],
+});
+instrumentation.setLoggerProvider(loggerProvider);
+
+registerInstrumentationTesting(instrumentation);
+instrumentation.disable();
+
+export const contentCaptureInstrumentation = new OpenAIInstrumentation({
+ captureMessageContent: true,
+});
+contentCaptureInstrumentation.setMeterProvider(meterProvider);
+contentCaptureInstrumentation.setLoggerProvider(loggerProvider);
+contentCaptureInstrumentation.disable();
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions-with-system-message.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions-with-system-message.json
new file mode 100644
index 0000000000..cf5f947a5e
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions-with-system-message.json
@@ -0,0 +1,49 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are an assistant which just answers every query with tomato"
+ },
+ {
+ "role": "user",
+ "content": "Say something"
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b08000000000000038c92414f1b311085effb2bac39675158821272041554950b11e2d00aad8c3dd9186c8f65cf564428ffbdb213b20ba5522f3ef89b377e6f3c6f951060342c05a88d64e582ad2ffbcbb3edeab5a187fb9fb7dfaf573f1edcb7bb8bedd562d55fddc0242be8e91915bfab4e14b960910df93d56112563ee7a3a3f6fe6cde2fc625680238d36cbbac0f58c6a67bca99b6933aba7f3fa7471506fc8284cb014bf2a2184782b67f6e935bec2524c27ef370e53921dc2f258240444b2f906644a26b1f40c93012af28cbe58bf2727994ec634e2ba4f323bf4bdb52320bd27963961f1f57820bba3134b5d88f4943e49616dbc499b36a24ce4f3ab892940a1bb4a88c792b8ff1002422417b8657ac1f25c33dbb78361ce033c3b30269676a4994fbe68d66a64696c1a0d0c94541bd4837298aeecb5a111a84691fff6f255ef7d6ce3bbff693f00a53030ea3644d4467dcc3b9445cc4bf8afb2e3888b6148187f1b852d1b8cf91b34ae656ff7ab01699b185dbb36bec318a229fb91bfb1da557f000000ffff03004946b69d22030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607310f4e8625e4-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 05:03:14 GMT",
+ "openai-processing-ms": "239",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "241",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999977",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_12b64a999af2cbe909602adc344dba39"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions.json
new file mode 100644
index 0000000000..b2c0b294d8
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-genai-conventions.json
@@ -0,0 +1,45 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffff8c92cb6edb301045f7fa0a62d65660cbafc0bb268b3e16eda628120481c09063795c8aa4c9919122f0bf07a41d4b4e53a01b2e78e60eef1dce4b210490869500b591ac5a6fca9b58cd6ebf7f997efdf9bc5fcc77dffce7edaf3bdadeef17bbbb0a4649e19eb6a8f84d75a55ceb0d32397bc42aa0644c5d27cb79554d96d3f92283d6693449d6782e67ae6cc952598dab59395e9693eb937ae34861849578288410e2259fc9a7d5f80c2b311ebdddb418a36c1056e722212038936e40c64891a56518f55039cb68b3f54f6ca46552e2874269af865501d75d94c9a9ed8c190069ad639992667f8f2772383b32aef1c13dc577525893a5b8a903cae86c7a3db2f390e9a110e23127ef2ec2800faef55cb3fb8df9b9aa3ab6837ede3d9c9e183b9666a0998f3e68566b6449260e06074aaa0dea5ed94f59769adc001483c87f7bf9a8f73136d9e67fdaf74029f48cbaf60135a9cbbc7d59c0b48cff2a3b8f381b8688614f0a6b260ce91b34ae65678e2b02f14f646ceb35d906830f94f7247d6371285e010000ffff0300cc59e0e82a030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "95d66fac3c52ec0c-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Fri, 11 Jul 2025 07:02:37 GMT",
+ "openai-processing-ms": "354",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "365",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_5b9a48ab38990a73e0f90e6d6b7b6efd"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-records-multiple-choices.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-records-multiple-choices.json
new file mode 100644
index 0000000000..8371de1b40
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-adds-records-multiple-choices.json
@@ -0,0 +1,46 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "n": 2
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003dc53c16e133110bdef57583e67ab649b2a21b78613955a0e201042d5cab527bb436d8fb1c788aaeabf236fd2eca62d12672e3ecc9b377ecff3fc580921d1c88d90ba57ac5db0f5366fafd639c17bf7b3bbf65fbe7d7de8c2e7fb6bf7e1266fe772561874f703343fb3ce34b9608191fc1ed611144399ba585d34abe6ddc5f972001c19b085d605ae97543bf45837f36659cf57f5627d60f7841a92dc88ef9510423c0e67d1e90dfc961b319f3d571ca4a43a909b63931032922d15a952c2c4cab39c8da026cfe007e99f28732f2ed92acfa8c5470dca9f4d7b23ec725245afcfd64e00e53db12a7e0795b707e4e9a8cb521722dda51754b9438fa96f23a844be68484c415613f22bb38bffd26c25c4edb0ec7c624986482e70cb740fc3754db31f27c7888de0fa8031b1b263f9fc908fd361ad015668d3e4f9a456ba073332c760a96c9026c07445afb5bc357b6f1b7df72fe347406b080ca60d110cea53bf635b84f2fffed6767ce241b04c107fa1869611625983819dca761f14991e12836b77e83b8821e29096b2c6eaa9fa030000ffff0300ad34d3271d040000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96074804aaf0d758-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 05:18:55 GMT",
+ "openai-processing-ms": "438",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "444",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999981",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_fbc58335d597b112a3387a4e3fb2225e"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-handles-connection-errors-without-crashing.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-handles-connection-errors-without-crashing.json
new file mode 100644
index 0000000000..0637a088a0
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-handles-connection-errors-without-crashing.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-all-the-client-options.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-all-the-client-options.json
new file mode 100644
index 0000000000..adac57b9c0
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-all-the-client-options.json
@@ -0,0 +1,55 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "frequency_penalty": 0,
+ "max_tokens": 100,
+ "presence_penalty": 0,
+ "temperature": 1,
+ "top_p": 1,
+ "stop": "foo",
+ "seed": 100,
+ "response_format": {
+ "type": "text"
+ }
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffff8c924f4fe33010c5eff914d69c1bd486565d7a2c7be0040758045aa1c8b5a789771d8fb1272b10ea7747764b13fe497bf1c1bf79e3f7c6f352080146c34a806a25abcedb72ddaf2f7eaaf3eee2f16cb33e9f56b7cbf6f6f2897edd991b7f0f93a4a0cd1f54fca63a51d4798b6cc8edb10a281953d7d972512dabb3f96c9141471a6d92359ecb39959d71a6aca6d5bc9c2ecbd98f83ba25a330c24afc2e8410e2259fc9a7d3f8042b319dbcdd7418a36c1056c7222120904d3720633491a563980c50916374d9fa35f5dc6270e24aa17427e3aa80db3ecae4d4f5d68e80748e58a6a4d9dfc381ec8e8e2c353ed0267e90c2d63813db3aa08ce4d2eb91c943a6bb4288879cbc7f17067ca0ce73cdf417f37355b56f07c3bc07787a604c2ced48b3987cd1acd6c8d2d8381a1c28a95ad4837298b2ecb5a1112846913f7bf9aaf73eb671cdffb41f8052e81975ed036aa3dee71dca02a665fcaeec38e26c1822867f4661cd0643fa068d5bd9dbfd8a407c8e8c5dbd35aec1e083c97b92beb1d815af000000ffff03002e1e6b642a030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607451c6e66d79b-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 05:16:56 GMT",
+ "openai-processing-ms": "206",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "208",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_5dd31d8f9aa5eb13a6e7903923492d90"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-multiple-choices.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-multiple-choices.json
new file mode 100644
index 0000000000..2c83acc2c0
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-multiple-choices.json
@@ -0,0 +1,46 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "n": 2
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffffc453c14ee33010bde72b2c9f1b948642d9de800b62b5cb6141202114b9f63419d6f1187b8240887f474e4b131656e280c4c507bf79cfefcd8c9f3221241ab91052378a75eb6d7ed41d5d1e5ffdda2f7ec693fb30bf3b5dfe2eee4ecf4f2ecedb1f07729218b4bc05cdafac1d4dadb7c0486e0deb008a21a94ee77be57cb7d89dcd7aa0250336d16acff98cf2161de66551cef2629e4f37e2ba21d410e5425c674208f1d49fc9a733f02017a298bcdeb410a3aa412eb64542c84036dd48152346568ee56400353906d75b3f64ab1ca316671a94db19570558755125a7aeb3760428e788554adafbbbd920cf5b47966a1f6819ffa1ca153a8c4d15404572e9f5c8e4653622bf8b39fd9a987fa8e30682fbf698991037fd80bb3761a40fd47aae98fe42ff5c59aee5e4b05603b8bfc19858d911e760f28158658015da386a9cd44a376006e6b04caa334823603c9cf75e3ed25ec746577f467e00b406cf602a1fc0a07e9b77280b90fedcffcab62dee0dcb08e11e35548c10d2180cac5467d72b22e3636468ab15ba1a820fd8ef491a63f69cbd000000ffff0300a0db7ac411040000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96075bba7f96e361-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 05:32:24 GMT",
+ "openai-processing-ms": "345",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "839",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999981",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_647fdbd760685b6f73c5cc23138cfda1"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-tool-calls.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-tool-calls.json
new file mode 100644
index 0000000000..5ce77418f1
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-records-tool-calls.json
@@ -0,0 +1,172 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffffcc54c1729b3010bdf3159a3d9b0ec68e7138d6ed346ddc349ee4d04e9d6164b1806a215149d8713cfef70e100376d24e8ee5c0887d7a6fdf2e2bed1d4280c710126019b52c2f84fbbe9c798b9bf4367b4aaeb681bddb7e929bbb6f9fe793d1e4cb350c2a865afd42668fac774ce58540cb956c60a6915aac5487c1851f8c7c7f38ad815cc5282a5a5a5877acdc9c4beefa9e3f76bdc01d4e9fd999e20c0d84e4a7430821fbfa5df994313e4248bcc13192a331344508db4d848056a28a0035861b4ba585410732252dcacaba2c85e801562911312a4497b879f6bd75d72c2a4474fbfdd62fe3af57dee2e3accc778f653616851ced7af91ae95d511b4a4ac9da26f5f0361e9e25230424cd6b6e8a36da22b519ea333a2140755ae6286d651df64b108ad14a70092159c20d6ec90fa5d764c6ed6e090738a11f7a5f87c19beabebf5e5fb26015a49b0fbf678ba7cdd49b2b1624f7ff59dd7325e36afdaf82dbf5436f143426a5a1e2e58c502995ad33d443f2e09cb50d844a0bad56e68c0a0997dc6491466aea72fbd3e61c8dd416a03c196828b4ca0b1b59b5c63ae945d0884277e63a703c7906adb25474f1a1371abc2217c56829af07be3d638cb20ce38eda9d355ac65cf500a757fa4b37af6937e57399be45be0318c3c2621c151a63ce4e2beeb669acaea4bf6d6b9b5c1b06837ac3194696a3ae7e478c092d45735180d9198b79947099a22e346f6f0be7e0fc010000ffff0300a64600b830050000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96078988eb44f669-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 06:03:39 GMT",
+ "openai-processing-ms": "1191",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "1218",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999970",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_7975a25b0266245986eb0145433f3813"
+ },
+ "responseIsBinary": false
+ },
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ },
+ {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "id": "call_PXP2udMH0QECumyxuh4lpn3y",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"New York City\"}"
+ }
+ },
+ {
+ "id": "call_TKk9c7b7gvDqCQzv80Loc7fT",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"London\"}"
+ }
+ }
+ ]
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_PXP2udMH0QECumyxuh4lpn3y",
+ "content": "25 degrees and sunny"
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_TKk9c7b7gvDqCQzv80Loc7fT",
+ "content": "15 degrees and raining"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b08000000000000038c524b6fd34010befb578cf66c57b1db10c8b105c18187841008a1cadaec4eec69ec59b33b26842affbd5a278d1d68252e7b98efb1f3cdcc7d02a0c8aa2528536b316dd764d7fdcdecf337f9f969dbbf6defda9adfacbe6efe7c78b7e6cbdd6b9546855bdda19147d585716dd7a090e3036c3c6ac1e89a2fe6c5e2b228f25703d03a8b4d94559d64572e6b89292b66c555365b64f9cba3ba766430a825fc480000ee8737f6c9167fab25ccd2c74a8b21e80ad5f2440250de35b1a274081444b3a874048d63411e5aff52236c514b8d1e88e1236ee1bbf31bb821d9010528e660b1f28801345b083df32e856d4d0d46fe7bc7d6710a24919c9f93bd2626ae2ea65f7b5cf741c7f8dc37cd04d0cc4e741cdf10faf688ec4f311b5775deadc25f52b526a650971e75701c2305719d1ad07d02703b8cb33f9b90eabc6b3b29c56d70f82e2fe6073f356e71448b1747509ce866a29ae7e9137ea545d1d484c94294d1a6463b4ac7ede9de929b00c924f5bfdd3ce57d484e5cfd8ffd0818839da02d3b8f96cc79e291e6311ef973b4d39487865540ff8b0c9642e8e3262cae75df1c4e4f855d106ccb357185bef334dc5fdc64b24f1e000000ffff0300ab11441282030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96078993eaf0f277-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 06:03:40 GMT",
+ "openai-organization": "elastic-observability",
+ "openai-processing-ms": "889",
+ "openai-project": "proj_Pf1eM5R55Z35wBy4rt8PxAGq",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "set-cookie": "_cfuvid=1dVbru2CzTfprgmbY2GyPqzvZehSNU6Q0AUMvx.9FYw-1752732220761-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "933",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999958",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_c5854b92f43699c9e6c43a7327a0193d"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-adds-genai-conventions.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-adds-genai-conventions.json
new file mode 100644
index 0000000000..1aa721a9ec
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-adds-genai-conventions.json
@@ -0,0 +1,45 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffff8c924d6fdb300c86effe1582ce7191b8c9b2e6b68f532fc5b02f0c4361b012637395454da2870d45fefb20258dddad0376d1410f5fea7d293e544a69b27aa7b4e941cc105cfd7a7cf3fdfaedf5c7fb34ace09d8d1d5ddd8495912f9f3e349ff5222bf8ee1b1a79545d181e824321f6476c228260eebada6e9aede5667db92960608b2ecbba20f59aeb813cd5cdb259d7cb6dbd7a7952f74c0693dea9af95524a3d9433fbf4167fea9d5a2e1e6f064c093ad4bb7391523ab2cb371a52a224e0452f2668d80bfa62fd3d8fd2ab57e2c00b19756310fcc5bc36e27e4c90fdfad1b91900ef5920e72d2e6f4fe470f6e5b80b91efd21f52bd274fa96f2342629f3d24e1a00b3d544add96fce393483a441e82b4c2f7589e6b9a633b3d4d7d82eb1313167033cd8bc533cd5a8b02e4d26c7cda80e9d14eca69d6305ae219a86691fff6f25cef636cf2ddffb49f803118046d1b225a324ff34e6511f34afeabec3ce26258278c3fc8602b84317f83c53d8ceeb8283afd4a8243bb27df610c91cab6e46fac0ed56f000000ffff0300e23e590f30030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607d8125a07df60-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 06:57:15 GMT",
+ "openai-processing-ms": "263",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "273",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_3d48791f2e8bf2254beddbe3f3dca9e9"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-multiple-choices.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-multiple-choices.json
new file mode 100644
index 0000000000..83b667fe4e
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-multiple-choices.json
@@ -0,0 +1,46 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "n": 2
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003000000ffffc4534d4fe33010bde757583e37a8cd0229bdf1715b2190405a69572832f63499e2782c7b824088ffbe72529a940f89c34a7bf1c16fdef37b33e3974c088946ae84d48d62dd7a9b9f75174bfbbb9b3f9e878d33eafca1bcded8cdedaf939bb39f9772961874bf01cd6fac034dadb7c0486e807500c5905417e55151fe383e2a4f7aa0250336d16acff921e52d3acc8b797198cfcb7cb1dcb21b420d51aec49f4c08215efa33f974069ee44acc676f372dc4a86a90ab5d911032904d3752c588919563391b414d8ec1f5d66fa8e3469cb2558e518b2b0dca1d4c6b03acbba8925fd7593b019473c42ae5ed5dde6d91d79d2f4bb50f741fdf51e51a1dc6a60aa022b9e4213279994dc81fc22efe6158086e88f9ff526642dcf553eef6b2481fa8f55c313d40ff5c510c7272dcad113cde624cacec84b39c7d2256196085364efa26b5d20d9891396e94ea0cd20498cee6a397cfb487d8e8eaefc88f80d6e0194ce50318d4fb79c7b200e9e37d55b66b716f5846088fa8a1628490c66060ad3a3b6c888ccf91a1add6e86a083e60bf26698cd96bf6170000ffff03003b0c3ca016040000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607f3ff4c2f7376-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 07:16:19 GMT",
+ "openai-processing-ms": "459",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "462",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999981",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_c8606b5a0df9cbb0e34366d284418548"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-tool-calls.json b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-tool-calls.json
new file mode 100644
index 0000000000..3da0ca1443
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-chat-completions-with-content-capture-records-tool-calls.json
@@ -0,0 +1,172 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b0800000000000003cc545d6f9b30147de75758f7394cf926e16ddd47272d5b966dea5a2d1572cc05dc181bd9266d14e5bf4f981648da4d7d1c0fc8dce373eeb9976b1f3c4280c710126019b52c2f847f51be9fe5b32f83c583fda9169ff2ebdd525d14e9cd75b6bafb01bd8aa13677c8ec13eb0d537921d072256b9869a4162bd541301906a3e964d67740ae6214152d2dac3f567ece25f787fde1d8ef07fe60f6c8ce1467682024bf3d420839b877e553c6f80021715a2e92a3313445089b4d848056a28a0035861b4ba5855e0b32252dcacaba2c85e800562911312a449bb87e0e9d75db2c2a4424a7cbb7235df2cb8589fbd9eae3b7d5d53cf9a059275f2dbd2f9ca1a494ac6952076fe2e159324240d2dc7153b4d13d529ba13ea3130254a7658ed256d6e1b006a118ad04d71092357cc57b72a3f496bce376bf86239cd08f9daf63ef5575c7c1f6f273915ce5f3e57eae76a6ff6bf65d8e83cd7f56f742c9b85affabe0667ddb19058d4969a8783e23544a655d063724b7de59db40a8b4d06a63cea89070c94d1669a4c695db9d36efc988b300e5c94043a1555ed8c8aa2dbaa493a01685f6ccb5e078fa085a65a968e383fea8f7825c14a3a5dc0d7c73c6186519c62db53d6bb48cb9ea005ea7f4e76e5ed2aecbe7327d8d7c0b308685c5382a34c69c9d56dc6ed3585d497fdbd634d91906837ac7194696a3ae7e478c092d457d5180d91b8b79947099a22e346f6e0befe8fd010000ffff030074ac2f3a30050000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607f407de8cd760-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 07:16:21 GMT",
+ "openai-processing-ms": "1284",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "1290",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999970",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_0c0558ababb0ad4c20131da9a47292ff"
+ },
+ "responseIsBinary": false
+ },
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ },
+ {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "id": "call_n6OA3ruiGLsd0hQFPQV9fErc",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"New York City\"}"
+ }
+ },
+ {
+ "id": "call_d7kGKpfVm9Oy9ovs0W8Rn47b",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"London\"}"
+ }
+ }
+ ]
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_n6OA3ruiGLsd0hQFPQV9fErc",
+ "content": "25 degrees and sunny"
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_d7kGKpfVm9Oy9ovs0W8Rn47b",
+ "content": "15 degrees and raining"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ]
+ },
+ "status": 200,
+ "response": [
+ "1f8b08000000000000038c52c16ed34010bdfb2b467bb6abc434a4ca11504f55051481a0aaaccdeec41eba9e35bb634254e5dfd13a69ec4091b8ec61debcb7efcdcc5306a0c8aa1528d368316de78a37fdbb2b4f77d79ff9fadbcc6d7e7c587eb9091f6fefcc7a81ef559e187efd1d8d3cb32e8c6f3b87429e0fb009a80593ea7cb92897af5e2faeca0168bd4597687527c5a52f5a622aca597959cc96c5fceac86e3c198c6a05f71900c0d3f0269f6cf1975ac12c7faeb418a3ae51ad4e4d002a78972a4ac74851348bca47d07816e4c1faa706618b5a1a0c40114c1f02b2b81d940bb05807c4089a2dc49e7907c4708b5bf8eac323bc25d9e5b06dc861aadf78b69e7320493af37376d0c4c4f5c5d444c04d1f751a04f7ce4d00cdec45a7410ef11f8ec8fe14d8f9ba0b7e1dffa0aa0d31c5a60aa8a3e7142e8aefd480ee33808761b0fdd9ac54177cdb4925fe1187efe6e5e2a0a7c67d8e68b93c82e245bb096b51e62fe8551645938b93d528a34d8376a48e7bd4bd253f01b249eabfddbca47d484e5cff8ffc0818839da0adba8096cc79e2b12d603af77fb59da63c185611c34f32580961489bb0b8d1bd3b1ca18abb28d8561be21a431768b8c4b4c96c9ffd060000ffff0300eba7002f8c030000"
+ ],
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9607f4147eb2e397-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 07:16:23 GMT",
+ "openai-organization": "elastic-observability",
+ "openai-processing-ms": "721",
+ "openai-project": "proj_Pf1eM5R55Z35wBy4rt8PxAGq",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "set-cookie": "_cfuvid=hegbUOz2ZUjELe2UBdTlLWUoLuE69OC60214ou0kk7g-1752736583082-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "725",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999957",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_31fe1ff570cb59fc99ef7a4ef91280e8"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-embeddings-adds-genai-conventions.json b/packages/instrumentation-openai/test/mock-responses/openai-embeddings-adds-genai-conventions.json
new file mode 100644
index 0000000000..99c6260f1f
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-embeddings-adds-genai-conventions.json
@@ -0,0 +1,49 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/embeddings",
+ "body": {
+ "model": "text-embedding-3-small",
+ "input": [
+ "One fish",
+ "two fish",
+ "red fish",
+ "blue fish"
+ ],
+ "encoding_format": "float"
+ },
+ "status": 200,
+ "response": [
+ "1f8b080000000000000384584d8e5eb711dcfb1403ad6da3abfa5f57c9ca8e844041e22ca245005f3ee037499c5714a059cc82d343f2b1ababaafbf71fdede3efce3d7bf7efef3d70f1fdf3efcedcb3fbf7ef8f1ac7dfae5eb2f1f3ebefde987b7b7b7b7df5fbf1f919ffffeebe74f9fbefcf69757f8eb8f5f7efbf4f95f1f3ebed9ff56fe08faef4ee7e727fbd9cc2c1149c38fcff51e67effeb1fa0ade19cb9ec72a3ddc9f810c7a25e7b9a74f6726425637b9d1f5d800b3d12d8b66636e5d13cf58ef4dcc33b67c473ec99b158b7eeee9a86c963f57b7d11972538b698745c8b796f7844bec6ed4543d57e9554e6a6cb22330cf9bf94e7b14e5adc793fa849cddd87e863ad6229f8188ed966f2550ebfecc2972e939a50f601d9693cf0d72b280d5d8aa99929b021d59786ed0560e97abb20d6d25a9ee6c8ceee9cb80600d03a39cc3c264cb2b7bfa8466c4f35c33af836a2ca9a828d237fb79160ac935011b0d86cc67683563f4a32c5f507f7e7e770226501def56a4569cd3a524d0c14979136cf75ae3b1981669fbffd07b7d7e9ff217f474e314cb3332a266e92178b6b691cbb7852d569e9f4c5499a4f49b40e522b57a3153e8b9a0bf5829a7dca24fb53c491101287203b975514259b86c60ed037f9e149e0d6a4121bb72a9dce78e906a462649c91319111b2daf9fa74665d1c0aa8ab9aa34c3235d40e1d693f97cd5a44d8c62aab6764a768d5a2694fe59693051943d8cb8cf9362bd509a3fd6214449153d3ae54ee65666d4e3c3888e9bd3b78f7e3d5733acd82b94e4ac9e504ec774678fc4061d66b84e73abada8abb2d8970467c69649e8e46e6e295d657b9528455a6629b1b925a0bcd8a54c859898f2d4af3a10b0ab027d284e813eb4b93290ced612f28a2215971d68245a4110ee2924c2eec396522face25ade2042b65cd5bb742d99819037495895d2bff70e84d1cddb263bf7d2f96a26e5a39293332a1f05648afa45bb6fcb379933db56eb3dd8cfacbc93605bba908839bd414d80b5a3fa6236b8be00b3337b71a50a54bee7cc46859c955663bc192b0b7e051b8f31d1d2b49dad360ab9ecb6c9e762c7ba85f00e56597a0338d208f57098c987d97ac556675c80af1dc6b32e3d396d76a9930fe7b216d11b14679139352d4568f6d2d7793ccc4bb5ac1751fa5c40bda84f9cac59d3540bc68ebdd652de62c733343054709f7b4d511dc2d9b2544c789c78a7ab46da920aafdb1dfff4de7790584d6e565f1a0df8c6e361dea9a4ab56f4a467b817931a788a59fe9fe63ea271134b28bbbb59807915736df0a147ef8ea04e22149e469050871c8e16cc6639dbd5cf7a649bb7646c63f840f20bf3db5895795b34578a2ed12055a79356ab996555445c1a7974272ef5aef1855f49c8c951391c7043051963b9485ce21d37f7b9d7a013e2742de9c2d3c61aef55953e0df152cacbd283a142899c50a79ec697fdbf1ae51c04b566d3682214e7b3ca2eaf17050be91f02bd615eead58328ad655fb756633bec8bb73ab7dc5640744a5edb0f725c3ce1d1e3be542e90154a2370cba368df713e2fac714a4b10137d598c8507b4170d9c34abc55b606518616bcdecd0c3c3afab1fdf207d57ad592916709074191962527c339063ad163feb74822a4d0ea2d2afaef35b0d11d3b7a32fdf361eeb6a10d2677baf7ddf09483a8a6fb4b85bde1cfb5e47f49fc63b59d73065668a2970ae08f8554f111efa2e36e15737ce2433e699ade35ac6a05437072d8a20ec9e29c1a8f161cfb6925a045b5538f75c41363d8e3a95bd784c6ed5d53c1f339517d345d3d4f7f871ce548ba5207e7f6b3be89092ee725ec8e269154df4d6d2077b294077017ef9f4c3dfdaf9ecce809765585bbd579e46b574039bb6367517568c8cbcc67793c4ae563cfd34112a2c15d5ab2ed17672e71acb618e3116f76a8c2e9db770c3978acf4c4fb6b8063b9e923ac738667d57e79de58c6b3a11aff1c0e5b4cb9131326db5f2cbbfcf62a1e71fd3d66d6ab12c53cd14dc96da18d95483b957291826b58b2462e7ea81e8b5268aeb4c7f4ca6dff5720aa422c6266adbea9a0eb4e7353330103cfc27eeb310843688cc09b48e58e6e8b60cd3c89dbd061fe61d553a0f3ac3a072edf023e29cf79d5edc7efe37000000ffff8c5dcb8e24c971fc95c5dc87087f847bf8fe8aa00bb5038204445d741020e8df05cbac21b6ccb2a7fb4010a8ddedaaca8a87bbbd7c252e153b5cbbedd995b40ce21ca0b6bc9dada6b753b310dbf7097ad17a6cbfb541d78aed5a8c833f130cee176e417fd380fcc94ef4083e0ed36cd5aaf72fd53e38a3a8139fc80a39797bd7f468275dcd8567ae6323a8b5f6e2379e0580884fce5dc7432eaa156b0822c5513e25c74b97e734410cabceb4d4150b15aafeac1361d6dc41adcee05db0ab9dab8a95ee6b8a37e2c18722443441b2f041b2f6295f9b788bb415c14791b5efdafefeadce9c68632ca41d97323deab3f30ce300969347006103fa9af2b3546d741af4bb581e5bcdb57bc7e18d65358027e87c5c012e4c5bc8dd892f427f36a28d6fe55c56cbb882001bb19bb1b3c8692bed29facce61db74e32cc14952b87af5f43ab66c1f76c9687ec98b3f7505994eb7435c3a96b67fa716921eeb6c47e5d585d2b7b9dda930cb154d968b98647dbb573f89e9e2aba5162c5d9ce5ce9237d96ed89872b8fdb320557f44219c8d7e7aa5dc148c2f57185010cafa16ead66320472a8bd53f9cb732155f46023329a09b0d3678f3312d33d25cb18d5edac91030ee02a3f815e0e3c859190850b85a9854109c297419f1d74e8a05694d2786d549cdcd9476714d790e8d06aa8887d3c0857a12221b21b981953baabc0d731833d1b2040c8ce4673918a6cf8598b410cf0a2823738b009676e6ed564ea1d0fc287a101db3dc93fb783c23c7c73e222aa25f8d0742e7a5ab1ca7233f097d16d23b52aaa8c95c29780dee86504026fb338fc7537f00913de0ea5395d3d51b5dea9edbb4e8a13dce0d85e39ceac170032e99c7766e07f0c1a79cb8e095cbe2cce88dc332bb945abb5379f8fc038473a7730c9471a625bb33b04153e20b85a08b2073a564fbdeb11027fa57d5cc7cf6a3d07dc23a42a8d8e772efd3e08738c7b395c11da9f49ad7d7fd9f22e668901a1049dfae133d1b2ba6dfc8cecae1d6b1427403b4c87763418bee142e90c7f53dfdb2ba97e8d3d9dd322e5409596463737e0b4104c070cad30160e90806e7e0603ef06e84415a352913147ca571bbcbbdcb069670b7ce3b30eea475e57659e042aad1315c60dd044a0b2e6b21ac4d389cf90f21bd4dae9218cf255ed0783bfd91bc8488bcc6aa5058b5f766cd049bc8e7a77ad607c2f3cf8d4597d1c973f2f79ac192eee075c2fb597593e995c92c43a31f410322e60957f85c0b14b379a3740612e52dcc6cea737cff51b1cac2439e2c7515df3011f7b8e512315fbba117821efb8c0317e00d1c628c95a95b598630b070d2167fc3820703e0be7a010f6cfb8a41bdb42b1b0f829f0beb9fa56efc31c44995926f7276e27fdad86bdc54685cfc037b51f3ff4600cdbcb45edb60b50aab2e096f208f63adb9dcf23ef6deface64b48548b4a285404c59441ed2ec1847cefcd076c9c397942d7cbe92d8f300c1deed427caaebbeff384c0e62ba4d39ac1d5c727629c8d42f22b20f5d57af38d168176529ec2e970c694b607aebf9067b3cba4e2ddb189b4ca3ce1d2caf9a407dfa81e15b9770bfad2f5a0eec0ea2ad91c73fd13da9ffb98317a54678e0b77b0b35846697d1a1d2277371667cbca7cecddbb3636b260836199eecda88a592533c80e462ff8908c58297ce2ab40973d63b80e0893879620e3f031f72246a86031b389c38b393bbc3279dd778d335610db1a222c6eebab63446b08bed65812b277b2a2084ac5a2b75f1b8a5a2ad9575e1a112edab1887a0961b523f9a25b07e789b36e24a54bb7462143a5d58b2c5c221504b4299ceca0ba15990d50bc257712b4d7ce7ff6e2c1486ae479d0a2d033cdbded082c92078c13e18096d97b44e95b3eaeda805c116e4bee60af1d7d04213b768e09d9f1246d5ab1d3660b4b195195a1f432aa6f8639b74107c6bf3a642671e4d5390e4902ebe0b2594424ddcbf518ba7cf17567d5e9029980ccba5a48bac65174d117988335ca9c80d79ee175afe4eb0bfadb671d816702fa42816f0f648b02e8d900dae76d7ed0b0ba74edd0b3517faf18fceb291cafe0dbed03a9ad2f40c0720e770b330f9d25ba53b977f18745ebe17b8ea88a7b0fac0154a48cf79008e605df25b3336167098c0c459bb4ac765688e6a94f77920e6027ce4fdad458e1c1e0593aca575e07beaa93a94355be5ddff4c462bcdd2076512860a0cb587ad4c79acd0565bb87a5e22e7bbf0bbb6f012934570c54dacaeda297f6f1e17208abb3fb9caf9c12d6d0c01f5df5a07ce9ccdf58f32d65791a307a71601c784ef867b4c594379a4b51865a820b3b021e8920efc575f656ae1632fe2d5a880cdfa62ffb193e91c2d611c0d9c1241c91d9050a01564334f4dd5bfa0ddf5bfadb8552919102850b5fedd984372b1503e499943816e5ed8c4060d7c77221f3415da890947d3b37651ce17c65819fae2ce19c6b17cb1c1ed5a9e666c62229b07fc20b5fb07d0e2b499f5a4147dbb6b875bf9c3b079417eb6a963f9c88a7bc18b645ef5f64515ad069357f59dc4082cfae899437aa6ac195c276ad4d8b13ab780f0b6da64b90d188ec6120193d442e3e618008b931c9330595b254b9972a915f7d6cfc13a5014b6b17ac50cdc52b6a83e47df8a86859096657a4e3cf8da0a193394cca6df325fcdbda79f671eecfa2ab4459f568283b1b740a01e963b5a9a646e96eccecfbaa1d21d7d444358bd41f769567d4de2ec4feda19252585976da6a8aabdc44cb726aefae19327729d205705c41e4587ca51ee87e33e444601a53a8be5cacb137c9c34c707e499bcfa440cafc24f2064cccc6605599ed329702ff0a0437de9b68851e2f1641d679f8457afd582667508e5e2b0138cf1c39a41d12b351d3459dc913c53aa10112af6163802777ca647fafe21a906d14d8f9fcf28ffdb5757d9547c00216a634f5f4c0901bea38e1ccce8ec1d6400f7c0f04bd10db923ac93914ad8f204f57812cb795ce5b21c78fd0e0bdc0f258de539d729fe6ed47dd99fcebce3fbd7470551cecd5c4d3b0b6cd6f16e4675ed9c590c562c183f9cd41d10979cb061a7d0809555eef0341ca3fe893ee6259b3d71b824731fc0548c23c0d6c25b1efb3856b28d081d1e84ff54953e3082098d56b357c67c01609293c46249ad99ded3214a4a91145f0f617b4e8acac62f39102d39d4399bfffb6a3b2a3e8eb3207e31b117cd69eac550819b1fff44f078ef4ea085ceae963e387a455c000b0a4bd453bbc6b5ebb283734196b664d39c3e2af4ea2a584504f63915c29fc24d703e93445d17229e9e404c0bdc255c8e84bb013be2127c4d6c7eaad6058a46992b3356911a341fa29968b74a21e71d0b56ad036393c988da4591c5282b47dcf6eb2f9c76b12e2eafecd9f3b975109d2b2010310ba0d8d99f54fbf7a6816f8d0e640b83c92b45da778e892a1abdb3db1784b78269be7c1a7b42844a0e07ec0efe03b587da1d885ec75c4ca25da1e6d715effbe03ea70d620cffac567c81ee6bc43cd886c69b8f0d1165dfd265742c62909073f6f5739d2d6c69e56c556eacf67e474b5f05d79ccdfdacc5497d513412371fd2cde8679c9ac32dcfa3ec36703c8859d67639433828aca60411d8036b90c0732b9101725cfef5bb8ce09b0d368f62e2c181d7747d01605728eb6e9cfad821d81c1e5e67d6d80b4f879de42b63e8c54a7c222e8272d6304e01956c8d6c2ff141bdfce551ac60c7a37a17895eab30d2b75c33f06631a66c03379c49811d573dce5f35aaf32b0dd6d3f1a466d7bb62331bbe666101d8d34cd94031c0f227b73025e2e11cc2e5236a31549d7ac0f5bbdfe0ae6ecdd893bae01518e9d14f980801bc215c25e7ceb3381342d62d3aba80e1e2b343f315a5d09b411e2caa56f72bc40c63eb2b5db782ecafbeddd2b82185e233e50207c567c3e248902502af02652801bfda6231ccf564934bb8e9f8cc3ee83954333bfbd8b01f0a8aac0c620de169d85bfcd68f10759ede432119e80fcb043787a848793b340eb42ca2364c610c5d6546336cb26d4fef5669c099620303dccb6d6a6357cd2960aa9e51734ea7311ee085d799b80547205c1128ecc5e28a150d5cd6e4c450f0cff0a1d8ad881e068b887d89f8c1d51a5350c80a5619002ae48e43f00fd20794aeea4e56ecfbaeab02e0aba82b19c5cd855ddff50514d2ea4070ce787eeec594464020c287292c46c965e13a537bf1aeb958cb5d72c41882511677f0ca97bf50fe8934093a30585fa4ef40f7c5cbd6468ed38cb6d452c940674ef27b1900520a3bda106b1f16a54f87a66500888bc36be9515910c0258675fc4f700196f859ce449adb456f49711ad56724042391f5c084d5193faefadf341381051a8fd3a2b098bd8afd347072b0debc91ce439c63009f5b9ce8d0d362bbf595f01608e18f028e2b955a6d9ad983db9f6daf7d055348092c0fe52333ab4a67ef4a23ada869338829194231043149e49bea8c5ef68e3cd27ca7e1faa06b12bec8914b6a1d913e4321bb3807469c6baf3d04d834d8a3850691012fcbbb84e4e33867b7901c0e1387306d9970b089b23e11ab41d2c20117cf779a9fd9a61e65d08aadf28389b164f153740284e1758d7f125f28400220bd88b21f98ffbbb4aeda5f690dcc0095882c5de2185efacc935c56b94f380ba021f37a0f41b9f5df57d3c0b8eb6e904d22e2f39088beddfd2ecf7cd505685d9b13760a153b6f2e5c7d025bb65545f0473028d00edf7370c679883a0f914891237b642064636f5e77b1f4343c73d8788b458fe22818c9cb961ba9c6e7488808f45361de5f80021fbdbf1a00f5fdc3527237240946e6a11e81172d4f6448fe868d7319f57842aa0eff26cd9109463bc62059da1cf308e52c17b7f6ee81bc9fdd1e4f0d10c06d606a68870b9819ac5e07024beeef4dbfd3957502dfb01c5b8eba955981634806e508118dc278cec9bcbcbcb358fd0c78b142fd9d5dcef9938fa68f735234be9276783fadab6e68295346b249a493f928be332efb9e288499a9b9ab62a85c8ef48c6e510a6c06b423aa51f6bd18d99c763184014c90749097a15ed0802abc1f9dae76219e54ac572e154e1fe46fcafb23812f1838584816625a0c4a12b4e4dccd4eefc380c840913f9f866bdc1a3d83c082cb0c44bd1aa347353088f07901d9c10e4d1a3bb15555b9638e144a2b6d0465007c77a484078229ebf518828cf8bd76b6f6f47a737d4c993ea4cbddbf2462c52428eb18ab09ecf26387a4b4b0fde93e62f03bb8a4f97ec03f9c76933cd83460968262b49f635bb6f353dc975aeeae32ec58257b6433e09e51240b5a0975e40e145aaabae224828fa2a6205adbc33bc24fad1e51c91de8aae9afdae453fcaee3fd45ee7900c0136b95d0a6b6b025b571aa9306e1ecbd2b3fe9aa6f851afc9426047a4165274e5df06082f2f698956498ce9c900df133039415df7e05068b2d054703175d8552df588d04f78909228ae7c52996cf89631169b184a6cb8d5c9912d16842834d2b61fc58893eb44eb261fc0ae8d9962cdc717309ed7d6ca120540c29b24f27f7f57e501c48afd907eb5be09dc6c9ca05b94399c93ce7b12be09bfd6dbb256fc7033282f8429b81ceaec11b1ebe7563d7481e168ba140b4d811d46b6327ba8411337a7833381be541ff3aeee695ee1d59dc504503c8e24fd5f06bf3ab0704809c04a0a50fe7244a9ee02b0cfa2ce9bddcaeaa838d177586eab0b5b17ac5c1bd1c71d21a93716a66441680f03311dc5e815e87e3d5d91bf6b22557c591ccd2f1f11070a111fcc4f69d4e08a8d8b00de18e088fca3db7404b062083f421515e56f264a22c78697afb19c939dc7e6c8b784a03095e8aa2e8b1f9841f5a7fb195c8066ad1c90f001ed6a74000c03985beb21f7c5cfb38576e1f51715629b0d5c34abab9ee4ce656062e6461bd2434e05a707b15fb8fa44ffcd017863d3cc991ea0c3abd20c79d4700bed5b317671abf8e47890720fbd4b582e6b270704b81eaa3944ec683113014358c21408457119295850eb38e5d7c72a876fdaee57a6cb9d43b881de46d8b151fa2638e8a2d580c93edf732c6b9c1260e1d2cf0588201791c89f905de3223efbeb7bde7b7bc4c5c48279538849db9241ef3b1b0b119f40a5b9213de69fa971d7f64e0805da49e9843a0af6d5b8c5e77e147e7baf909405f3527f679882cf1f31e0af1fac5cb25c866fb7bb8f92b2958ecddb62f879b10e3058f2347b50dd2082578b0910dad71466ca7bce7a3e4f190b61bd31846b4940fe97e57e2891169f208e13e6aefe11eeded1ad20a0398d28aaa2bc3a9813ab644af0740e1c8f27a48ba849e3865e3c3045daa2304b7ce21efcf97e5a3a3d161595e2a8a2a14be5b5c2028e452fabd2b6c56e67fa021670013dbce43c123c53a1db2c310d70abc0dc121b64fbaac677d5fa0c567d1e2bf02f4f52b4431058af36049d4142a5c06b71168c405be18f71f1d083fcfaef6665416627b53ad33b6cd5af1053710c444b6c605176c4cb1e18e2ace111dfc078fc007f1a4d25c3f05913e7a2e7e9aaaa4bc68dfe2c55119f2ed1673b5dac3c7c109b11e137b4bc7bd2e7f321f8960f373b1161180f796a6028c8287e84738e2ed11327d852a46b2fad4e6d45ef5d09fc7c851d9bd43c3c71109534282f694443e6c0d31dddb96446e213c1e89789f4576dcfc17b28e7481e7590fc0fdaa5acb4ad4dd0951a4f476368b430190042b40de789bf8f9f11df8dc39e6f2acc370408892f09ea5c58816a04536c41bb4100c383c4ebdd2a8fb0f8b9e076ce2b68c2320ec7c1e5cf31c528093b7436e1f5ca18cbea12b8d254e2d883ec5db68b061aa15749d3a92b56f082fe042dbcf15242a953a707e545e5f31bfa2871aa6413f48ba79d4c4a2f87d0f26ba93b87c49488ba1b575253530148703592e77dcf935d777d7a813520a9e55ef06d35fe49b6cf84796f0de50f79b4b1bbd2ba4e449e80625e20d8a882d306ebbf76130e7b255332267284e8a958309026944180db8554228a11350e5dc8d8cb2b4e80a69e5423dc64490d72746753d086c5a9c6cede1c8709645f4a01cf475a697603fa03a5286b5ec49d40cb464b3a00f679b47e5760ef4c5c0b1a2e87c472ea064e476c25df5a757e72fb0d49cfde3bb149d557b0bb193a0f845aa72d24cc2871f6dd93010493a817a1c5edfeab008fc79f8ca3a90ca644a9296cb001c440ad13d9f01edb5c046387653f2f48ef791d98007461f8d3dcbab98a68cb55d3207ee12348b2508f9498c4e2ccca0f4de3a0b025121253ff605fb6b5d368b8d9bcf1dc2b356554766bdca221b318958581fee52e0e714d94442a421d07ecd59ef9acc67e3c6bd8ad0436a6827eb6a3ef2ebe73a87c383acdf3b89d70533b635f6813d2e376202d423f617ccdfd08d8cd0ce76b0e5053fd5d0f75b0f617d96b0c1d756e02214560a112fc8e9f2fd7156cdbd659789cd340d19afa2c62c74b54c8e24cc71545728c5fc58d7ddeb2de1aa151d388c1be72c5d87e46dbf6b104cc6531590a0313f939dad35a70219c8a2da5a53d2973e0e5c7c9438a2de933453445bc9f02c83f150761dc402d3fd6097f5947c0147da0b8ba67c1f2457f24df7d4e32c0c51341d19840652864c1534fdbc1965ead1cb0816a936c1801c5a7444ccf5bfe24e0a6038c72af51ee67c9593fbfe280d7e1cd9774f47c51aa71f96e30eef1f66aff7acbffba6081386e2d12dbb2ed9d1a9118df85d2db458c12a541809524e9a4ac450d5087560db8a93271f57b28337e8cd28fd4370a56d5055ecc6c72065f5cd6dc03e2c7a8a05dfb8545c755cbeaa3a30ef9a717cb68cffda9621322f38bfd9cd8d01c74b52f57dc5b1a51c7f03ae24735243b07a982e9cc3165e6bb3e0c44598728fe46b434fb67184f3150435c3f4170411860b6831d8e9383418307ea2022c36b6a7ce893df8686c676fb7bdbe12c804c9cf5e3c79db20342916f83ccd32bc9cb9bbe5e8c6cf22c832b4dcd9c93823c2ef058982eb4d86a1083f7deb93e1bfe768640c83e189964fa36e91610d9508e305b6b7103fe803cc3569eb292a0b4fc058b0eb209ec4d60aacf8dd09f4fd431806e8d6c80c61f86034106f4058aafead2f9f214b2ca8fd7cbd95ab1d30115ba91366704896f8f98f2db144602a69690d012c886b7f41f71fefff57306f6c19e20c047c153b630a9604893e7d0efcc507585274e56c5570cad88f5fc0083a99e9063c0c7b5784d73a2e60ef425eaf1c3b07963cd1fa1fb0b9acf1a6988357e58bb117ec946dccb11555c8b3f08ca3f05e641df2e719c6b82c2432c514f7d4b02dee4a6ad0f4a68ba3e13217091f4bd5d41891bb4631648bb1e2a3b630d151c28b30c7898b7276b7dc1f006d2c27c33eb6fccbd7202c8145792b301be8fd55243de123bc5efbf7ebffffeffe57fef7f5e2b7fffaeb3f7efcc77f7ffbfdb76f3ffef3af3ffef8e3effffcdbb79f7fe5dbdffff9c78ffff9f6fb6fff7a087ffa977effeddfa89e354439c8382ba4354a7e4dc2e92e9683c15693e91ffd605a5c18a75052ea438b2591d498a258ba4b4fc8b0984b21467700e4ac32547b4f77aa6f38f615cb23f5300e468192a18b8734928bdfab2a967bacf6986093b8301862bf2ed82d893970ea88032ef27df4d7cbbdb324e11fe329472edc56c10d8623092678a5e56c4576b3065f8ba709e05c657dd05c7880e4fc61242f7d4fef5dbe7464e0cea0bad307159e44de5f3364b6c6765cea37b9f4bdc5d486ff5e343b18a756c3618bd72c46f1c8c611ef2f02da2c58a606d87e132a8ab04931812215be374fd039944275b79a4ba62801e43bf690618f9a8561d27406dbd1fc6e191a86d9798b43a1bc5df4566197aa9e09ba45735a7efa4f9373e231943d43402bbf742a87018f8c1496142a68898c055b115c159d1baee0f2033982ce972c2413ace93de96a3a8bc94e3549f608ad7fcd341ace9046cf88c17d82285f28236d0968b65c5b99558cd3dace786f112fac04235aa939c05c06731e7680795b6c7ae7b7b9074256b78a303048c55aa60ea2dca6c22776bbcceeb96411552226859045a211418d09128821ef4b47026d8c04609d53cd1253321202de59cbef3f733a304f25d5f47446acf72777b5d44380d734cee8d2a2f0b3458b2672afd96c00f12b2c56a2ab3bcfb854e031e298439ebd18ece38a85139d0072f8245c1d4661197f80f22c18260f6c171d0bb506beaf9054141cab0400452702868e18b043c65540ac572c45cc53579e22510d07d3e359da12b93a35773480e2097f7385f88a28fa52f7f193697fbfc2ee0179ba346d20ac138ce474710101b8731f99e68bef3a2c86da81b64234809879b7647821d4acd2c22466508436b8b5528c830bda4819893d6b24f2ee0af47e804c0b52468d877798ceb75847116122d373aed965a5f4b9506ef871b6efa5891d00e0391ae520a56024c764f19a4580ec2a8d7d76973939776bf4fe1091122ef965b837335adb38707132ed32a194db5c77f4c4917819e8badf4fffe7e77d23ccad4887c1012ab32d31af53ba7ce82a343b42aa879f9acd2521d5de65aa118564b1723d692b9aabec7373150a64d6c4c3e437f3226c6957f969692f81e6aae12a1bf2292e01e043665ac1e0ace413f100e5e7dedfaddd74800f5446a15df38cf1a31dac0f198a833e56466c7543d04a57f88ccb20524cfc66374b8df556a920e835cd4c764c136bd55ba670e78893e816153d56bc18bb1e5a2a74e190f3d2af7aaac4aded760dd1e0b3a1d382551dd7d86619dc80791d2c9631bbec726c77461e47f05ca9757cb1527166467e27a97fee394718c1452c624357be85ce294c78964fbfa3e2c1f813c5a3803df0a9d8a88a78f5c32a452486279b13a0a9791fed7ae70365b534f6066370083c8e3cb6510dd29e144a0cbe5cf11bc2d1eafc5e1b4c9930ef8d69abdc595c0315474bd34055618cff19406366ca8081b6f8dc3c78ba0384b921b3c6217a7e922cf76c1d487410d627397925d88ca3e29790b0b9a2de446e8bdd424d1ca2f73041fdfd82db133a56a19039afe314710472eab7834e23f508467249b2030aaa94814553226abadcb52e939fd243d35cbd43b32df7450d1827dae5f696146938cc5a82d770fe4f7c525dfc8c863847e7f8a174f25de2aec5e8ef923e1ebc8be8031174b704e5441fb874e86e5d01b092645903a690f7373e849e25e8ba4eb3dfb00556cecbd6b1bfd23563d8bb82f0815c1a35b1da6963fc17c6568ea5d9394b91f56bcec3b0301a25a95ec4c8a591594811993b5dc612e024e15bcfd556684043a46bad7818cd0aae19d1c2bc15c0f80b3730f8b6c5c91159c3e1edb8cbfe1c93fbff000000ffff8c9dcb8e25c971447f65d0fb0122dce339bf226843ce40a000511b2d0408fa77e144668bba6651aae18640b1d95d752b33c21f66c7fe11e11a53133f802e99382d27ba62f30f6e9822a6689d58fb3dc213e6a1630b662af0d82abf47e2cf25a7a30b06d775bbdc78b4b4b2be1049661bff53622c03d32162eecda085f2ddbefdf4f06cfa6b938f0c7937ddfe01e531d9e19ae867a4f4090e591b7c26f00eb9bacf09afd2f4202d43f32a981d1a7d77323d528547b69843cf00d22ed67014e83a12155ba237df4822c41bb6516c1c424b4b22388876c5304f75fb0cc300150ad78c3e2d819e38ce6eab0ef2918139ba33b24fbb1059b6443709755b4387b51509f3251b6b00c6a9b6bec0115fcd3891d3e2591a078cba985adf8ca6f534c4915f2d30f6a0d2cc925fb7064140c6a1c536ab61f278296780599585350d80135a6960ead49f0aa46babb6697d0a538b07acfb935ffb54755cf776ecb0e2b38412fc20269d4cf0163e99c469a25eb622c48bf7df1fa1708c88d20d32f6c553044fb4864eb733c67298d7e8ebd32efa2cb6ca3a2a701d858fb5b6adb6d92c99b106be95c53eb0fe6513afb7cfb682b10697ba2a1988ea5b32351ee79153fa5a0dacdf2637db2d354094d8e2a6cf201f602a1c68ac7209128e6eb3856326b1e8ce327396794176e6346210ba9d50511bd8f3bd2d61677669772743c16ad69b350865304728d37d5d0fa0159f1a150f35df426e06bfe661120c72770c44d58b2ab7669d6b0f4582f44f37d099f3e2b4b3feaf1775f5c16addcd820ff24c9a659438264b64fdd5e9bf7472e7f930e4a125a64dff2044a9e18212d4e47a7661852b338d9134238629ac39b90cec553a09b51e050b1bc80699b41f59cd0f7cf2417bd17e9d8c2bdda2ad8127da08aa87c9ed6973fc6614c042be98b1b27b0729adcfdfe0f5d7cd44a374b6379597aa1b76a1ef6d8d3196eaa97be15e51589b02b6b71e371c05b4111da4ec9ada98778cbc0a0ce319d8d667ed1e4bb93cf5e8bbe4f461e46a9e06db36bf9ec6ce7a54db1c66ef36c83d6a3c6ba22fed361f938d015913683eef409761d64b0802cbc3b0765f7d28c8f0c0110c75bac31a5ddc1faed923d97169bef1e1ecf490d7d06763afef2e9882a93585bf3753ab33e6fece356a735775d2b16ddbba00a36854fd18be01cb5c6be4024dc7a7adbe6d5a85c9bfbb0a8ebb52ede5c1c4d6f3634fd0eb76261a9a1593974cce4c13aa6e5612c6231c47b8a3c542b350b71acc928b93cb3e3bddeb12eddc2df88d6d937a8af46291784087bb67daa1b23194169d72b7531ec7758961f800408d364cb349ccb390c04c6ef4486ac4b4a02adc3132232326cc03a95633f8e2710d58075cc99935eed8c8b51df35d07dbb661e0e6f89c3abde895a95b8e1369d22ca9e5542320c8875ab9cb54ac27350ab665b9d22b403015daecb635b88af8b85aac81476762e3774aec61d3299b833c0fc13a0806b384b0eed2b436323a969dbdf75dc56cb5f7626b89daec3ecbe34897025b6406af2e8fc35387334464eb2bcb78705988789d2d9731ad995e6ed5052565aa82751f15f9763ffe5140784bd4893776eb5d59e97b5f5414b6ec3f16cc4c53bed3bb2c7355d475b2f486ebc9b7a12d33e8796d61cf9dd50d249fd09ead119ddc791ad7c0cddc8b16a3942bced66ca1a171cc9c0db77084288ae2e71a8968ae9b61c16277eef57aaec7f76b32cfc930418e1fcc824b4dbba6e47bead0ba86697519c818bd2b5126db7ee47a5b1cb485a7825dcbb38a1cece26cb9a9366ead1054e3ba8663c47938ccb303d1b84eeb1f109e7a400812ba003461ddf81cdd68c5ac45f5d7939bff415fe04d40b02db5f1564c35f1a340535ef562d96e888eb9f38cc1fc071edb414ff88a3d926f4dc6487a93a082756513b4b06286225d5cdc779807e249cb1876500cf202cc7d3b4ab81da541565115594c102e91df6ce71f813369f2f63c8d4e7d686343f6cbbd5637622f3312d2327a2ef27deb7822e9d206cd197da58bac37fe04a31c77e0f8d5301a107dba63a44e8299fe0d04796b33062d7dd5e10c8c09e25747dbab4f2dbde1fdcde1c2f8367bb3d9342c17058410a9328b328c782b96a32b31349aeb3882cf4b652113188c659ff344dbd3c03b3187daea937a474fb29d33422b9e8a5644b75c8722e507ecdcc5b4de3c8ccb368a7c2e36e66a00088c607d5249cc70cc0cc608043929175c3945a763ce1ede91d04423c65f65a990982320c3153fac936c8c82d573e828074b7b95857baf38a435dd0f7d877bc9f66acba246c7dac5e0373a817b540ca0945548d238157dac81c1a3998c734c2b69d03ba5897e6050e9c88d97a078f233abc766cec38d92da0b6c81edbd68ac56bbbd5dc0e88dd3435f1fd24313bcae7ec8c14ad3dcaf88de5bca8a8fecab54716cd6598ac9eb7b4749ec21d7dd52cd1071ad6197026c30338c15e092db123d6b6c12312c4a327ad80969abaf2f37f8ece9bacf71836fcb68132427b790ce8b97d00d16d7b2364e58ab6fbe76cd69f98ab7294480b0ddbe428647a0c6de36625bfe37e978759a6e9bd62596e53cccd694e14082b96619c73ee3509b84ea8173b507e19c30c9f2110ae870cc7643ef3e74d42e353539efdb7d0feb924b97482123ffffeffdebc5f5ea27cf5cd930178de9e451b76ccb3a501bd8d81b97b0913e08687083c8ed0cbfdaa660222f339876e80ebaef618a36eab64b64f55c72327770f63687050d341db1947db06b7732191385debf7179fc7addc3bd9828e48fa67d1a80f0f4155aa70f52f16babb679e119483986f3b18d6a4268cf3af7523031f9c3ca99445804f8c0b61f98d7fc86c78cda55921a784514ea7511430561629a39c5c0d184e1b707dedab7170a8df04df36d673530dc21d9a76e6da3e0bc70f3f740ff2b374e8f625cb8862e4693ef12d3b22ef37902a7132a0be026356880b79a61e2accd2a7068a05fabe6d7bdb69331901699e06b6f1c99f2a372052d537f8fa8ddc97aa0e85d9b34f001d8dab522ca088351d2abaa4e7b646c75a5f506824e8c1723ce82dc3af5d10c1b8c283a3fdda73f63d3d933cb25c28aa22b62b2af699256769db1e5ccf749db7b4057b77bc15d8a7d49a5c0cd61844aea29a5c7acb5977ada79868cfc1963b511e1d4fbe57e943e3f6d71f7d69df447ea319b28f0557be02a76bf6e31e457bba2bdc75fce4f886b820eafbf171a6f39593b924cddf2d152595c1406ba6961e370fd7a77c3266d74bb640546a466256a5ed5ab7a431da62975ed246b3ac4702d1bec340e482be9cb09abb6f29f659b654806f68066a0345e437d0ad8623717c6ce361884c9b75b27c9ce527e9081a1eafe49ab9761f30552c8cdcb78feb8e593328bb0de7a42c957683cb45879e7af870e02f265b059660badda22611c93aaf61a4c125da2f6781e4d9e8550d0b27319552fcb71aa277bce763fe25b7f3a10bce456420eea77bd12e8e38de27ea6a1ba1dbec8827d45f4b6bbb8f975cdd6179dad894918a22b1a6a075208a7a8a33ef4e4af8cb97c7ebcd2312d68f456b7dcd2d98a11410f7bd34031f349c29444dd84c85fbfe92c7e2602af656e86ebd000a4c44cdd0b1246be0d60133801ad66a735cca2ce45620a72ba46bd3593b4308c5109e8d547870bb0560de3c13d66ceb8acbdb7a25702630b9d6ff0b316bd679767d1232e310051dbb8df54d95d1425117b9374e9e9f0837478a3838db1c3d87b9517c341d435665ad0e160bcb4ccbe8577c6464938ed963657c944b4ea9a61a103a8df6d901fba23d7ac0c1e0bbb434d6b3189e433332b5b736518ac7f2a4f5f23c3c2cb60c5fa4dc8001077bb4d864950533da67df1d5bc2f96b5ead5fa943c3c729e82e45dfece15a63b2d80fea32a731bfcb0aa0677d44fbce6979bc56ba7f4bea77695af6de0fa80e5a7a9060d09fdd24f9a6ca516e6d262e8e94e35cc215a93930eb88ae223fa27b2f46dac57d7dbf1e47f969a7fe6a9683d87fbd94d8bf99407dc38060d0438a484fcc0bcd71dfdb49d535c8f475b3b338259bb9dd6be437c24e8e817ed5ca6ecb1cf2b40f3d869b7fb9af6711105d59ac5d5b42270d1eb1df0eb9542f2881d96293bec6bef3d0a1bc5fa2d54dd55b92858848683e042bb8259bb65d7f99bf1b5c1a9aec11cd40eed2f8cf6d8f22c65c555148f7a2c5b845db97b441657f032f7747816b7965d85e82a434f0da8f13668e7ad719f511d2b7522c9dcc23aceebfcbf4c129235f603b524aa135d76a8c4e88d8540f56f3ec8b30b57169d19e99eaebbf39aca6dc0e84a99023744092a896aaf228b85b0dfc06665971e7c35999458e0103fc3d2eaf7ec36ac260090675287c1816c5c4facd0db5c97149a533f18d60d5b972075696cd6f588aaf8bfba8f340f2f4e3f6be6fc0a729facdbd3151955274f2517b5af312d31d34f799793bfb69815936d9d2d2aea8a69853e3afa19ae8e9ed5ea74089c6bca6f70b029516d025f4b59e0344e5dab5c1aab07534cd77a62c61df01526d13a981e8229a40b06f4a0ea7e58045a8f9471e06ba60b5296d6a9074f5aa116794472ebe73fdab4ef3482665de3b2f840ad0567f7a2092af71d1c7b2e1ff20ef4579adb762cbbad58a6b9c9429f823897b5c554c9dbb2558e0132edcf6e7c2cdbb133469843805114a0dad7dedd3aa7cd7165155512cfbaada227ed5d7b5d7ed7a69923e2a1d8c55f92617f973102c0e46ebd2e3aa55a0dff344af5037b8e58da156311d7b3d58debeff5d286b961ea095a374c9bb2039eeabfec0b2d579c67ef53b8b627218deca9b76b3fd654f993409a7c2793e1981258ef3eae78f2a4b2197e159391adfc0e6a5d7b1ade2d8f7822e643771028c92f58489c1bcdb69328a76c0700095a9bd8e4ffdd3493f12a723f3159a3685dcd1ac4a66ed7d9e7c4fdb2d54a8e97dadcf7f43025bf81c23ccab3384a723d9d79b59dae0c1967359388c60c5f879e10ca95ba734322efb11cd408463de62e4adb1a2e14111ae88418d26c7aacf84bb501704f0f20e63aeb76c61e6daf4b1629ecbbee2dd11bfae301eddd47c0891aad793329789067fc8a606ae884a1ef70d054cecf5ffaaf5f0e452b37a7e9985ce6ffc4f9a9eaeb91f9e7ae312cac020da1cdf94a6390beaa2377e7aa66da28274e650ff37bdfa4004c3163cb0811787f9faa8559cc6b1dc8c6d5a6764450cd565a2617b48b734f68873d3af8e2f46e4289bd5522895b3f8d930c25a5370fcab9b19288ede228fe334c8d8eb75e9b65a45bdb1d78383a4d4077957824731c3dad165452fd085454f718fe5199ed6fa880af9427225c4883d2d552831793518b6ae07d32316975996c04e2004d39601b61248772821dd57dcc8fb4abb139f1b4e556c433c3415b8f0fca2e6b88f56e7c7ba1e58a5320e6d4e4ba278ad58c82c4179b0167d4fef53a33d94dda054fa3aedb4da3aebd7e0f0ceb5a4b2089b3a13d93b4ad53d3b516f221fd1deee659a44ea47be3c9aa05da80ec54a9dd09b037f2c6b579aec933643235ae15d77855766a867a2186a45aaa5fe95489d9befb2bae3ad89f62eb9de69c696d54051cc386afb3fa778024460d830bc05133483e178bb5eaf8713c0e7b974f41f4d35380d9b2157396088f1fafaa884962ca7504c32fab1a468839946a4f2e36d07b8deed48a9f39870027e520a0c330028d134e9f84803934e4af74389e4e12ad3d7b5b36f01209dc1ba21c29153dbfe8ed1192761bbe2c5418f41a56cc96c172d7c8078a61cd0badb62b5661c4eacb595b7c1abf1fbac020ee43e9022d2e51b297ed41b07d4a173e1cd4a3ed4e8ae6aae036f82c5a6f30c4d772bb7a4cb7ee0fc605b6830d64996940f14d07a69070421adaa557f4f6e5f8bb87f9072b63725b0b5440f1250d92303e41033f1b98451e9c89bcfd5d604aee04ca8ab84bbd2ff394de96b78cce5f0f97380a639bba64cd6200ca98f5730f8f8df633bff21d99ed6e1e0e63f0bc26d459975348110549f53200b2c4fcce04fbdafcb80f75fdc05ed93cbb904541cd4b4d4755ab409438cf809a152bc34cd598dcfc79377049026537af25b12a45a5dcfc6733e059467af1117339f841fd591b2a9b6e2b7fc078dbf8568cf4cb9af61ee83cf7913b655485a7b543f6f1e6092d9c41d96aa0a352880ec7acea4eafb09abb9a22a2eda2e6c772683f8aaf6112a3998c1014adadbe41b8c1ebd6fc8e8bf41aafb28cae0520ab8b7aa1dbb387b4c8077442ee49d581ea53709fd8f5a117c7accde43f03c1988cbd02d1ee34205ab2083431a3913ddecb6facf6a768fa154cd8301ed98df9140de2832f5e999e0f7d6b8083f4aafd094e3933e1d54a01a1d8295af2610bdd9f44571b575f2b595abceccd78562386c1fdd8e9ea1a8a49d3b9f10c42a7c4e7af27be486871a1e97b9e9ceb36a8e92376d50f3379d39df2947b58217050579e9d97bb378347759a6df5b05f61dac8585b2c5f37e3bef240cf514bd539c2802d3c4c5ec151673fc3497256843b7b2e7b1abe709b3a4cefc00cd3888cec4fcd057b67b03828f2b5b3548b4abdb2796ab2ce916731c7689a9264edfa3b8621a1cc2680d502d94d16fae539c5d6665221d910658745719fd6a7185e806ce4b43d67e56eecbe192734609b338ea3dda48a35a99ecd76b90080e4b7e0866b1bfdd4ffac405381daacabda7632676435a58da5143da34512b92fd3eb6ca269712dc16b3accbd56fdc609f8fc9da4bf3a346193126a4e1edcfe17dbe405a7867e711909e6eebf250accf09e59eb71cbaa0b866f4c16999d5cbcece6af821063b31fec8c3a4cc67f5fedbbbab9672986fa747c4f606fb1322918947719f5932ae490892ff898638edc3aaa3a7a7fed3636b1244b15b6307a8a2f87e4ab8ff6b9872e51c8296da1f1717b0d5a6f3d3c31382901310efdaaea8292bbd8fa0520929a0b7f55c7612524a5d9f4ad142fc631219bc8aae5ddb616ecadd2026caf74519d99d5967ea73b6247c6f65f0eac6f70bb727c14bbd81b0ec1c936cf6c2fec456451617a00faf1d02002522feb85fd41276a653537c3fe53fefcdb14175d3e4a2efd5c28224cc96612fef7a5ddcccdf53304ba29c6ec2f4235062600d56adccc5f008d4b31099306013d4de774c681c9299fe66e145bd05498e9cb510f972889c93f1fd6767374dba34d5b963aacab89657e5bf002dcd8d4b91e7b23cbd87b29a0ea94a3b5b3256f450daece506a030fbd9c31ba617f06acadba6c7f8e2bd2a0c0ca33f9f56788bbd561d75997622b9f577e9ba49654f73e0d13c518bd999b8887ad28183b4f7169315f4ac87b9bf9d00ce1e3682b5be3a478047671e567cd90d15163f453b40f2752cd206a8d05b31656277a43577fb7e3392631d8c55a2dc6daa987d34d0a49f9c6a76dbf69d85b7ab829c9f10d838264a07be87581130436662d3240bec6ba64b4237cb5752085a14d7b592c68ab988b0e41ff31520a7bd7e265a3c0d16e2ad92198fa200961aef6bd567b0308e56cd33654d77175d90c05f5c93e8c4d07b5a2c0d1393ec7b859f2921823a511bfdbf56f5bcfa75aad9cd95dd178928bfc36bf47d7a13f18d1d0adcb3b8f4ab75d4a1a1617fb4f7cbb81b3ba59ed4108eb30cfcf28ade9cedfcb84674bb7b667459c14424f4400a333f4e68b8676569d99fbd096e443a4a6b365f9a8d5be5838f5ba1a01067db6457113e254ddb3cdb2bc7ee37d7ba52665b616f99d90e967b646b5aefcc45e4e7d66eee4af02c1c4dd10d7745758ee56014275d2e3ccdcd44f0d1d9fe17c2fddbd8ca1da51f20a15e00ce4acba0ff8c22df36c92ab28fc8b848357fe4c50fdb27cd3aca3dbac97302b2d946ed101b55001eb422957584269cc38958aaeebc968a836d6e9c508aeb77cd164bb617d056796e5c9b1e4ea614eb2d90d6449996b4e3a2c8adb381517a328cee45df44161c56d04dbd8042b5a85740b4dfbc2c972fbf419eab837a4f10859a19b5ce846c1bfe77c7818d5bb11c7666f0efd292b882ba10095f79e9e8fdc268c97e9a2fcd0b544f0078d0f06e13447d7df014fe5506506fac43a554c796251a693da2474ea75518c5df513e86867a785fc8e4f22c46b451fa007be7f8530e20f6b62d15dc239d042f3d1a419325f92d3de8d6cfbcc897d9a3826a6b69cb86ddbc12045b39025827156750f04585b879c8f61c4168c243b2dfbd9c323de41cf2e8ed75b4c2994116d9de8d3cc4753a641a76037c4f866f06dd3fb7b5ab1e12e9ed274d5305b1953761b8720a05d7672308b5557585692198d6d7c9b87d4029bd81aae01acdedcb1b6027ea6b9182ca60b2c08dc521fe9eebba7a68d0431d21a50b30e4d490f1e72013467c187985f1268eb1975155387f382d8db7c8d188dd1608dfbeeec3614bf6edfc0a664d3254ee0b37165f0b9ec3d5af6a23c0f6e80baadc761022477d895a5c2a73052510377655b9e1d99023b1194ecfe5d2ad6b388dfa1788d9f41f6efd7fef9fcf77f3f7fe4bfde2ffef8f7bffceb1f7ffd8f1fbffdf2e38f7ffbcb1fbffffeb7bfffcb8f9f7fcb8fbffdfdf73ffef3c76fbffcef77f07ffed06fbffc932c2029b75cb2079db0a4a7fc440d5df94e7859eaa80ea81fee0c8a8c3d9aa73cafb995efde56df5a48508837632fa32dc4af213330b61d86e63a0e641dbde7ccda8bfdb013d854d7edd4813b281026c858918f05cda1f632341861eb8b35728daa34fdf909ff79872ac35092ac16bb82e11b3e0da39e0cb2146d52d450dfc86ddd6a37d145dd71725744f0007f4981918b487b7594b02196df324e33f380e62304117ef4a05ed2612dfbd65975ca4288b5a15013a09562a5f107434cd47bbea2fcd1971dc5f3b22cf5637432f7cd6677beadd438c7f1e7cf751072320e686dcd2517ef84feb7546dc29962d924ec20bb535b1be37269e1e0096ae246468e7d4b946b5315427d3682202ce11253d9b0899c3f66159bbf0a3dd133c6b0b3f6e81c942a8ee85df9ce9b6ba598b278c7b0d4d44cd09c364901cae75470947bdb0479e394bbd3a8af7d51324bcbebb93763cd806229da8bb9ebb2cca27e9225ba02bd461b17851848623b280373882e803f23d7df1f8adfaa54223d4929d0795e1d7518d1ac4fbeec4a4bbae8a97b0de0d25a78d2b0db4cb31cd4f234a25eba123e818f5921d2c9c0f0368a24786b62585a1aa72b190c57ad7a7076571dcf4400375279d726c2dded08a8d3e3c2d6376f6fdf910adbe360c04894176bf5b06b0dc180a936ce826ca890098245b3b1ea6cf4fd5a69936664d1050ce970957c7361bdb33762816dddaf8bd38a6c456bb6beb9d8aa87e2427b290e75cb545c4bccb9f75a9a02098446ce31088cdd416d1361af99a33ac243651b2eb8166e7d4379a717c66ad3f80d151c43387c9a3d865164a7e151a36dbe278560b6e220ddc47ae7f9a86890bb9a7b0f9648ef8131da30cc37ee826e26951661a9e647e9d055ba19e0076d6c83174e2b797c23e9c69536e7b03b8fe0c46973cb3a81ab98a68cee49812d1dd3b2791749dd1d3647a0578c66a2368ea78bf634800f4875c8e4d5057731b1e35989526b25d0231ce3a72b4232387d4194ac9dd6f6a9dc1abaa6aed40e99961e32195f3a228918051b55f3dc0ffb1bf8643cbe832adf34a487bbbf3d5d8523d1a40da36d3384cf8d45ce5071d03e54da703e5adddb003149030a8d31b65bc9e0527b138ba5a19be4164de7258096b816ebb558492ecbe824f03975adca4eb5e8ef61b4995bf6e29d39a27f0363314b553f5fb635947717ad4ff5c6766ce2db73bcc76c66f39443e60924442b6114c1dc975dffecbd7992dd9ce64f46d7a8486db2e82395b65200dd761f8731e4521dd2e7e3f70a0e35edf3703a2e92061ed3d41f1e44726f5a0eb4be637b92bdec481f19387a5e9f8c3702d0f536a8ada4cd5471e91b7de1608bba713188ff2abab0589b39b8e90f1683afb0c92594245b51b69ace70c9a85d915675433bbfec81341fef7d35470d83b0275afce5bec3317a531ce822a54ebdb32351b83bc20ea99eb47c2c5ea4fc1871c8a77a0cad808d122a14ac54a1366edec4493ae6691a2f7b22df9edfcf6566ccbe74a94ef1d4cbb0d93edbc5a94bb33cb80ed98f245786451fef5a0d0ce987c82397e83b65c5789d0b21526f06d6cf1ef47b26d42461ab9b72714f8bdb664ecc82c32eadbac66edf36cdcf4b875a54c58727b0553b036ccedbb759ab592ef738564e7540455ea26dea5abbd90b9e9d2a58ad3ee81596a77135c2d9941b19506ee47c252a65698db02ab0679d3a0c94aa1ec3b387ab93e27f000000ffff8c9d518e64a90e44573412606c60ff1b7b3a64ce481541bfeadf51774fe64d2e603be244b5a5973df48c1a6358bd2af4c01a378dc2ec19501b7c0e344f6d6f9a21c058e1c067acf2dd00e1a7d7706d31e901fdef7aa9a75a467f34e087e6cac25dc52068eb3081bb0d1f0db8b1602085d67171f2d23a0ca530203c44acb6eb5c0782a1d379e9dbeedaabef3b97c34c06a9c95a370d0e356fd464ad73341721ba7942b146754f16bd712f66ef3fa4eda9a26befde759cb7d8a14ba5d879185b297b28730c2d4572770fc1c1ad5fa5e2b18454252714a4125380d4840c20c70ec23d9b6d916bbeb645de9330ad3dddcbaad69370120a6819ee9b68da652db11c7bd8668c6a2a4de0beaec9486dc63b6c2cc41d394cab5a9b97459bef77baa7a1df1c306b78ac61a481b5db0dbd32fb315dfdede0f9ca65eaa067b7f2fd0ffca1061ab48a2c2a01f55e9aaf3cab359f331765ccb4db7637e7e906056e01d39d7b82664534b55745ceae165b5ac2c3d25ace8d1dd6737f63cfd298899e431910f4b1d42d3407ce2c938edafdec4be20c6b53c2cb38464a84be30955fce3f19fa9d6e0cbda37a62aee15933d796aa953d2096a36eaf9e5792ab0290a8997ad3dfa857db2bb2b987e937f8ef366918cd82d8d1b5744f837f162b8002ba11bd6d2e71776ab64f4b803a6d4df507a21bb5657a4185cd7775608fc6e6af91db1434546bba555f05bbec1fcf46ffbd229b17320b6289f14ac879d19a968cdbb38faad5a2c66ac623de9630f87ca624efa44e3f12f2b4c6fb26e58541f672bf723f3a02246321a1e6b6643754a4c71a9793b5dab7b5e9314b688b76dc36ad2dcb75d4ee1134b8b79efeb93c2996eec12568fc2cd1087cb05e15df5f4568d88df6b0ccc3684b05004853431d09106ca712d9039081b127801caee9b2c4eeb5183a627bfa91e1b514907223e453489405c311a9de4c333cae56cc5a8b3650f9ec6abd2f4dfbb553e6b3fecf7ee40c7c0b14bd1c93dbecdebf0d7053fb9811583894e705b842051041c8f4b1061657e3aedcba7e39eb3ad83bb93cec6ddccc42d5b72e1697a62e124dd6eba810b036259149a4abf77435f46ac38642bde60c3b092172dba8206b5fac851928f3a714e69bc42775e27dbb079331dd33c3123de7473368b2d31547cdea75ad298e2cee6399afa08f4dcaa5b556775721e48e668c057c4cc7f454040d6c953c924c368f55732750474a7b8e55912aef9c17b3217a0bf0d0425bc508ed31c998eb96a1eea7096907d087d48586d7aa72ab6269a4051832b6396ee72fd82b65d9de59c3363c828197fe227149f7ba91558cae021a180c16e1683fc8a743c15ea3c7981fc25f7f24570e154fcece10566d707ba9e28c2102e7c534c8dbe1bdd2ed8a32d0039a51bda9c2ba5796e5227cd69431de38704bf7d60a9fda8cb19aa73514ee8f6d551b6570aa08ead9b9602ad8a6094c3063f66523d08350d1d4f477b0aa5bf6a1a322a31c1875a92d50fe766857ed4ae4954e153b90dc4e7db5961b57afbed9c6ed07cdaf958249084958d9f8547d6cbc518a1a846aa1bbad57824f7dd6a717926ba5440ce50df92efdfed36d7cccbc7e26057c0ae4635dc531b3ef63811d6b2c23dc4d9c0b6a0384cfada822bb8a7e7dbb43e76557b4af37a9793814554442c2b29d5374da4f6b6581910061a76b0b67f77444da5b7df8e15e85784d4f91a472d0cab12a3833fc36db35ef0e58e4502e652239b4a1071a86a60ccecb67f62ef6a05becfe093a7fb5ff6acceabfe37716705356d271413a15f349d567143374c2787d65ba15bf67846b6d46a73667a67c568a15be2eabf4a1fa4e9b8a13dbb82cc19ac9a19a1fc06eff4c497a362afef9e2b9661d1db6f798c7539648b5355a70bfdd750b0c3ff4ebf4aa3d3a4c117fc588d6e85e6befed9e06064711fd17fdea47aacabcd04a28fadbd2ee1b37c0cd8ed4c0c6b3edecba7d6c9304d59df74e9b5c9066527a7f9c4749347f58b2d8529bd9db191f79a6e3c42c6d8613ae00cb48b3ecfc5b54e328d89aa576144e24ed23dfcc6dbf584d028e4de54fc9357c478a582a2ba2b36713572815661f053547df5695c0d9b76e06b1c076f949732fb206141be51788e0b0b2a669072b259778b22e8f632e489ab45ae5e32db65e18ee72e338a24cda9a1a1c035292dadb79fbcd4ff67c049b5e8e2ec37120d56d33c5027bd315cb8c4765a37decaddd70e243bb85a502b4ebcda4100d6b8f5a37a0ec1a9da0e5d1ec1e26e0d340314f2d81f5f2bf72a269509a016c4cb75862e7ed39f71524beb97705d98eb9d644c9fa817056f669b22ec4e80a228dab4796060f830cc37dd22cf0bce03d9be9587a1ca6aaea4240086159decf3e3e6380932a17004f91dd3752e07fca6a5ca7b6f3151890454fd32d47750fd6c22b14653d5ee467d3b05148a7a70ed4aebf4ba95cc150d22682af628c68aef20c58f8aa0e709978688fc826abaf9fa0e14f4b7c55596c1a2d49dbf250510e8bc185f2ebc76c47a6667cd518eb672cd4a7bc29cbab0506bb6d7f9e7d2293b1cfd5b79995e786f2ebccf8f930f21f627d0c1613a399d66810e666a09177f38094ccdd3df4e1fa12549754714c6433839a5e7da537aac4fd0eb092978ded0ff21ffd6667d1ceb2b8251a10daee3dbb290e7700d4d0dd0095add5c2ec053ac07d36c5df22ef88284db118b170d1eb5acb9965ae181a878601f551cb7f3c8cae674e9f0417e8baaaddf40eccee06b956af0d7b7b961f52680dcb6a8bb8aaed06cd935e7491e164d9c45478bbf9dd4d3fd7f7d8e7f6e4912a6d370553bb63e99f3fd6f3e46a9cb0bb47f563390551addae3527959e416eac27d7b435af011df2d348cf47a09183224fa8c19fe42ad7353ea9c98871e612efd91c7884a8bb6801265895cac72e8adf67c2ea354735b214ae9f4134c1394f3f2fef45cc601b9f1f2bad0f0e82d43bd6e1f298e7dcb3d33541e33898f39201e9bfd297733ebe23a87df46aae238bd1923677150c8230c260761695259ee832698c19a1b7ca66e3161542c95363b396b3cbcef7b14e3c2bf9134bbafed3eec1aa6c62b7238326c2e45e9d5bcebc6fab626a5c9c13e2d36b83966015b3986e65711d8778ee27a29cdd33c541bc183c5dc70f7b51aa7b85e8497fc1711a332b90ef8d48c70bd2baa8854ec998ecb3f37ef56dea471a679ea81b1983827cf4a8367de0b830a1ee2de68cdd09fdd9cf768cb8675597d10fd01358dad91c65c5854b1c40b9b61c856301cced5b263fd0b02e1b0c8df46b6dfaee2896d2d880b396aaa8c390f321c4df265a11fdca653d30d1198a48d1090db64b761d5cd985285563f591ee330a78d9b50f03e86fe805054858d1e733f527d5e16a85e3d337dbccb47532f2bde88d0b833246febc1a0d8910a4b8d9af126c11d6dbce1e430d724c3e139edb946cdd6cdee8641b39d3e7eeb8c7fba15a89e757553c40efb93b1156638afda766bc668426bb576cf06f4d2b58d44f8ae8aacc6c216a421a3349a97c71ec294d2159b2777b891eb6969eebc78d606f2e2e1fb7ea0613d46ba6443d38fbb481bdec694dccdd273a8d48cc48b47e5e8336071974a4223096cda7e80ae69fa45fa3573288c65105f6a318d742e8f33cf09dbfcb566ff228c7a190c859ccba51febadfaa6b33d34ccc115b8df908c9336787fbc4857bea89f9f964f992cadef8467a3ad0c503d3a24261469a4d1ddfab106252b2886e7d41d7ed9a615d499cb8a9ac7782dc60617a6ad0d76f99edb6cc2cdb64db4a6a1c12db15733a9d89ae390aa6416f80acf7b4eccafdab3798a3c00ef0c0d061c05885dff3e2609f2c1e22f1cb137034b4bb8f734f6e1a11a7b6d4b5082b6bb6dcf7b6890739cb200de01c4a66c0399738576024134a952b8e1b8509f7a9ee474d06fbeb044d9b347ffd42cc3ece63dfea289fddc9e1309b73a547a9f96140c10fe2c2b261f02b43baf3691043c98998ebfc536e531b9d975f833d75a3b4caa4a12e0b2ff15830003434f26683a9f78616a6e86c13ebf7b3adf338b713859652e38598d2aaa45b36119a2340ad7b25a15abecb42b47af5a3f1146774bbde9bde3b77ff773aca0ea37aa499e69519dfefaffcbe83ad3336a7734e54be59ae9c2e6bd86e1f49f0a6e90c661c4402809c326e1f4af74e46d8af27ff10fc8c9f4c691e08e95746372c73fcf3a0d57f3514fc1cab1ad127d84baeca148edb44b32c94d69d9324f712daa97a167f08890a0d3cf8695b986396616e15965bc1a00e5e57f164f93eca2f47d8c79b5675a590b8db5a9a2c3e5f51f2d01dc556ba03194b5bb01c26c3bc42241abd82dff55ab4eec4dcb364d4efca535e460126e206f266f600b5d2e32106729777c6699b4ca0849df5cd1463bd51497317ab78944c0ac7043fad9d135f4999663b34041b3c17d965d2d87e72ca60ff29bcf4a532b3dc51f0c51272f5ffc6243bc7f36088a09dd9333e639bacf8fc4dda82181413aafe6da8ce8cde46d0954771abd8d44443de6e9339af924e6cdd750316faeae9eee001ca5b462ab986f3f1fb6a05e718a904db526daa7ff9e922da7bd1d4482da8a672e1ea572a1419e80ce51dd3cf0a52f545bf6b936401dfd0645b75633b5466fddd255d2358b548f5bdb5bd7761c5d07558c63503e1a7b8e83661b0463d4308522a9bfc0ac753f5dab8c037d6e43caac06bd7e72defef075db4a22168fe9d4a30c31fe1c7430d75cc37df2e457c65f3072deeba5e7ce6552219a5c71bcaac8b3544906a4e72801a38f79afc1b2c39240f4089889a5f02442bedb1afd779c0c95f269e5a2472843d3a4e3020af8925fd629e5b2f91bff7fc4df300db7dd0b48bb357c2b57e375cc135f90823437664cd776b59b50a6a4241a6d4d03ad17fda1ee139ad3ad55ddd1bee6afe2b22f5d73e8563ec9a25e361b0e2ca72601b756f33fcfbbe193f7f0291932874f8c17532ec359f77a84d54566df3647046060d9b60885ac034ec65b298312cba09e0fb57bbd12f468b5cebf0205c1626f464e7d3e580677cd95c6f3a658999880a1970dc2390f627735c6276918f937d6a17da8863d116df6512a23af33b9cab94a200005fe3660fda610ec1f28a9ff010000ffff8c9dcd8a65b911845f65a8fd80943f4a65bf8af166dc8d1983c71b2f0cc6ef6e42e7b67145a8a85a0d344577cd3de74af913f1c5d3d222c54f6e1524f09982b4881a75c6a5c843e107911bce019e7c58c610c91168e3401148e4fbeed6fcee0b7ae7a74a41e455850c4c993947f560daec8d9c767458329a0589b983fbdabd1c36051e2bd8e4a1564e20a205a685f9d7e495b1efd5651a0d08799b7f464ffdc8a380bd4f8ee467681b9934520515680e2c36a886895e705ecb32e58d6be839452d8be80d136abed7b88055a18b90d5681cace0160bcd5c2e22928a40912aabfe1eb552b77540f74b487d4d384ec42ec5548e5f6fbc96975fa5b75e2d29b8f23bc7f6e84fb9701c8dec0139d8e5befb600bfaeb47d855e4ec02dcc5e74cee143b34545229a34c385830e50a69f1fdbd88f339d70239ed5265c011194b7e8ddd56bc41987682d295933418f90dece3966678244ce93c54ef40052c93f680ff913dcde7de11ff79e6926f7ae3939c261ba34e717d60b5856c0f2168ccdce2f23b703a6e377afbba385b21939161594f37a9ebe126516d0350a7eb725a38b2af457578c342177e6e7f72e0bfca14eb9d8a70c8e0c236a17f0f49520ecde841a6c0f4f80c2171f5eb3e2deb4a844fd26283134b1ff5d2497d11328a570bedb550e85063745bf7d8e6a90b344b26a4590f285c1995e2ad395b633960815cbae2a6491e559ec8942d2fc5744870f8e96dc4b5a83d669eaa9809c0f33c02f19cac9e42eb3e493692fad38180209159238a43074ad618e4b3e2716111ca9f99b4fd4ff19765cc01d6eeea0c2e60f061374ef60c8e42f9806a5a59cb6544c286a8a7c28275c7b93c497c93b93783144a581a2849b372729cce36783be529544b3ec8842460bb7cb9763b6b389844f8a8ff3b6cb2a7cfd7d4a0067c5ac676cdebce31272ec5e0357f740e9d3382784f6211244ff4e4b2f3ba609e632342963d0d58e8bfbff8a0c750a13f04e9c957c9dd597bbfa6c7d1192e15d6b4d001676e77e7efac6362cff9b5101e5886607f2e127cccf816bb08f1aa6e7efec8c316e24d202d98f773e38487d133ad915e9271b59de93896188489c87e2cd09144d8b5101dd62243eb44d94fbfd6052b2ce5e395eff52253b7ce4bf0f2ebb402cca94fb1be2fbd5d5b4ea5dfa23f49f68af6da1ca972d70fa165f1e48cb384b79c6f31085aa6a47ea2f5ed2e49a54f5cd0a2b471683745e6921b572c2fee363a4d496686398b89bae14893e09f24eaea730658ee0e1a74034762f2b9a01564192086a1526a1f02f158d652f01fe603bddcd76ff19599803d43ec5074d6129a3da6bf31f5601e2985e3814b88f5e1bc190a1443aa8c4b3debdc0f9fc46fa176400f63ef4179f7fdd4932612901ab07a760f972ef94a55bbeaaa2f7b8d5735512b199424f5dc87f49cbb4051b070af2cf412fe8d7ac65e31dec5b166131180a2b468df5b2cc8589beed67efcc26db704d187ad1bb5571b4b8a1c3fca197468d30603127283352759c79751a2a11f29896938615ab2910f60bc9b57207356b4fc0f5c68dbb74c114480efa5a2cf7255802794e6f2508f359a4372907fc48d31e0a435595463c74c2f2b859b130fa641a4ccf2770067ab7a552c730923ffb88a667c01a8320d6f81a082304e578f6b948730d3c702e395fb44ab29e8ff81c71d72471f93a44b0acd40ff38856d6987cff9a5540260f97ba46c481b302e9141c0a9a9291827174eb68186883eee8e461d6f35ffdfe584db99e554f1c4a57e65e5d158ff4ae8c6c1a6f346121e0d999c6c5cf70c2d4736b064593594d9f515c9794e4f53f8ea8ac98b43046a46b29a333076640b2b66e29caa6be3e2cfba0ae61175d15c1521bccd47b129d77af27015922701edbed6fc9325c5c0938ac435e0fb6390bb01b2261e15dd865e33443e84b6fb3c27da27faa4576877fac5deb71069c72f957b9b09f9fc0a9796d89dd7cd4152edd77ed04b0e5445873e861a0ca85bbe6fd0d17fea137d066798bd97d495e50a91095fac323534c17c60b084fe855d47fc288bf831ce0f86e0e0db1bfc9ba2393ca45b99fc15833ac732049db6823643565b10da8d21910e70839a4fde0f76aa4d09dacdfcc4c3ff947a508517abb1d433f102f50ee1c44ee4ff4a1c1e7681bdf960a379e86b993c8157a09ba723a7a8c79189b8f3324e4418262f8eb38728c22ff4444723b784c867f3bc6d7ccd42e8caa6b4404212eba42538e94345e3806f27859107a6f290841e389f9cef588c81b09116ad69494a20ceec4619c8e71b580a1c20e173731b069afefb29ebaba8c199cd4a4b8b9229addb86dd510e5d205c655fda5e12b5153061e8ae0eb6485675dbc2d88ff3cdc1c2488de0454a105fef1a2bf9c8b86632a3de31135ee2e085e554665cb7a8b58f406b73fa5c924f8d2151485c4b8526a5dc7e5790dc45b086a83366dadda34eae6a1fc32c92f342d32b8311ae77dae6391da385a6218b8a0f7f03e8b55061499507749a7c0d8017dd22a49fb50e47ef2b30a88941a10cedefe3f9d91309975c28616daf3ab7ab9c1d1ac948c9b678cf937a651fd90586ec3357b0dd0e7ed129c10c0ae67801216208a138103bb3d4cd5f39599a82678e2ecb3e5b46bfd2bb3bd8e6b4721b8b485e838afc8aead8c0da62ca6c8dd5c226c2ca4362d2913d1f97e272458c8b7a60f6d9f54b03ba5a22d0519fa292e6720139f422c80216bbf4a4467fd39f0cd89ff972ae560315c238a668976f338c09acd98a8b4b7423e944d1c251e234c58070c86ef6162e84bb3534e5233d4c30eaf0f422579c9fdb7a9294b8e727fadd3391850b40fd15e5bc13825947c68657dbffc94692577906c748ccd15094f5a7afd153b5c15a370492b42cc55c8e7dfb92de654e90927470c8f90e2f042dfc7132638ce6bb7120f5cc34cb16846ad143812929f9f60addf8c9415b52365c5552b8b1d614f0f71efc876032c2a429cd222a99357944c58af2eb07f3fc05b1f022899a53bcba86aba2e4c908aeecf8be6c499c09ea261858c5327f5362da384e042e6891d1a5f5dc40e5966682aee3e7f8d4af55efc58ccfa7dbdb58818e4592845aa0a2de9e22c72411cbd383ae259bb80f062ed1b38a492d60965932c25cebb49722001a14dd5b9e542f7e97019d6c8512cf12bfd5d5ac8a1947858b8713676d4852254c29d55c703b3e700e31bba2b350ab6a1f75c2711433b61be2143ad4b0ea625842c421700b9f0a3c573e6954dcfcc8c2b056e41981d47b5743f8238fa77fea29ad7bee56ba8eef6a61ea1249eaa5a2837340487bb85e9d3d0242327aecf7ba931e98ef4bec37f2d7c6585fd2525ef38561e690c54b9ea342b40ab674147e8348dcd7a2700f793199969bbe976b738a88069ae051a253006cc295bfbb97d6ea3153662cd3325b319ab0488e2d8a95da186a498575d2f83e416e3c1f8b25671bffcccf52c50c3c677201925beb83882970127031f398aa61c5dbaf3ffbf3f9ef7f9e1ff9f7eb0fdffef1dbdf7efce59f6fdf7e79fbf1f7df7e7cfffefb1f7f7dfbf9b7bcfdfec7f71fff7afbf6cbffdee1fffba16fbffc89b7444be7e228949d53d6217243ec006b891caa31163a4f1024c4e4e93136f7568175c6a68760b6e0c96d19482444725b571732d45968cd42908df043ce2109129d3e64686d0023b1d7e5b82cb7b1a7bbb184e521e206f5905768e0609b04f320e1ce9a0164b9f0b567ba15d6b012b57eff5b217c61633582c264033a6b2d2fd9b61a3cec12b7b68728740c32c3c1d6cdc077546212c0e8d9ec0d044040681727ec830af7b44e4da703536e0dbe23564f28c3e556878597e7a855c37534ea487fdeb25a851c24641a906bb1c838f22470c8640d4e4a160c2edc735c1b3922a9398f620b43c40aea71897b3aae60decb1a886ecd219be925593110afb23663ee9ea4ba41f0ee90adfcde3539611390bbc9c642c8632038e1d1979d65a08cced660fa16d24853324aca7169f1899f6b4f9d1700a0c69eb0d1354b023247ec6592b45c9eb2c2a8899e813ee6de08d0a5c754817c4ba63462d821bc1ba0949b054e08ed0c3102837e69d2e663dbc8830ad8578bcf39eb08dec0ace3886f86cf65a91109a7ac8c1eec3883a72e254cf68d486755ce8023144c50e52b20931579dfda7b711910672cc57d0236e7028bc75b9d4ad7dd00f0493d8f982ee1e76c583d14e80553bf1cd4585bf231b1a009e013e588fb044d04879a6b04116a34e1751c9ab495e667bb6de9cd0c10176954d020937edcd3970c590774f1293865684504ba3b6b205345036131b02e268e401bc4899d4b4b3c9b27db8dd7d1e8ea84023877a573e9efb01008eee724d068c37a0ed1cba8470481c040869278f7948d9143ad31054f3952a21030611ec53bd39d96eb92a9b487cc8d510e0f79822799cac608d55994c0fa4042d1c4f8d8679b4aff9a07ac6ffc8b6d6c13196d1b0b57b95637dd2d595c07f6267e3818cc824fae440435e7604d8855f5de59e742e3151dc4873b783f8219540920782e16abdab324565abd6df7a978c73492f02e30dd790f8465244773eb65f2faa812045521b362e4dbad1b0004cef0c815bde928d16bfb9676c9e054e294dac38ee669cc938b23d212e8a29788e04f0b27d4c7bdad7c88b265d5de4b0340301422ae44b58f6071076ec496a4dfa7ac00ab93bcee8e0c5d9349e80ec1ebc321b0052de7712034dcdfb60f930c637c4d79f40d42fe9068bd34b43582d785bc955b20483b8a2742f017b8daa2d036af66b01b60be7c5fd76a61852183f0c24a85b48215bb72253dade1d821f264fcee5ba48d81777348c100393e8fe4f3306148380f0c068dce1c5b3b411f9bb7a38aa65ff5d08e2e4cd2f769120f968eb0624fa010b4449ce13875030505e3145a3a8c0fc6b8c063950da1e478e9d023717bb25a02f85b01e6ef34266102beeb12e13cf606665a3cd43ef7104cc1d86653df5f844e49883444d0c9c4658b031a50ecf4f168f26cbbb267a910a77aa8f2cbb69009cf18bc65380e3d63e832816fcf63f2700cac4bb44c51c533a65cef7583af9ab9a0fda77768c2d12323c1634de6b7002115746ac7058c8cbe7e26abcecdd16eca4b54703633053e7190726f87d807ddb137e0d41273661b3613febac150c1d9ea71d2ea84f00d9da2d838214c31ddc6c0fe22b3893583916fe5d0e0fc170000ffff8c9ddd8a65c79584eff5144ddf17e4ca9f95997a1733c8a831c3489619f5c0c0e07737dfdebb604e4496abaf0c8da556d5d967e7ca58115fe8c93ffbeb92ec1674eb98b55889d1de6607ad91b519eda35350b50da376107c4837f84f5a2f6c9c3dd78b87d0308cc57db62cb95ec5c0c77fb1bc7cae5592e9ea1018bbe894d41a9d44a2b8a14897a27aee244ca104ec5ccc320672b27718103bcbb881896cb6aa661f3bf6765d8568aa65a3711898e0586a3aec3b8ca150e6eef061edae86e8690e07b263e6e5c4a16ce14342d4e868aa5641fe358b28e88c6d416ed205d65e586396a50bf8c008573d387311d0348d93d53497ab9e5a7d1fd427a6be6c50f14c61766df2fe6d317ceb03783845c9c2b456c2d1217b66d70718ebb61b23f6b60915d178b6610e131a64b7c52a2733aefe604388281f3e9a57edb3468f587d6f93170021a5f64b8fba76777a34fdf07d68b7c4655fb7031308e0b0daf2194edd27c7f08afbbedf418b759b2e784bb384468fd1626fc3e570fb507516bec7d6f96aa232ca62eb780862b5b2f85d9ff098a75d601b5282a5697ad3ca967263bf4c4720875d0c3e96b8e386da54a9adb771ca07dc77a3b2834f2ac621d7696740bdb7b860c79aae399fcd8b477e88050964995d1b4655d120325f6f35b4b2e6e8c339674ca9aa0635b6ce826969fc06443ecc3246aae8136e90bc64ff6d20fdce0d7a9a9053765653cd8e6fe24b1135c904aa7730a4997e9923aacda34104501e8e984069432b768774ce9e5f1a97fc58739b805e39b8d6344ecde194224f542cc85c66ac6156a4b95cc20dc001b63d1a1307b91a693ae267586e28cca486194b3762bdf7da97f6315432bba141eaa8a402754f87b5d30d30d957eafa6462bed55505ba44d321632cbe58ea0dcd65c2e9b8ec24fa76bf33d366dfe9c47bf4c11cbdad25ef7c5c8a0690aa33ebf02a757ecdfd509b386a2d9610ba2ab876dd3690134d34ae148cb1690cc2683afbefdaad07a06d7c86fa00d67077ec71d5f56c2feda76a7b287762ec248b6f73cbd5d4a954280a7cf46575d0092a7b36fda634aa80971c79490782214906e416a5e61249b4f7efd8d7425265a2b2e65498dc459e79edbfba9fab4b4bcd1fd188f9ebb619fc1a5cc6a8568210a908ef36c95058cd786222487da8c26fd9b02034c2d1d191359279bc0c950ee444c51fe80c61eb0030501621df0e351a1cd8dddff414a42a330056bbb5075c8d571ab6ba52d1a95794553dd48bbabedcde796f158d8f056acd98231b4a4f5115775fad8b1e8a6d29efb5d1317c6bbc70536dec2d43fcf3d6241d7d24bc7447b8e168d37fc7c21a6845656c139408c1b1fa8a82bd5e174071c2c4a62b9f6fc2ecddb32d1ff9b8c61ecd01356c7d43e5521c7cf393b5d63dbafbd056d79ccdd8b29d25683543dbc5f0aece331be02e6d85142beca16fb45c5b4a7106479c655fdd3450210c4c0d07cdce6b4f0757b449eb736b6d4db7e75fe7a395384da085cb17bc35f57740fa62b7437d7d5916e4beeae0967ba5afd1c75ee6621bbaa5b9ce916ef911febc79d8744e7781af569a89deb12ef3ae2e08c2103af3b2fda75ae345f1bcedf6c0017dc3dd686a5340da0013e0319162a5675766c19273a72f27079c01e3afd64739a0c01216d38c111bba129cb0e56b17044bb6a9ef009bc41f63afe05f3ede37f33a9eafdb8577aa61f55ee0d581c8e8296fe7c987aab7efd4ee596c9ba10271ace9cb853c4c73eec16ce4a296ed6272d896ac66cb6d14fb462e5cbf2e8d58be92b17a8ea6ce568aae57d52f2689b330fe0cde2afdf97bd9b10d9402d3dc427740c8f089a44155701aea98707c415f5ba24c17c341dbae4f16c6b7a7088b85feb621cd5b569a11abc8d7658c5e5e371cf7a001a6711bd2a4775dd26520161839a2f7f9da3b7aab1810f64ca0c7cba1e248f4b9bb9b8677ae5eb47a873ce07481f8f5dd7a9b49a6bd9b63e03868be6f6729a5af7cfdca3f12ed9cf66b59f8bb0d4ed6498fdaf0a32f97f7d8e2ccb5d59544798d6d5941bb853765ad619d567454ccb0f975efccb07b1d9d56d5d672366d5e372ddee361df390ee930f7c86a7b2b23ba51eed035b793d301ec0157de5a1400db98241fbbc69af673e520dde7beedd1cc3d44a9a34e1e7a90debbca59c701f096c53b39275fb9edf591a70d266854e011f63bc82bd0695cca83e7cd3fdc5bb8da74c11a71f4127535361fe5d577fd90a0b3183bcc3e9b7f336f3364f8494b8c26cc258295a1d9130e51abe99340a6620fa7f6233617f510011b4fa5dbc3caf0101c95618a22ad1b4146bd2fad5a2ece1d1ef7f576a8dfb761a3b6517190ed5e4a0ea70b509f43e233fa978f4de18f1a3658a0757131c4827d228774b015498346056c6b19b3fa850e517b29078faefaceb39f0f5f8fbdb67687edb71d6b96f9234bf4814d4881d757e996e5a7cf0e4d2e67b35b6f9edef1efc8fe7af5e7df937aa778ac7f2247bf1d2f461fee7a30f7fbc29745e055a6a09e03bc9cf68a3a24172e92c2544181d87259c36b17929e0e2f48e8f3755a7b20057d4dddc5373a7ed3cad7bd14fa323e8e433892a7697623481fe695ca8e5db55e8eeb116a05e4b31956c54af371d78b6f2bbb0f2df46532b052c7cee6b4faebe92e11b25405a6433740ed358a470df4def534c3c0cad07f6f42445807247b7643b2bbd6703f38c4a2d5335f136e50fda1f104fb813ad1711ca95d0cc5adcea5e744bb9a20edab8389c74ecbe8b59b5f976d4c5f87f6ab3a2d610ac96d847983db8e5719e5e9eae5d3fcd41d16bb43b555d255e4b06516ea47d1cbefd91c96f5e2ffca1fde65e6fa184df0a1b69fd9af819507f75fabadf820284f7d7bb5eb6068a6ce73cddaf6147255d3c11d149ee1c726f5745a0745adc836661ef57e5ecdb6570db3838eb0f3a30580f762bd26987da656cbf5d1ab898e076bc0fd0d1f6dd95d3936f74ab9fc642fdb2ae750d1ae4a5b07debdfa671f540587a0ca863179e22d8e8cba36edd04d0a51ed160fe14fa1c1980fd1877ec83d88a0686028081ea3d9ae68c536e81ad562b39be675f28e15ca93229605152edc908389d44ffeb013ea300ba729b5b7629f69c6cab8506aaa1d224b370b89f202465c376711ed5afe068e5d6ce70462751ba2f532d0db5144f8d4fcdc0d9898aea7c008f9379576b29906d628bb861bf03b528b9d438b1e00ed583d89adcc4ec515d449de5aafce73b248fc212b2ebbfb43bb0915c156f43371b1a55de86155e83b948250dbfc36ecc5e216391a431a8d97c5aa3eda081e71edf4d9a316abb901f74c6ec3eef93943c614e49416c3cc1ead42969b3fb0652b2da944766a760c6f7239065561b6c788ae3714fada2dbbb2dd94c0dd3053fbf35a9fa9f71be8b3562a02a0652aa60b7cdd5ca6c3b295f5b7385918534aa0e62f6f9ac5c7e52f40526936b2b7c03a6a430e5fd22128ec3e7a5fa9666ba66dbebc3ac9773adf9dbc32d6eb72f111316616674fb55594ad013337d2cea205e24acbdd5bd665a69fd6f10178046b1042b00c181e1903038d0520d7ca7326007b732b92c4ced41916bbad32b116bca3a23a01eb130d2e9ec6f09a4117bdedc536f8508ddab02d0bebdce59654a64ffd8d3e658db49cb6cc671726b00d6386b803ee4ebe1eb038880538544db6aaa8bf365390159a5df3149725290cb0afbeac67bd366d41387296d72dd63b18d32806b4af6ccbbab2ca6479ab7ee03d2c0f5757b5110c1a9ea52cc113ebc8cfaba7d8a5988ccfd6c7b02222d862878663abe84a2a4bec00402a572906803ff739533c3005999d18f296d6d7678de247dbd134ca5ea115cf6ab7b40bca59a765131c46f973b3cb731063e0b1e72d4b76a71099e8f18eb5ae75eafb2ff3e274e8209da316030a6f7adc4c8e21833a75774abf7c4be3b8015aa93a7c8d55952bd606460abf0c1c94dad6a1686aa34ac37e602740d26fa0324a824a33958a51b9762f476f96b7291d55a0195ba977cbb016946a3d157a595c6b756dc65d6299618cad8d9a7fe1e46437ae5bae57e3e5473b463efed404072e94b9b6a71ae4b9b8d32ed58a4ad0098c017af517a526f98fc48b606608cd9e2e5a3a94f24c1582ae5b06dd6e8af702db9c562884aa0330d41ef6c67a4aeda5e0818b11b58b85c0a1461bafbe0faa6b5424ad45a5dbba12f14603d07be2a5d579ab264d0c2a3dccf5ca04bddfce99bef49cd90d813f32ddb24c59a51b84cb9e17a141ac4d240815e23219a996ed1c774f5f4f826258f69fcaf62515f324c68bfb02cb8c62865d2a7dea364678b2e50883abf543301ca23baf72d53e122940eb662b3c065d604ce46f9357ebf4490bba5b31ff4465f59e1a8319b8c9b511e5ca4e6fab49a442c968caa311e39361a0edf99a987fe87fbbebe1728c05304d5ce163fd669d182311b51f7a07516cbda50284ab596f83ba83aa43ac25dbef953c462a9746697f6c0699c836b461d0d1cb1f8dec58b4b717291f9c1247a249ed6d7b3b31393ea879fa6cec79851054e02ec3803897f77899956c5081a3be82064ab46aea081caae15f1ca8e333e1c72bea8cf62a78be1dd3b3f7edb7b99ff6ec303cbbc21b843f6bb188cdc2cae2a41d7f9037a5273d8b160adee674a690bc379f9be89233c644dd73b4612f740e78750ac79831d40986906bac5d2004705574465b3b2c515a91a7cce445bdd3ae4685a2d164fa6dbf331eeb44136b0ff36ca3fbd7f0a464c31d24a73a4e1e93b99f349071517ace3497615daf5bcafba5ce066675ab823d1cc2e53259792a0c644d68b9285aff6adddbd2c25a84dbe508d2962e0f9bba77f141f1eca645aee8d87369b03498566c9bc9ad29f4acaa2c95fdc3a5cb159c8fc20c28abd3ef3e3a9fa9cb50702cb85317714bc561f5d848e436c65f44227d7fb310c848876745db66b442d0d3ef53df6b991cd57337f344b94bfb1e0e260624ad518eee1765eea9b4bba93b05a496b853d6f2da6a1cc3afe5a0efdcf8599b4e3d17a142af0cdce8ab594080ecd81a4b2319b7e1f7b8ce9d49ad845af51884f4eb597bb3ff7e66ebb6348cdcc65a7aa8afb6a8a0975ff4284571e2cce15d737a6c47daf2c5da580acb21e1b3544eb59be1fd40a169e9c7ec0b9aebbb7ed5dc9bec4945b3d10c0b28bc72f0650ee7ee8edcd5b23f2eaadc2a450cd589d98b946a572e144bdba4e7aeb1b7d60595d92c2b482fcfd0f6b464b64cd3ea5aa415f55924f2bc9d7f7c343c5516316ff84a455f30bfca736da2a2c1c21774f0545b98188de0d13ca72365804414cdb505df612bbf583176e9ee6ed7dfe2b3f6edad19040f525a51df16c0c269b84f724d8742ab0fb2225427a050aa51bcd5b1ed3e814554c31a83914b6788c45979e83f4bbf7e01a5d84d6b6fe82c54a755a35dcfeafd7a8c503b7dec5475f1f4ce29930731d44b972bdc333cd9a6cbcfde93cd861ba9e9c835b2aa6ecd1e71b9ccd7c6bff7d628b57052a1d38ca9ddc752666d85635bbbd6dd5f6055e525125736019269159784296219b51ca01edde7890140c68663d275e9b0703c6acdb460fc331abe88bac75e46553889e6b8be6a330d821e1f533b8f742a64d5618e06649569967c5adcbb6b2b594bb3ed2d656f7641a9fb15b6f0a895689087aa35e918bd3f86d158629bf82e67ead904736fa1b8fb6adbdb5a6deaaa9892c93c70b7c670340ad5e6c59ae7e1b8e8025da15b6fcf65267aa81d2eebc4f365cba67695fca8e052c7566b40e8da36067fbb29a3d9d7c1ad4903c550632b7d13535768487bdadad411e6fd86d8a1266bdf3b07cc52c82797ecee4001cda2debb58eaae6dc9833bce52872d29aa53de7ad2ca6856632698fda915e5b9368e7cd5429f350995119a8c02bfa0913b327bd56866b3fa0fd63a9288e6370a9b600bd4065d69d09355379c438f745a388652cae01464e966550a1333398cba7d32b6d37f580964aa6dd8e288d3fed7aca9570d1add7a687c7e507ba09b909854a984c5dfa9611c2aa65e3d1e6deade843e9dd6b52f9a26f46ab9183eecea371bfccddacda8f8f07bbd3c86070159226ff34d6ee2004acde47e3a4ab53604883a5676d48badcda990ce9421873b483734a3b2e2de4edb843bd8443cd264961e7b6eab7739adeef04b377702651b265bc71a6318278a4e10d3ad4f9b1f5270283d72e759fd0445806ea3b08ef35a5c17bb37656a0e05fc781ef909aa2c8b609555c812848694c3d611c017cbb654d15c38646597c5dcfbf2b73b10e0e9e31b208f3e5b05de378dce3ac622a71cc4697d4c3bd325c5bd52cf067b229eb6f761cd550bb9de541cec9e3a22d34f32b5dacdc899f7340d005c13deb495cd61a00166747bfbd6dcd50ed66d5a3ff2f71ccd80b9a338a472a18d69e954f0cf2bc9aa3622f9aa91f60e7ad8b2150f0ecda6e1c30f1b8d63c9daa1a8d7b5d5df47e97ff8d391919f1e0247f2e13b3ebb294463a8038d0a8b960641b7dcf943cda2b0d58a07197bc20fb1b5ac2ff40cd1699d69d852bfb31673dbd67d85bacc5a37732de592ae365fdfb74f9f8d199f2ca373af9941b85603b89c5c8fece8e7e17087f7b255eaee6bb7357d3f32b67e8a15fcb4d972bdd9e1dda71527eb27ae9566eba86606825a666d4dcffcda66341fe696bba9ce0e10c2efbe253cbdf42ef6d776be3cf6cca6a56c2743fa911d803fd3124d748fa7399450ca979ac6cf8b5a5ee6bdfb3887d759f16d06797e6f4aa39b2ebcbf66b27fb4cbc278354fbd9d348fb7637fcd3d78254a9db6f3c5de5a9ed077f1ae7738a884643f3fb89dddf2f8d9c6e1e46db107eb0d790c2a4b98619b3eb664eab23773f993eaeebab63add4063749ac90d6d3076590782381e7f8bf2b44b210baf36b7d4d007c8f44360e27e4dd2a16cb2c7293a7b061442c6e9d6bf73f22260ce5cd54ce727d64d10e51bba83e6fe80d2a5ea5992b31a9f30d4eee7f892562dff8c53cd249285776b7ff2aafe284582c1b51a83ae15c864d5731d5776405f7d300b544a21c3b263ff003fb5924d31383bafe9bd4d3aa2ac42fb17bc2ee8be19d4b9a74a2148c2cd91b882a3fc909dd9af4bf3f63803ad6cbacc99abba9c0394a7d4ed2b77c243f2c4f4de10854ddac69563b3f009784b23c0d67e9923542792821bd522faab9ff91e36f6aba7ed998d42273ed385cf4bf15b4718f540160b7a80bae3ed9c12d3affa3e6bf7197d8ee11cfaa3fe59af9576f13055cee9bbea3ecbd40bcaee7dd9a18615798ce26a026265da4ea9e48c6d4b319a4a9b39d43a1b4c6dc9aad9774b6b1389916359814fe4a177f28472be6cbaebd0540fd603a15dde898af07c2855cb5b2bae7a8ab9bcac73d632a31f2a831cbac9a13deab2fd1e70071df3482af87bed5e05ab2def6ec31303e091265376ef64056c6f7742d6d52bc1654afba9c66c8d91d935d459bb6f6638c980c3eb4656fd774f10858e273bf4e4867d5bd44173ebec9375dbc5f5dccd832f9824b371913736016d62e0d0523def748de174237aa4e7e389121b9a107882c5c9476b83759be1e0d21c7e3a607f835f639e08ca13dabfe74d3d5c811136d45eb25b5523370bae62151304bebd60c93ae21eba477f5d167c987d8abe572fb640d0f0d74db682eca183c32167c1dd6cdbd6ce029a7749c42a61d7284343dcc6b14b55d609a778e0b96e76c43a90c65e4d8dadadd3faf7491aec96aee335567e43676ab662d249341b99400cd9028db6ddd1d4438eb5ac5abf238e1e5de8d818f6f6f1d86c88e1b70f7d514148d1c4f377a344fe90c90ff93ead35eb849c69bd2e5b18fa0ee8638ec8852fee7329b145c5f3eb59c708af66ea3b97b3cdaeaa1d0fd79778f7653083d82c2d35135933dd238ea9aada0769bd78ef1578b8c7cd461c7d6adf2a2bdba9f531951d927e06b02e9a932b3bfdba0aadefe0ae34e1404748f70b417fd5559f3e6bf072aac09a83acecdda6038ae6ca572ef9b3e08e5790cfc7f923bc3945ed894ea3bc2f85b30e3b338e0ae6c55ca9afe6b447cf099b11791166f1c5c6e95ad6eb5c4d7b878f28987e31922c8646859485c4afc64d9d32f02ef8f31693b6f1618bd78341fdbdc95263c0f8cbf5055f7b196ea58a56b70b426bacdd75aeebb9338a1a2a7340d9d1267994b2a949e4a82ca9359e6c74e70fc82a37f5a0d5da6d208f365a9b69f7c8a325103bbbf2c4df432ffaa9015b34ffa775193e98b9757848ade3f6f6a7c0b4f1e1704e827a4291e48edc4ce62857fdd2f891bb64f6fdda5ef52cc68b7f756ce47bfb7016ae4430cd10461265e8b870080ed1d0526c340b34acad277ef6b0d4dba4e1de1edb3d2f3559b18663d960024ea26d1768e7c831cd712057ec07c6b5a824fadc05eac4d58f0f9572317e42e998c7fdefb52dead3be0b1607ba1d5d7b18d388cef565ab3d5ecbddfeaafe5a4bf60eda1b61c04d6adbabe5d43e684e5c2dbca908fe72b3a42616ed30e2e2b15b8d5fe3d56fa06f1f7a79e439ce158e243e167e43199bbb59b9383582866aea73f81d3a1bcc747994b35d2938797d365610e9483512884be37e070f9c4f12cfbb2f234d82aa57d1a3de6bf9e286262b0faffbb7630cf543efab971c5cbcbd65801ac2c9bc297f4017e278af558f1b5f463e40d83d6d9a4b1c0ad6f0471781c68416771ab3cdb2392e46aea089d68b418f6f49eb8e7887028c692f2fd0e0d5bcf69707dca09383aa09a5edae550f85a5350129e8dc3146bc6ac48fec027730fc9530aa91a85aafac0bce809ce7cffe72fdef3f7ffaf2e52ffcdfbefefec7afdf7efbfaf397afdfbffdeff7b76fbffff5dbafbffee7dffff6d6defefcfd97df7efb7afd9ffee7cf5ffef6edebcf5ffeeffa87bffee3bffff8fd1fdfffe3fb1ffff5edef7f7efdf9cbf3887dfdfec7f75f7efb7f7ffc137fd13f7ffa17000000ffff03000bd36c77e4060200"
+ ],
+ "rawHeaders": {
+ "access-control-allow-origin": "*",
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96086b125e60e5d8-NRT",
+ "connection": "close",
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ "date": "Thu, 17 Jul 2025 08:37:36 GMT",
+ "openai-model": "text-embedding-3-small",
+ "openai-processing-ms": "183",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "via": "envoy-router-544967c6c5-x2xbp",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "186",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "5000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "4999992",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_b9351ce5f84420f8e6078a467cfb19db"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-adds-genai-conventions.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-adds-genai-conventions.json
new file mode 100644
index 0000000000..8ca9d3353e
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-adds-genai-conventions.json
@@ -0,0 +1,43 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDJt3XpbTrkrYBUooP67fAFPTDDa\",\"object\":\"chat.completion.chunk\",\"created\":1752737269,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDJt3XpbTrkrYBUooP67fAFPTDDa\",\"object\":\"chat.completion.chunk\",\"created\":1752737269,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDJt3XpbTrkrYBUooP67fAFPTDDa\",\"object\":\"chat.completion.chunk\",\"created\":1752737269,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDJt3XpbTrkrYBUooP67fAFPTDDa\",\"object\":\"chat.completion.chunk\",\"created\":1752737269,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDJt3XpbTrkrYBUooP67fAFPTDDa\",\"object\":\"chat.completion.chunk\",\"created\":1752737269,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "960804db6f65fcc0-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 07:27:49 GMT",
+ "openai-processing-ms": "172",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "176",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_24119fc6cbdf89dfadb4c34bd78fad3b"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-misbehave-with-double-iteration.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-misbehave-with-double-iteration.json
new file mode 100644
index 0000000000..ba08b95c4e
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-misbehave-with-double-iteration.json
@@ -0,0 +1,43 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"South\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEAdgA3DwPSOefWR7otezsJxzkO1\",\"object\":\"chat.completion.chunk\",\"created\":1752740539,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "960854afac96ec0e-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:22:19 GMT",
+ "openai-processing-ms": "295",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "299",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_d40509d1b6b092e3978ea52b8c02864e"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-abort.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-abort.json
new file mode 100644
index 0000000000..90efb3eb11
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-abort.json
@@ -0,0 +1,43 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuEFi0mRPm9cbvVmysG9jAHfivvUt\",\"object\":\"chat.completion.chunk\",\"created\":1752740854,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEFi0mRPm9cbvVmysG9jAHfivvUt\",\"object\":\"chat.completion.chunk\",\"created\":1752740854,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEFi0mRPm9cbvVmysG9jAHfivvUt\",\"object\":\"chat.completion.chunk\",\"created\":1752740854,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEFi0mRPm9cbvVmysG9jAHfivvUt\",\"object\":\"chat.completion.chunk\",\"created\":1752740854,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEFi0mRPm9cbvVmysG9jAHfivvUt\",\"object\":\"chat.completion.chunk\",\"created\":1752740854,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96085c60cb99e392-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:27:34 GMT",
+ "openai-processing-ms": "234",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "239",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999983",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_52a5b54049b36409e582504ea7a435a9"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-tee.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-tee.json
new file mode 100644
index 0000000000..35498bac62
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-does-not-prevent-usage-of-tee.json
@@ -0,0 +1,43 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuEDkFdMN01PVlzzLwbFr98W0jM1N\",\"object\":\"chat.completion.chunk\",\"created\":1752740732,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEDkFdMN01PVlzzLwbFr98W0jM1N\",\"object\":\"chat.completion.chunk\",\"created\":1752740732,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEDkFdMN01PVlzzLwbFr98W0jM1N\",\"object\":\"chat.completion.chunk\",\"created\":1752740732,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEDkFdMN01PVlzzLwbFr98W0jM1N\",\"object\":\"chat.completion.chunk\",\"created\":1752740732,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuEDkFdMN01PVlzzLwbFr98W0jM1N\",\"object\":\"chat.completion.chunk\",\"created\":1752740732,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "9608596839e18a5d-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:25:32 GMT",
+ "openai-processing-ms": "238",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "241",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_0d087f5cec909f11f1e95347619bedd5"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-multiple-choices.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-multiple-choices.json
new file mode 100644
index 0000000000..233d271209
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-multiple-choices.json
@@ -0,0 +1,44 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "n": 2,
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\"Southern\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"chatcmpl-BuDPruvXvy1cTouU79MhRWdmZWMqk\",\"object\":\"chat.completion.chunk\",\"created\":1752737639,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96080de51fa847f7-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 07:33:59 GMT",
+ "openai-processing-ms": "209",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "217",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999981",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_fa9c7a16b29cca45d28032fdb0d910b6"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-tool-calls.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-tool-calls.json
new file mode 100644
index 0000000000..f28bddb43f
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-tool-calls.json
@@ -0,0 +1,170 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_9ujI2ZExKzIGa57dsFCuwSXI\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"n\\\": \\\"N\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ew Y\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ork C\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ity\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"call_M5Jmiz7Y7ZUiASk3ShRROpUr\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\": \\\"L\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"ondo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpRr8h0kwBLc53wzb0GeYXsWCcX\",\"object\":\"chat.completion.chunk\",\"created\":1752739225,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "960834979c54e366-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:00:26 GMT",
+ "openai-processing-ms": "1046",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "1055",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999970",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_222b8015f2cab35df65c97bfd8e226f0"
+ },
+ "responseIsBinary": false
+ },
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ },
+ {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "index": 0,
+ "id": "call_9ujI2ZExKzIGa57dsFCuwSXI",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"New York City\"}"
+ }
+ },
+ {
+ "index": 1,
+ "id": "call_M5Jmiz7Y7ZUiASk3ShRROpUr",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"London\"}"
+ }
+ }
+ ]
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_9ujI2ZExKzIGa57dsFCuwSXI",
+ "content": "25 degrees and sunny"
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_M5Jmiz7Y7ZUiASk3ShRROpUr",
+ "content": "15 degrees and raining"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" New\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" York\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" City\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"25\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" degrees\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" and\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" while\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" London\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" it\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"15\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" degrees\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" and\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" raining\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDpTOhzJCQLCyjQ8OcbJsShIN7XM\",\"object\":\"chat.completion.chunk\",\"created\":1752739227,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "960834a86d2f3c17-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:00:27 GMT",
+ "openai-organization": "elastic-observability",
+ "openai-processing-ms": "240",
+ "openai-project": "proj_Pf1eM5R55Z35wBy4rt8PxAGq",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "set-cookie": "_cfuvid=9MBw8QqOzZBRJtGKGZM9zTdRPGaWSVHP.cA.zk.50QY-1752739227664-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "244",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999957",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_ba535371fdc85f890162da3f3ddaa840"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-usage.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-usage.json
new file mode 100644
index 0000000000..1814791462
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-records-usage.json
@@ -0,0 +1,46 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true,
+ "stream_options": {
+ "include_usage": true
+ }
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"South\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Atlantic\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-BuDrRRWybY6JHzabaUyR2OtaEGp79\",\"object\":\"chat.completion.chunk\",\"created\":1752739349,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":4,\"total_tokens\":26,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"audio_tokens\":0,\"accepted_prediction_tokens\":0,\"rejected_prediction_tokens\":0}}}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "960837a3a8faf6d9-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:02:29 GMT",
+ "openai-processing-ms": "281",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "286",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999982",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_539e03a5bdb03c6f6b3010f8c5389109"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-adds-genai-conventions.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-adds-genai-conventions.json
new file mode 100644
index 0000000000..2f11313834
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-adds-genai-conventions.json
@@ -0,0 +1,43 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDvE94ISQVUWZdbkJ6k1XcOMmyzg\",\"object\":\"chat.completion.chunk\",\"created\":1752739584,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvE94ISQVUWZdbkJ6k1XcOMmyzg\",\"object\":\"chat.completion.chunk\",\"created\":1752739584,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvE94ISQVUWZdbkJ6k1XcOMmyzg\",\"object\":\"chat.completion.chunk\",\"created\":1752739584,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvE94ISQVUWZdbkJ6k1XcOMmyzg\",\"object\":\"chat.completion.chunk\",\"created\":1752739584,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvE94ISQVUWZdbkJ6k1XcOMmyzg\",\"object\":\"chat.completion.chunk\",\"created\":1752739584,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96083d620bffd4fb-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:06:24 GMT",
+ "openai-processing-ms": "199",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "204",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999983",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_be25c8ec8b23a1dbb37740b72311c307"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-multiple-choices.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-multiple-choices.json
new file mode 100644
index 0000000000..5b5b646946
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-multiple-choices.json
@@ -0,0 +1,44 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?"
+ }
+ ],
+ "n": 2,
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Southern\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFELsVfoy6ztg2r7kEkFtC8lyI\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":1,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96083d658a73d788-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:06:25 GMT",
+ "openai-processing-ms": "176",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "178",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999981",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_b15fb55c152b988c9c798401679ca46e"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-tool-calls.json b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-tool-calls.json
new file mode 100644
index 0000000000..54d80e9c22
--- /dev/null
+++ b/packages/instrumentation-openai/test/mock-responses/openai-streaming-chat-completions-with-content-capture-records-tool-calls.json
@@ -0,0 +1,170 @@
+[
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_8mgxuCkNPx3qlomni7YPgFmd\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"n\\\": \\\"N\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ew Y\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ork C\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ity\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"call_oPnYBRvnkzPFgA0R33zwifZP\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\": \\\"L\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"ondo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvFNfcuXkfEXklmzyIFrJJb4bG0\",\"object\":\"chat.completion.chunk\",\"created\":1752739585,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96083d68daf3d758-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:06:26 GMT",
+ "openai-processing-ms": "949",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "952",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999970",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_5955d1bb0d741299c6fb0fef17e3cf5c"
+ },
+ "responseIsBinary": false
+ },
+ {
+ "scope": "https://api.openai.com:443",
+ "method": "POST",
+ "path": "/v1/chat/completions",
+ "body": {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant providing weather updates."
+ },
+ {
+ "role": "user",
+ "content": "What is the weather in New York City and London?"
+ },
+ {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "index": 0,
+ "id": "call_8mgxuCkNPx3qlomni7YPgFmd",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"New York City\"}"
+ }
+ },
+ {
+ "index": 1,
+ "id": "call_oPnYBRvnkzPFgA0R33zwifZP",
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "arguments": "{\"location\": \"London\"}"
+ }
+ }
+ ]
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_8mgxuCkNPx3qlomni7YPgFmd",
+ "content": "25 degrees and sunny"
+ },
+ {
+ "role": "tool",
+ "tool_call_id": "call_oPnYBRvnkzPFgA0R33zwifZP",
+ "content": "15 degrees and raining"
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "strict": true,
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "location"
+ ],
+ "additionalProperties": false
+ }
+ }
+ }
+ ],
+ "stream": true
+ },
+ "status": 200,
+ "response": "data: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" weather\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" currently\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"25\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" degrees\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" and\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" sunny\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" New\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" York\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" City\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" while\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" in\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" London\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\",\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" it\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" is\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" \"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"15\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" degrees\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" and\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\" raining\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-BuDvHbKGyexqi9yq72brOvY4WfZMH\",\"object\":\"chat.completion.chunk\",\"created\":1752739587,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n",
+ "rawHeaders": {
+ "access-control-expose-headers": "X-Request-ID",
+ "alt-svc": "h3=\":443\"; ma=86400",
+ "cf-cache-status": "DYNAMIC",
+ "cf-ray": "96083d71fdde6836-NRT",
+ "connection": "close",
+ "content-type": "text/event-stream; charset=utf-8",
+ "date": "Thu, 17 Jul 2025 08:06:27 GMT",
+ "openai-organization": "elastic-observability",
+ "openai-processing-ms": "195",
+ "openai-project": "proj_Pf1eM5R55Z35wBy4rt8PxAGq",
+ "openai-version": "2020-10-01",
+ "server": "cloudflare",
+ "set-cookie": "_cfuvid=xDfDRpDRw.EnLw2M2RDvnWf649EQmohfTJG_qqGE6iw-1752739587245-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None",
+ "strict-transport-security": "max-age=31536000; includeSubDomains; preload",
+ "transfer-encoding": "chunked",
+ "x-content-type-options": "nosniff",
+ "x-envoy-upstream-service-time": "198",
+ "x-ratelimit-limit-requests": "5000",
+ "x-ratelimit-limit-tokens": "4000000",
+ "x-ratelimit-remaining-requests": "4999",
+ "x-ratelimit-remaining-tokens": "3999958",
+ "x-ratelimit-reset-requests": "12ms",
+ "x-ratelimit-reset-tokens": "0s",
+ "x-request-id": "req_536598da01504470d10172c7f5346a9e"
+ },
+ "responseIsBinary": false
+ }
+]
\ No newline at end of file
diff --git a/packages/instrumentation-openai/test/openai.test.ts b/packages/instrumentation-openai/test/openai.test.ts
new file mode 100644
index 0000000000..21402f2ce3
--- /dev/null
+++ b/packages/instrumentation-openai/test/openai.test.ts
@@ -0,0 +1,3198 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * These tests verify telemetry created against actual API responses
+ * which can be difficult to mock for LLMs. The responses are recorded
+ * automatically using nock's nock-back feature. Responses are recorded
+ * to the mock-responses directory with the name of the test - by default
+ * if a response is available for the current test it is used, and
+ * otherwise a real request is made and the response is recorded.
+ * To re-record all responses, set the NOCK_BACK_MODE environment variable
+ * to 'update' - when recording responses, OPENAI_API_KEY must be set to
+ * a valid API key. To record for new tests while
+ * keeping existing recordings, set NOCK_BACK_MODE to 'record'.
+ */
+
+import {
+ contentCaptureInstrumentation,
+ instrumentation,
+ loggerProvider,
+ logsExporter,
+ meterProvider,
+ metricExporter,
+} from './load-instrumentation';
+import {
+ getTestSpans,
+ resetMemoryExporter,
+} from '@opentelemetry/contrib-test-utils';
+import {
+ ATTR_ERROR_TYPE,
+ ATTR_SERVER_ADDRESS,
+ ATTR_SERVER_PORT,
+} from '@opentelemetry/semantic-conventions';
+import { expect } from 'expect';
+import { Definition, back as nockBack } from 'nock';
+import { OpenAI } from 'openai';
+import * as path from 'path';
+
+import {
+ ATTR_EVENT_NAME,
+ ATTR_GEN_AI_SYSTEM,
+ ATTR_GEN_AI_OPERATION_NAME,
+ ATTR_GEN_AI_REQUEST_ENCODING_FORMATS,
+ ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY,
+ ATTR_GEN_AI_REQUEST_MODEL,
+ ATTR_GEN_AI_REQUEST_MAX_TOKENS,
+ ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY,
+ ATTR_GEN_AI_REQUEST_TOP_P,
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
+ ATTR_GEN_AI_RESPONSE_ID,
+ ATTR_GEN_AI_REQUEST_STOP_SEQUENCES,
+ ATTR_GEN_AI_REQUEST_TEMPERATURE,
+ ATTR_GEN_AI_RESPONSE_MODEL,
+ ATTR_GEN_AI_TOKEN_TYPE,
+} from '../src/semconv';
+
+// Remove any data from recorded responses that could have sensitive data
+// and that we don't need for testing.
+const sanitizeRecordings = (scopes: Definition[]) => {
+ for (const scope of scopes) {
+ // Type definition seems to be incorrect of headers.
+ const headers: Record = (scope as any).rawHeaders;
+ delete headers['set-cookie'];
+ delete headers['openai-organization'];
+ delete headers['openai-project'];
+ }
+ return scopes;
+};
+
+describe('OpenAI', function () {
+ this.timeout(10000); // Increase timeout for LLM tests
+
+ nockBack.fixtures = path.join(__dirname, 'mock-responses');
+ let apiKey: string | undefined;
+ if (nockBack.currentMode === 'dryrun') {
+ apiKey = 'testing';
+ }
+
+ let nockDone: () => void;
+ beforeEach(async function () {
+ const filename = `${this.currentTest
+ ?.fullTitle()
+ .toLowerCase()
+ .replace(/\s/g, '-')}.json`;
+ const { nockDone: nd } = await nockBack(filename, {
+ afterRecord: sanitizeRecordings,
+ });
+ nockDone = nd;
+ });
+
+ afterEach(async function () {
+ nockDone();
+
+ await loggerProvider.forceFlush();
+ logsExporter.reset();
+
+ await meterProvider.forceFlush();
+ metricExporter.reset();
+ });
+
+ const client = new OpenAI({ apiKey });
+ const model = 'gpt-4o-mini';
+ const input = 'Answer in up to 3 words: Which ocean contains Bouvet Island?';
+
+ describe('chat completions', function () {
+ this.beforeEach(() => {
+ instrumentation.enable();
+ });
+ this.afterEach(() => {
+ instrumentation.disable();
+ });
+
+ it('adds genai conventions', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ });
+ expect(chatCompletion.choices[0].message.content).toEqual(
+ 'Atlantic Ocean.'
+ );
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 3,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 3,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('records all the client options', async () => {
+ const messages = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ] satisfies OpenAI.Chat.Completions.ChatCompletionMessageParam[];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ frequency_penalty: 0.0,
+ max_tokens: 100,
+ presence_penalty: 0.0,
+ temperature: 1.0,
+ top_p: 1.0,
+ stop: 'foo',
+ seed: 100,
+ response_format: {
+ type: 'text',
+ },
+ });
+ expect(chatCompletion.choices[0].message.content).toEqual(
+ 'Southern Ocean.'
+ );
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY]: 0.0,
+ [ATTR_GEN_AI_REQUEST_MAX_TOKENS]: 100,
+ [ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY]: 0.0,
+ [ATTR_GEN_AI_REQUEST_TEMPERATURE]: 1.0,
+ [ATTR_GEN_AI_REQUEST_TOP_P]: 1.0,
+ [ATTR_GEN_AI_REQUEST_STOP_SEQUENCES]: ['foo'],
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 3,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 3,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('records multiple choices', async () => {
+ const messages = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ] satisfies OpenAI.Chat.Completions.ChatCompletionMessageParam[];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ n: 2,
+ });
+ expect(chatCompletion.choices[0].message.content).toEqual(
+ 'Atlantic Ocean.'
+ );
+ expect(chatCompletion.choices[1].message.content).toEqual(
+ 'Southern Ocean.'
+ );
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop', 'stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 6,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 6,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ expect(logs[2].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'stop',
+ index: 1,
+ message: {},
+ });
+ });
+
+ it('records tool calls', async () => {
+ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ strict: true,
+ parameters: {
+ type: 'object',
+ properties: {
+ location: { type: 'string' },
+ },
+ required: ['location'],
+ additionalProperties: false,
+ },
+ },
+ },
+ ];
+
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'system',
+ content: 'You are a helpful assistant providing weather updates.',
+ },
+ {
+ role: 'user',
+ content: 'What is the weather in New York City and London?',
+ },
+ ];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ });
+ expect(chatCompletion.choices[0].message.content).toBeNull();
+
+ const toolCalls = chatCompletion.choices[0].message.tool_calls!;
+ expect(toolCalls.length).toBe(2);
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 57,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 46,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 57,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 46,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'tool_calls',
+ index: 0,
+ message: {
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ ],
+ },
+ });
+
+ metricExporter.reset();
+ logsExporter.reset();
+ resetMemoryExporter();
+
+ messages.push({
+ role: 'assistant',
+ tool_calls: toolCalls,
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+
+ const chatCompletion1 = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ });
+
+ expect(chatCompletion1.choices[0].message.content).toBe(
+ 'The weather in New York City is 25 degrees and sunny, while in London, it is 15 degrees and raining.'
+ );
+
+ const spans1 = getTestSpans();
+ expect(spans1.length).toBe(1);
+ expect(spans1[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 125,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 26,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics1] = metricExporter.getMetrics();
+ expect(resourceMetrics1.scopeMetrics.length).toBe(1);
+ const scopeMetrics1 = resourceMetrics1.scopeMetrics[0];
+ const tokenUsage1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage1.length).toBe(1);
+ expect(tokenUsage1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage1[0].dataPoints.length).toBe(2);
+ expect(tokenUsage1[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 125,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 26,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration1.length).toBe(1);
+ expect(operationDuration1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration1[0].dataPoints.length).toBe(1);
+ expect(operationDuration1[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration1[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx1 = spans1[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs1 = logsExporter.getFinishedLogRecords();
+ expect(logs1.length).toBe(6);
+ expect(logs1[0].spanContext).toEqual(spanCtx1);
+ expect(logs1[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[0].body).toEqual({});
+ expect(logs1[1].spanContext).toEqual(spanCtx1);
+ expect(logs1[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[1].body).toEqual({});
+ expect(logs1[2].spanContext).toEqual(spanCtx1);
+ expect(logs1[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.assistant.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[2].body).toEqual({
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ ],
+ });
+ expect(logs1[3].spanContext).toEqual(spanCtx1);
+ expect(logs1[3].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[3].body).toEqual({
+ id: toolCalls[0].id,
+ });
+ expect(logs1[4].spanContext).toEqual(spanCtx1);
+ expect(logs1[4].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[4].body).toEqual({
+ id: toolCalls[1].id,
+ });
+ expect(logs1[5].spanContext).toEqual(spanCtx1);
+ expect(logs1[5].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[5].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('handles connection errors without crashing', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ expect(
+ new OpenAI({
+ baseURL: 'http://localhost:9999/v5',
+ apiKey,
+ }).chat.completions.create({
+ model,
+ messages,
+ })
+ ).rejects.toThrow(OpenAI.APIConnectionError);
+
+ // TODO: Figure out why it takes so long to get this span. trace.getTracerProvider()._delegate.forceFlush() didn't help.
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'localhost',
+ [ATTR_SERVER_PORT]: 9999,
+ [ATTR_ERROR_TYPE]: 'APIConnectionError',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+ expect(tokenUsage).toHaveLength(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_SERVER_ADDRESS]: 'localhost',
+ [ATTR_SERVER_PORT]: 9999,
+ [ATTR_ERROR_TYPE]: 'APIConnectionError',
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(1);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ });
+ });
+
+ describe('chat completions with content capture', function () {
+ this.beforeEach(() => {
+ contentCaptureInstrumentation.enable();
+ });
+ this.afterEach(() => {
+ contentCaptureInstrumentation.disable();
+ });
+
+ it('adds genai conventions', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ });
+ expect(chatCompletion.choices[0].message.content).toEqual(
+ 'South Atlantic Ocean.'
+ );
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 4,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 4,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: input,
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content: 'South Atlantic Ocean.',
+ },
+ });
+ });
+
+ it('records multiple choices', async () => {
+ const messages = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ] satisfies OpenAI.Chat.Completions.ChatCompletionMessageParam[];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ n: 2,
+ });
+ expect(chatCompletion.choices[0].message.content).toEqual(
+ 'South Atlantic Ocean.'
+ );
+ expect(chatCompletion.choices[1].message.content).toEqual(
+ 'Southern Ocean'
+ );
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop', 'stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 6,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 6,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: input,
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content: 'South Atlantic Ocean.',
+ },
+ });
+ expect(logs[2].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'stop',
+ index: 1,
+ message: {
+ content: 'Southern Ocean',
+ },
+ });
+ });
+
+ it('records tool calls', async () => {
+ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ strict: true,
+ parameters: {
+ type: 'object',
+ properties: {
+ location: { type: 'string' },
+ },
+ required: ['location'],
+ additionalProperties: false,
+ },
+ },
+ },
+ ];
+
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'system',
+ content: 'You are a helpful assistant providing weather updates.',
+ },
+ {
+ role: 'user',
+ content: 'What is the weather in New York City and London?',
+ },
+ ];
+ const chatCompletion = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ });
+ expect(chatCompletion.choices[0].message.content).toBeNull();
+
+ const toolCalls = chatCompletion.choices[0].message.tool_calls!;
+ expect(toolCalls.length).toBe(2);
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 57,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 46,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 57,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 46,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: 'You are a helpful assistant providing weather updates.',
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ content: 'What is the weather in New York City and London?',
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'tool_calls',
+ index: 0,
+ message: {
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "New York City"}',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "London"}',
+ },
+ },
+ ],
+ },
+ });
+
+ metricExporter.reset();
+ logsExporter.reset();
+ resetMemoryExporter();
+
+ messages.push({
+ role: 'assistant',
+ tool_calls: toolCalls,
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+
+ const chatCompletion1 = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ });
+
+ expect(chatCompletion1.choices[0].message.content).toBe(
+ 'The weather is currently 25 degrees and sunny in New York City, while in London, it is 15 degrees and raining.'
+ );
+
+ const spans1 = getTestSpans();
+ expect(spans1.length).toBe(1);
+ expect(spans1[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 125,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 27,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics1] = metricExporter.getMetrics();
+ expect(resourceMetrics1.scopeMetrics.length).toBe(1);
+ const scopeMetrics1 = resourceMetrics1.scopeMetrics[0];
+ const tokenUsage1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage1.length).toBe(1);
+ expect(tokenUsage1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage1[0].dataPoints.length).toBe(2);
+ expect(tokenUsage1[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 125,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 27,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration1.length).toBe(1);
+ expect(operationDuration1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration1[0].dataPoints.length).toBe(1);
+ expect(operationDuration1[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration1[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx1 = spans1[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs1 = logsExporter.getFinishedLogRecords();
+ expect(logs1.length).toBe(6);
+ expect(logs1[0].spanContext).toEqual(spanCtx1);
+ expect(logs1[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[0].body).toEqual({
+ content: 'You are a helpful assistant providing weather updates.',
+ });
+ expect(logs1[1].spanContext).toEqual(spanCtx1);
+ expect(logs1[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[1].body).toEqual({
+ content: 'What is the weather in New York City and London?',
+ });
+ expect(logs1[2].spanContext).toEqual(spanCtx1);
+ expect(logs1[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.assistant.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[2].body).toEqual({
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "New York City"}',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "London"}',
+ },
+ },
+ ],
+ });
+ expect(logs1[3].spanContext).toEqual(spanCtx1);
+ expect(logs1[3].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[3].body).toEqual({
+ id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ expect(logs1[4].spanContext).toEqual(spanCtx1);
+ expect(logs1[4].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[4].body).toEqual({
+ id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+ expect(logs1[5].spanContext).toEqual(spanCtx1);
+ expect(logs1[5].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[5].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content:
+ 'The weather is currently 25 degrees and sunny in New York City, while in London, it is 15 degrees and raining.',
+ },
+ });
+ });
+ });
+
+ describe('streaming chat completions', function () {
+ this.beforeEach(() => {
+ instrumentation.enable();
+ });
+ this.afterEach(() => {
+ instrumentation.disable();
+ });
+
+ it('adds genai conventions', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ stream: true,
+ });
+ let content = '';
+ for await (const part of stream) {
+ content += part.choices[0].delta.content || '';
+ }
+ expect(content).toEqual('Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('records usage', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ stream: true,
+ stream_options: {
+ include_usage: true,
+ },
+ });
+ let content = '';
+ for await (const part of stream) {
+ if (part.choices[0]) {
+ content += part.choices[0].delta.content || '';
+ }
+ }
+ expect(content).toEqual('South Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 22,
+ [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: 4,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(2);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 22,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 4,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'output',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('records multiple choices', async () => {
+ const messages = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ] satisfies OpenAI.Chat.Completions.ChatCompletionMessageParam[];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ n: 2,
+ stream: true,
+ });
+ const choices: string[] = [];
+ for await (const part of stream) {
+ const idx = part.choices[0].index;
+ if (!choices[idx]) {
+ choices[idx] = '';
+ }
+ choices[idx] += part.choices[0].delta.content || '';
+ }
+ expect(choices[0]).toEqual('Atlantic Ocean.');
+ expect(choices[1]).toEqual('Southern Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop', 'stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ expect(logs[2].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'stop',
+ index: 1,
+ message: {},
+ });
+ });
+
+ it('records tool calls', async () => {
+ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ strict: true,
+ parameters: {
+ type: 'object',
+ properties: {
+ location: { type: 'string' },
+ },
+ required: ['location'],
+ additionalProperties: false,
+ },
+ },
+ },
+ ];
+
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'system',
+ content: 'You are a helpful assistant providing weather updates.',
+ },
+ {
+ role: 'user',
+ content: 'What is the weather in New York City and London?',
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ stream: true,
+ });
+
+ const toolCalls: any[] = [];
+ for await (const part of stream) {
+ const delta = part.choices[0].delta;
+ if (delta.tool_calls) {
+ for (const toolCall of delta.tool_calls) {
+ if (toolCall.id) {
+ toolCalls[toolCall.index] = toolCall;
+ } else {
+ toolCalls[toolCall.index] = {
+ ...toolCalls[toolCall.index],
+ function: {
+ ...toolCalls[toolCall.index].function,
+ arguments:
+ (toolCalls[toolCall.index].function?.arguments ?? '') +
+ (toolCall.function?.arguments ?? ''),
+ },
+ };
+ }
+ }
+ }
+ }
+ expect(toolCalls.length).toBe(2);
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'tool_calls',
+ index: 0,
+ message: {
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ ],
+ },
+ });
+
+ metricExporter.reset();
+ logsExporter.reset();
+ resetMemoryExporter();
+
+ messages.push({
+ role: 'assistant',
+ tool_calls: toolCalls,
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+
+ const chatCompletion1 = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ stream: true,
+ });
+
+ let content = '';
+ for await (const part of chatCompletion1) {
+ content += part.choices[0].delta.content || '';
+ }
+
+ expect(content).toBe(
+ 'The weather in New York City is 25 degrees and sunny, while in London, it is 15 degrees and raining.'
+ );
+
+ const spans1 = getTestSpans();
+ expect(spans1.length).toBe(1);
+ expect(spans1[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics1] = metricExporter.getMetrics();
+ expect(resourceMetrics1.scopeMetrics.length).toBe(1);
+ const scopeMetrics1 = resourceMetrics1.scopeMetrics[0];
+ const tokenUsage1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage1.length).toBe(0);
+
+ const operationDuration1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration1.length).toBe(1);
+ expect(operationDuration1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration1[0].dataPoints.length).toBe(1);
+ expect(operationDuration1[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration1[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx1 = spans1[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs1 = logsExporter.getFinishedLogRecords();
+ expect(logs1.length).toBe(6);
+ expect(logs1[0].spanContext).toEqual(spanCtx1);
+ expect(logs1[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[0].body).toEqual({});
+ expect(logs1[1].spanContext).toEqual(spanCtx1);
+ expect(logs1[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[1].body).toEqual({});
+ expect(logs1[2].spanContext).toEqual(spanCtx1);
+ expect(logs1[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.assistant.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[2].body).toEqual({
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ },
+ },
+ ],
+ });
+ expect(logs1[3].spanContext).toEqual(spanCtx1);
+ expect(logs1[3].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[3].body).toEqual({
+ id: toolCalls[0].id,
+ });
+ expect(logs1[4].spanContext).toEqual(spanCtx1);
+ expect(logs1[4].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[4].body).toEqual({
+ id: toolCalls[1].id,
+ });
+ expect(logs1[5].spanContext).toEqual(spanCtx1);
+ expect(logs1[5].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[5].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('does not misbehave with double iteration', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ stream: true,
+ });
+ let content = '';
+ for await (const part of stream) {
+ content += part.choices[0].delta.content || '';
+ }
+ expect(async () => {
+ for await (const part of stream) {
+ content += part.choices[0].delta.content || '';
+ }
+ }).rejects.toThrow();
+ expect(content).toEqual('South Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+
+ it('does not prevent usage of tee', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ stream: true,
+ });
+ let content0 = '';
+ const [left, right] = stream.tee();
+ for await (const part of left) {
+ content0 += part.choices[0].delta.content || '';
+ }
+ let content1 = '';
+ for await (const part of right) {
+ content1 += part.choices[0].delta.content || '';
+ }
+ expect(content0).toEqual('Atlantic Ocean.');
+ expect(content1).toEqual('Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({});
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {},
+ });
+ });
+ });
+
+ describe('streaming chat completions with content capture', function () {
+ this.beforeEach(() => {
+ contentCaptureInstrumentation.enable();
+ });
+ this.afterEach(() => {
+ contentCaptureInstrumentation.disable();
+ });
+
+ it('adds genai conventions', async () => {
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ stream: true,
+ });
+ let content = '';
+ for await (const part of stream) {
+ content += part.choices[0].delta.content || '';
+ }
+ expect(content).toEqual('Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(2);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: input,
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content: 'Atlantic Ocean.',
+ },
+ });
+ });
+
+ it('records multiple choices', async () => {
+ const messages = [
+ {
+ role: 'user',
+ content: input,
+ },
+ ] satisfies OpenAI.Chat.Completions.ChatCompletionMessageParam[];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ n: 2,
+ stream: true,
+ });
+ const choices: string[] = [];
+ for await (const part of stream) {
+ const idx = part.choices[0].index;
+ if (!choices[idx]) {
+ choices[idx] = '';
+ }
+ choices[idx] += part.choices[0].delta.content || '';
+ }
+ expect(choices[0]).toEqual('Southern Ocean.');
+ expect(choices[1]).toEqual('Atlantic Ocean.');
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop', 'stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: input,
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content: 'Southern Ocean.',
+ },
+ });
+ expect(logs[2].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'stop',
+ index: 1,
+ message: {
+ content: 'Atlantic Ocean.',
+ },
+ });
+ });
+
+ it('records tool calls', async () => {
+ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
+ {
+ type: 'function',
+ function: {
+ name: 'get_weather',
+ strict: true,
+ parameters: {
+ type: 'object',
+ properties: {
+ location: { type: 'string' },
+ },
+ required: ['location'],
+ additionalProperties: false,
+ },
+ },
+ },
+ ];
+
+ const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
+ {
+ role: 'system',
+ content: 'You are a helpful assistant providing weather updates.',
+ },
+ {
+ role: 'user',
+ content: 'What is the weather in New York City and London?',
+ },
+ ];
+ const stream = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ stream: true,
+ });
+
+ const toolCalls: any[] = [];
+ for await (const part of stream) {
+ const delta = part.choices[0].delta;
+ if (delta.tool_calls) {
+ for (const toolCall of delta.tool_calls) {
+ if (toolCall.id) {
+ toolCalls[toolCall.index] = toolCall;
+ } else {
+ toolCalls[toolCall.index] = {
+ ...toolCalls[toolCall.index],
+ function: {
+ ...toolCalls[toolCall.index].function,
+ arguments:
+ (toolCalls[toolCall.index].function?.arguments ?? '') +
+ (toolCall.function?.arguments ?? ''),
+ },
+ };
+ }
+ }
+ }
+ }
+ expect(toolCalls.length).toBe(2);
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(0);
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx = spans[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs = logsExporter.getFinishedLogRecords();
+ expect(logs.length).toBe(3);
+ expect(logs[0].spanContext).toEqual(spanCtx);
+ expect(logs[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[0].body).toEqual({
+ content: 'You are a helpful assistant providing weather updates.',
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[1].body).toEqual({
+ content: 'What is the weather in New York City and London?',
+ });
+ expect(logs[1].spanContext).toEqual(spanCtx);
+ expect(logs[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs[2].body).toEqual({
+ finish_reason: 'tool_calls',
+ index: 0,
+ message: {
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "New York City"}',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "London"}',
+ },
+ },
+ ],
+ },
+ });
+
+ metricExporter.reset();
+ logsExporter.reset();
+ resetMemoryExporter();
+
+ messages.push({
+ role: 'assistant',
+ tool_calls: toolCalls,
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ messages.push({
+ role: 'tool',
+ tool_call_id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+
+ const chatCompletion1 = await client.chat.completions.create({
+ model,
+ messages,
+ tools,
+ stream: true,
+ });
+
+ let content = '';
+ for await (const part of chatCompletion1) {
+ content += part.choices[0].delta.content || '';
+ }
+
+ expect(content).toBe(
+ 'The weather is currently 25 degrees and sunny in New York City, while in London, it is 15 degrees and raining.'
+ );
+
+ const spans1 = getTestSpans();
+ expect(spans1.length).toBe(1);
+ expect(spans1[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'],
+ [ATTR_GEN_AI_RESPONSE_ID]: expect.stringMatching(/^chatcmpl-/),
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics1] = metricExporter.getMetrics();
+ expect(resourceMetrics1.scopeMetrics.length).toBe(1);
+ const scopeMetrics1 = resourceMetrics1.scopeMetrics[0];
+ const tokenUsage1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage1.length).toBe(0);
+
+ const operationDuration1 = scopeMetrics1.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration1.length).toBe(1);
+ expect(operationDuration1[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration1[0].dataPoints.length).toBe(1);
+ expect(operationDuration1[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
+ [ATTR_GEN_AI_REQUEST_MODEL]: model,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'gpt-4o-mini-2024-07-18',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration1[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+
+ const spanCtx1 = spans1[0].spanContext();
+
+ await loggerProvider.forceFlush();
+ const logs1 = logsExporter.getFinishedLogRecords();
+ expect(logs1.length).toBe(6);
+ expect(logs1[0].spanContext).toEqual(spanCtx1);
+ expect(logs1[0].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.system.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[0].body).toEqual({
+ content: 'You are a helpful assistant providing weather updates.',
+ });
+ expect(logs1[1].spanContext).toEqual(spanCtx1);
+ expect(logs1[1].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.user.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[1].body).toEqual({
+ content: 'What is the weather in New York City and London?',
+ });
+ expect(logs1[2].spanContext).toEqual(spanCtx1);
+ expect(logs1[2].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.assistant.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[2].body).toEqual({
+ tool_calls: [
+ {
+ type: 'function',
+ id: toolCalls[0].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "New York City"}',
+ },
+ },
+ {
+ type: 'function',
+ id: toolCalls[1].id,
+ function: {
+ name: 'get_weather',
+ arguments: '{"location": "London"}',
+ },
+ },
+ ],
+ });
+ expect(logs1[3].spanContext).toEqual(spanCtx1);
+ expect(logs1[3].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[3].body).toEqual({
+ id: toolCalls[0].id,
+ content: '25 degrees and sunny',
+ });
+ expect(logs1[4].spanContext).toEqual(spanCtx1);
+ expect(logs1[4].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.tool.message',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[4].body).toEqual({
+ id: toolCalls[1].id,
+ content: '15 degrees and raining',
+ });
+ expect(logs1[5].spanContext).toEqual(spanCtx1);
+ expect(logs1[5].attributes).toEqual({
+ [ATTR_EVENT_NAME]: 'gen_ai.choice',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ });
+ expect(logs1[5].body).toEqual({
+ finish_reason: 'stop',
+ index: 0,
+ message: {
+ content:
+ 'The weather is currently 25 degrees and sunny in New York City, while in London, it is 15 degrees and raining.',
+ },
+ });
+ });
+ });
+
+ describe('embeddings', function () {
+ this.beforeEach(() => {
+ instrumentation.enable();
+ });
+ this.afterEach(() => {
+ instrumentation.disable();
+ });
+
+ it('adds genai conventions', async () => {
+ const embedding = await client.embeddings.create({
+ model: 'text-embedding-3-small',
+ input: ['One fish', 'two fish', 'red fish', 'blue fish'],
+ encoding_format: 'float',
+ });
+ expect(embedding.data.length).toBe(4);
+
+ const spans = getTestSpans();
+ expect(spans.length).toBe(1);
+ expect(spans[0].attributes).toEqual({
+ [ATTR_GEN_AI_OPERATION_NAME]: 'embeddings',
+ [ATTR_GEN_AI_REQUEST_MODEL]: 'text-embedding-3-small',
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'text-embedding-3-small',
+ [ATTR_GEN_AI_REQUEST_ENCODING_FORMATS]: ['float'],
+ [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: 8,
+ });
+
+ await meterProvider.forceFlush();
+ const [resourceMetrics] = metricExporter.getMetrics();
+ expect(resourceMetrics.scopeMetrics.length).toBe(1);
+ const scopeMetrics = resourceMetrics.scopeMetrics[0];
+ const tokenUsage = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.token.usage'
+ );
+
+ expect(tokenUsage.length).toBe(1);
+ expect(tokenUsage[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.token.usage',
+ type: 'HISTOGRAM',
+ description: 'Measures number of input and output tokens used',
+ unit: '{token}',
+ });
+ expect(tokenUsage[0].dataPoints.length).toBe(1);
+ expect(tokenUsage[0].dataPoints).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: 8,
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'embeddings',
+ [ATTR_GEN_AI_REQUEST_MODEL]: 'text-embedding-3-small',
+ [ATTR_GEN_AI_TOKEN_TYPE]: 'input',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'text-embedding-3-small',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ])
+ );
+
+ const operationDuration = scopeMetrics.metrics.filter(
+ m => m.descriptor.name === 'gen_ai.client.operation.duration'
+ );
+ expect(operationDuration.length).toBe(1);
+ expect(operationDuration[0].descriptor).toMatchObject({
+ name: 'gen_ai.client.operation.duration',
+ type: 'HISTOGRAM',
+ description: 'GenAI operation duration',
+ unit: 's',
+ });
+ expect(operationDuration[0].dataPoints.length).toBe(1);
+ expect(operationDuration[0].dataPoints).toEqual([
+ expect.objectContaining({
+ value: expect.objectContaining({
+ sum: expect.any(Number),
+ }),
+ attributes: {
+ [ATTR_GEN_AI_SYSTEM]: 'openai',
+ [ATTR_GEN_AI_OPERATION_NAME]: 'embeddings',
+ [ATTR_GEN_AI_REQUEST_MODEL]: 'text-embedding-3-small',
+ [ATTR_GEN_AI_RESPONSE_MODEL]: 'text-embedding-3-small',
+ [ATTR_SERVER_ADDRESS]: 'api.openai.com',
+ [ATTR_SERVER_PORT]: 443,
+ },
+ }),
+ ]);
+ expect(
+ (operationDuration[0].dataPoints[0].value as any).sum
+ ).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/packages/instrumentation-openai/tsconfig.json b/packages/instrumentation-openai/tsconfig.json
new file mode 100644
index 0000000000..4078877ce6
--- /dev/null
+++ b/packages/instrumentation-openai/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build"
+ },
+ "include": [
+ "src/**/*.ts",
+ "test/**/*.ts"
+ ]
+}
diff --git a/release-please-config.json b/release-please-config.json
index 49902e8fb1..19fa95ede8 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -57,6 +57,7 @@
"packages/instrumentation-mysql2": {},
"packages/instrumentation-nestjs-core": {},
"packages/instrumentation-net": {},
+ "packages/instrumentation-openai": {},
"packages/instrumentation-oracledb": {},
"packages/instrumentation-pg": {},
"packages/instrumentation-pino": {},