From dd25a80c0deb8b1c5837d8f8e970d44df80d056f Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 12 Dec 2024 10:15:07 -0800 Subject: [PATCH] feat: instrumentation-openai (#469) This adds a `@elastic/opentelemetry-instrumentation-openai` instrumentation. --- .../release-instrumentation-openai.yml | 55 + .github/workflows/release-mockotlpserver.yml | 9 +- .github/workflows/{test.yml => test-edot.yml} | 26 +- .../workflows/test-instrumentation-openai.yml | 94 + CONTRIBUTING.md | 20 +- RELEASING.md | 29 +- examples/package.json | 4 +- package-lock.json | 42 +- package.json | 5 +- packages/instrumentation-openai/.editorconfig | 12 + packages/instrumentation-openai/.eslintrc.js | 145 + packages/instrumentation-openai/.gitignore | 5 + packages/instrumentation-openai/.tav.yml | 5 + packages/instrumentation-openai/CHANGELOG.md | 9 + .../instrumentation-openai/CONTRIBUTING.md | 45 + packages/instrumentation-openai/NOTICE.md | 2 + packages/instrumentation-openai/README.md | 163 + packages/instrumentation-openai/TESTING.md | 142 + .../instrumentation-openai/azure.env.template | 37 + packages/instrumentation-openai/dev.env | 8 + .../instrumentation-openai/examples/openai.js | 57 + .../examples/telemetry.js | 21 + .../examples/telemetry.mjs | 102 + .../examples/use-chat-esm.mjs | 52 + .../examples/use-chat.js | 54 + .../examples/use-embeddings.js | 37 + .../use-stream-with-parallel-tool-calls.js | 111 + packages/instrumentation-openai/ollama.env | 8 + .../openai.env.template | 24 + .../instrumentation-openai/package-lock.json | 4981 +++++++++++++++++ packages/instrumentation-openai/package.json | 89 + .../instrumentation-openai/prettier.config.js | 28 + .../scripts/gen-version-ts.js | 40 + .../scripts/semconv-gen.js | 95 + packages/instrumentation-openai/src/index.ts | 21 + .../src/instrumentation.ts | 752 +++ .../src/internal-types.ts | 65 + .../instrumentation-openai/src/semconv.ts | 71 + packages/instrumentation-openai/src/types.ts | 31 + packages/instrumentation-openai/src/utils.ts | 98 + .../test/config.test.js | 97 + .../test/fixtures.test.js | 1346 +++++ .../test/fixtures/chat-completion.js | 55 + .../test/fixtures/embeddings.js | 41 + .../nock-recordings/chat-completion.json | 69 + .../fixtures/nock-recordings/embeddings.json | 91 + .../streaming-bad-iterate.json | 62 + .../streaming-chat-completion.json | 62 + .../streaming-parallel-tool-calls.json | 90 + .../nock-recordings/streaming-tool-calls.json | 101 + .../streaming-with-include_usage.json | 65 + .../nock-recordings/streaming-with-tee.json | 65 + .../fixtures/nock-recordings/tool-calls.json | 100 + .../test/fixtures/streaming-abort.js | 50 + .../test/fixtures/streaming-bad-iterate.js | 56 + .../fixtures/streaming-chat-completion.js | 49 + .../fixtures/streaming-parallel-tool-calls.js | 71 + .../test/fixtures/streaming-tools.js | 86 + .../fixtures/streaming-with-include_usage.js | 54 + .../test/fixtures/streaming-with-tee.js | 60 + .../test/fixtures/telemetry.js | 78 + .../test/fixtures/tools.js | 80 + .../instrumentation-openai/test/testutils.js | 693 +++ .../instrumentation-openai/tsconfig.base.json | 27 + packages/instrumentation-openai/tsconfig.json | 12 + packages/mockotlpserver/package.json | 1 + packages/opentelemetry-node/TESTING.md | 3 + packages/opentelemetry-node/package.json | 1 + 68 files changed, 11103 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/release-instrumentation-openai.yml rename .github/workflows/{test.yml => test-edot.yml} (75%) create mode 100644 .github/workflows/test-instrumentation-openai.yml create mode 100644 packages/instrumentation-openai/.editorconfig create mode 100644 packages/instrumentation-openai/.eslintrc.js create mode 100644 packages/instrumentation-openai/.gitignore create mode 100644 packages/instrumentation-openai/.tav.yml create mode 100644 packages/instrumentation-openai/CHANGELOG.md create mode 100644 packages/instrumentation-openai/CONTRIBUTING.md create mode 100644 packages/instrumentation-openai/NOTICE.md create mode 100644 packages/instrumentation-openai/README.md create mode 100644 packages/instrumentation-openai/TESTING.md create mode 100644 packages/instrumentation-openai/azure.env.template create mode 100644 packages/instrumentation-openai/dev.env create mode 100644 packages/instrumentation-openai/examples/openai.js create mode 100644 packages/instrumentation-openai/examples/telemetry.js create mode 100644 packages/instrumentation-openai/examples/telemetry.mjs create mode 100644 packages/instrumentation-openai/examples/use-chat-esm.mjs create mode 100644 packages/instrumentation-openai/examples/use-chat.js create mode 100644 packages/instrumentation-openai/examples/use-embeddings.js create mode 100644 packages/instrumentation-openai/examples/use-stream-with-parallel-tool-calls.js create mode 100644 packages/instrumentation-openai/ollama.env create mode 100644 packages/instrumentation-openai/openai.env.template create mode 100644 packages/instrumentation-openai/package-lock.json create mode 100644 packages/instrumentation-openai/package.json create mode 100644 packages/instrumentation-openai/prettier.config.js create mode 100755 packages/instrumentation-openai/scripts/gen-version-ts.js create mode 100755 packages/instrumentation-openai/scripts/semconv-gen.js create mode 100644 packages/instrumentation-openai/src/index.ts create mode 100644 packages/instrumentation-openai/src/instrumentation.ts create mode 100644 packages/instrumentation-openai/src/internal-types.ts create mode 100644 packages/instrumentation-openai/src/semconv.ts create mode 100644 packages/instrumentation-openai/src/types.ts create mode 100644 packages/instrumentation-openai/src/utils.ts create mode 100644 packages/instrumentation-openai/test/config.test.js create mode 100644 packages/instrumentation-openai/test/fixtures.test.js create mode 100644 packages/instrumentation-openai/test/fixtures/chat-completion.js create mode 100644 packages/instrumentation-openai/test/fixtures/embeddings.js create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/chat-completion.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/embeddings.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-bad-iterate.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-chat-completion.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-parallel-tool-calls.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-tool-calls.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-include_usage.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-tee.json create mode 100644 packages/instrumentation-openai/test/fixtures/nock-recordings/tool-calls.json create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-abort.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-bad-iterate.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-chat-completion.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-parallel-tool-calls.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-tools.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-with-include_usage.js create mode 100644 packages/instrumentation-openai/test/fixtures/streaming-with-tee.js create mode 100644 packages/instrumentation-openai/test/fixtures/telemetry.js create mode 100644 packages/instrumentation-openai/test/fixtures/tools.js create mode 100644 packages/instrumentation-openai/test/testutils.js create mode 100644 packages/instrumentation-openai/tsconfig.base.json create mode 100644 packages/instrumentation-openai/tsconfig.json diff --git a/.github/workflows/release-instrumentation-openai.yml b/.github/workflows/release-instrumentation-openai.yml new file mode 100644 index 00000000..4ef3ef62 --- /dev/null +++ b/.github/workflows/release-instrumentation-openai.yml @@ -0,0 +1,55 @@ +# Release a tagged version of the '@elastic/opentelemetry-instrumentation-openai' package. +name: release-instrumentation-openai + +on: + push: + tags: + - instrumentation-openai-v*.*.* + +# 'id-token' perm needed for npm publishing with provenance (see +# https://docs.npmjs.com/generating-provenance-statements#example-github-actions-workflow) +permissions: + contents: write + pull-requests: read + id-token: write + +jobs: + release: + runs-on: ubuntu-latest + env: + PKGDIR: packages/instrumentation-openai + PKGNAME: "@elastic/opentelemetry-instrumentation-openai" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 'v18.20.4' + registry-url: 'https://registry.npmjs.org' + + - name: npm publish + working-directory: $PKGDIR + run: npm publish + env: + # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: GitHub release + run: | + npm ci # need top-level devDeps for github-release.sh script + ./scripts/github-release.sh "$PKGDIR" "${{ github.ref_name }}" + env: + GH_TOKEN: ${{ github.token }} + + - name: Notify in Slack + # Only notify on failure, because on success the published GitHub + # Release will result in a notification from the GitHub Slack app + # (assuming '/github subscribe elastic/elastic-otel-node'). + if: ${{ failure() }} + uses: elastic/oblt-actions/slack/notify-result@v1.16.0 + with: + bot-token: ${{ secrets.SLACK_BOT_TOKEN }} + channel-id: "#apm-agent-node" + message: '[${{ github.repository }}] Release `${{ env.PKGNAME }}` *${{ github.ref_name }}*' diff --git a/.github/workflows/release-mockotlpserver.yml b/.github/workflows/release-mockotlpserver.yml index e59d023e..7881e7dd 100644 --- a/.github/workflows/release-mockotlpserver.yml +++ b/.github/workflows/release-mockotlpserver.yml @@ -16,6 +16,9 @@ permissions: jobs: release: runs-on: ubuntu-latest + env: + PKGDIR: packages/mockotlpserver + PKGNAME: "@elastic/mockotlpserver" steps: - uses: actions/checkout@v4 with: @@ -27,7 +30,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: npm publish - working-directory: ./packages/mockotlpserver + working-directory: $PKGDIR run: npm publish env: # https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry @@ -36,7 +39,7 @@ jobs: - name: GitHub release run: | npm ci # need top-level devDeps for github-release.sh script - ./scripts/github-release.sh "packages/mockotlpserver" "${{ github.ref_name }}" + ./scripts/github-release.sh "$PKGDIR" "${{ github.ref_name }}" env: GH_TOKEN: ${{ github.token }} @@ -49,4 +52,4 @@ jobs: with: bot-token: ${{ secrets.SLACK_BOT_TOKEN }} channel-id: "#apm-agent-node" - message: '[${{ github.repository }}] Release `@elastic/mockotlpserver` *${{ github.ref_name }}*' + message: '[${{ github.repository }}] Release `${{ env.PKGNAME }}` *${{ github.ref_name }}*' diff --git a/.github/workflows/test.yml b/.github/workflows/test-edot.yml similarity index 75% rename from .github/workflows/test.yml rename to .github/workflows/test-edot.yml index c08a3c78..9c76001a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-edot.yml @@ -1,23 +1,20 @@ -name: test +# Test "packages/opentelemetry-node" (a.k.a. EDOT Node.js). +name: test-edot on: workflow_dispatch: push: branches: - main - paths-ignore: - - '**/*.md' - - '**/*.asciidoc' - - 'docs/**' - - 'examples/**' + paths: + - 'packages/opentelemetry-node/**' + - '.github/workflows/test-edot.yml' pull_request: branches: - main - paths-ignore: - - '**/*.md' - - '**/*.asciidoc' - - 'docs/**' - - 'examples/**' + paths: + - 'packages/opentelemetry-node/**' + - '.github/workflows/test-edot.yml' # Cancel jobs running for old commits on a PR. Allow concurrent runs on 'main'. concurrency: @@ -81,7 +78,12 @@ jobs: - name: Update npm to a version that supports package-lock lockfileVersion=2. if: ${{ startsWith(matrix.node, '14') }} run: npm install -g npm@9 # npm@9 supports node >=14.17.0 - - run: npm run ci-all + - name: npm ci in mockotlpserver package, used in EDOT tests + run: npm ci + working-directory: packages/mockotlpserver + - run: npm ci + working-directory: packages/opentelemetry-node - run: npm test + working-directory: packages/opentelemetry-node # TODO: test-windows eventually diff --git a/.github/workflows/test-instrumentation-openai.yml b/.github/workflows/test-instrumentation-openai.yml new file mode 100644 index 00000000..27997ce3 --- /dev/null +++ b/.github/workflows/test-instrumentation-openai.yml @@ -0,0 +1,94 @@ +name: test-instrumentation-openai + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'packages/instrumentation-openai/**' + - '.github/workflows/test-instrumentation-openai.yml' + pull_request: + branches: + - main + paths: + - 'packages/instrumentation-openai/**' + - '.github/workflows/test-instrumentation-openai.yml' + +# Cancel jobs running for old commits on a PR. Allow concurrent runs on 'main'. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +permissions: + contents: read + +jobs: + unit-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: + - '22' + - '20' + - '18' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: npm ci + working-directory: packages/instrumentation-openai + - run: npm run compile + working-directory: packages/instrumentation-openai + - run: npm test + working-directory: packages/instrumentation-openai + + # This runs the unit tests against a number of 'openai' versions in the + # supported range. + test-all-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: npm install + working-directory: packages/instrumentation-openai + - run: npm run compile + working-directory: packages/instrumentation-openai + - run: npm run test-all-versions + working-directory: packages/instrumentation-openai + + integration-test: + runs-on: ubuntu-latest + services: + ollama: + # A light fork of Ollama to float some in-progress contributions related + # to more closely matching OpenAI behavior. + image: ghcr.io/elastic/ollama/ollama:testing + ports: + - 11434:11434 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Pull Ollama models + run: | + source packages/instrumentation-openai/ollama.env + curl -s http://localhost:11434/api/pull -d "{\"model\": \"$TEST_MODEL_TOOLS\"}" + curl -s http://localhost:11434/api/pull -d "{\"model\": \"$TEST_MODEL_EMBEDDINGS\"}" + curl -s http://localhost:11434/api/tags | jq + # Dump Ollama container logs if it doesn't appear to be working. + curl -fsS http://localhost:11434/ || docker logs $(docker ps -q) + - run: npm install + working-directory: packages/instrumentation-openai + - run: npm run compile + working-directory: packages/instrumentation-openai + - name: Integration tests + run: | + set -a; source ./ollama.env + npm run test:integration + working-directory: packages/instrumentation-openai diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7acfc3a..ddd97da7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ obvious at first sight. Ensure your code contribution pass our linting (style and static checking): ``` -npm run ci-all +npm run ci-all # runs 'npm ci' in all package dirs; see note 1 npm run lint ``` @@ -39,22 +39,18 @@ Often style checking issues can be automatically resolve by running: npm run lint:fix ``` +> *Note 1*: While this repo holds multiple packages, it is *not* using npm workspaces. This means that one must `npm ci` (or `npm install`) in each package directory separately. See [this issue](https://github.com/elastic/elastic-otel-node/pull/279) for why npm workspaces are not being used. + ## Testing -tl;dr: +This repo holds a number of mostly-independent packages. Please see the +`packages/**/TESTING.md` document in a specific package for notes on how to run +its test suite. For example: -```shell -npm run ci-all # runs 'npm ci' in all package dirs; see note 1 -cd packages/opentelemetry-node -npm run test-services:start # requires Docker -npm test -npm run test-services:stop -``` +- [Testing opentelemetry-node](./packages/opentelemetry-node/TESTING.md) +- [Testing instrumentation-openai](./packages/instrumentation-openai/TESTING.md) -See [TESTING.md](./TESTING.md) for full details. - -> *Note 1*: While this repo holds multiple packages, it is *not* using npm workspaces. This means that one must `npm ci` (or `npm install`) in each package directory separately. See [this issue](https://github.com/elastic/elastic-otel-node/pull/279) for why npm workspaces are not being used. ## Commit message guidelines diff --git a/RELEASING.md b/RELEASING.md index b379899c..7f7d1a0e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -14,6 +14,7 @@ packages will use a prefix (the basename of the npm package name). For example, | ----------------------------- | ------------------- | -------- | | `@elastic/opentelemetry-node` | `v*` | v1.0.0 | | `@elastic/mockotlpserver` | `mockotlpserver-v*` | mockotlpserver-v0.2.0 | +| `@elastic/opentelemetry-instrumentation-openai` | `instrumentation-openai-v*` | instrumentation-openai-v0.3.0 | ## How to release `@elastic/opentelemetry-node` @@ -40,26 +41,32 @@ Assuming "x.y.z" is the release verison: https://github.com/elastic/elastic-otel-node/actions/workflows/release.yml -## How to release `@elastic/mockotlpserver` +## How to release other packages -Assuming "x.y.z" is the release verison: +Assuming: +- **VERSION="x.y.z"** is the release version and +- **PKGSUBDIR="instrumentation-openai"** is the package being released. + (Note that the PKGSUBDIR, the directory under "packages/" is *not necessarily* + the basename of the npm package.) 1. Choose the appropriate version number according to semver. 2. Create a PR with these changes: - - Bump the "version" in "packages/mockotlpserver/package.json". - - Run `npm install` in "packages/mockotlpserver/" to update "packages/mockotlpserver/package-lock.json". - - Update "packages/mockotlpserver/CHANGELOG.md" as necessary. - - Name the PR something like "release @elastic/mockotlpserver@x.y.z". + + - Bump the "version" in "packages/$PKGSUBDIR/package.json". + - Run `npm install` in "packages/$PKGSUBDIR/" to update "packages/$PKGSUBDIR/package-lock.json". + - Update "packages/$PKGSUBDIR/CHANGELOG.md" as necessary. + - Name the PR something like "release $PKGSUBDIR x.y.z". 3. Get the PR approved and merged. 4. Working on the elastic repo (not a fork), tag the commit as follows: + ``` - git tag mockotlpserver-vx.y.z - git push origin mockotlpserver-vx.y.z + git tag $PKGSUBDIR-vx.y.z + git push origin $PKGSUBDIR-vx.y.z ``` - The GitHub Actions "release-mockotlpserver" workflow will handle the release - steps -- including the `npm publish`. See the appropriate run at: - https://github.com/elastic/elastic-otel-node/actions/workflows/release-mockotlpserver.yml + The GitHub Actions "release-$PKGSUBDIR" workflow will handle the release + steps -- including the `npm publish`. See the appropriate run at: + https://github.com/elastic/elastic-otel-node/actions diff --git a/examples/package.json b/examples/package.json index b3606e97..787c0485 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,7 +3,9 @@ "license": "Apache-2.0", "private": true, "version": "0.1.0", - "scripts": {}, + "scripts": { + "clean": "rm -rf node_modules" + }, "dependencies": { "@elastic/opentelemetry-node": "../packages/opentelemetry-node", "@opentelemetry/api": "^1.9.0", diff --git a/package-lock.json b/package-lock.json index 020e1cf5..8b955583 100644 --- a/package-lock.json +++ b/package-lock.json @@ -286,9 +286,10 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.0", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, - "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -1251,12 +1252,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, - "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3195,9 +3197,10 @@ } }, "node_modules/synckit": { - "version": "0.8.8", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, - "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" @@ -3237,9 +3240,10 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "dev": true, - "license": "0BSD" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true }, "node_modules/type-check": { "version": "0.4.0", @@ -3641,7 +3645,9 @@ "optional": true }, "@pkgr/core": { - "version": "0.1.0", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true }, "@types/glob": { @@ -4296,11 +4302,13 @@ } }, "eslint-plugin-prettier": { - "version": "5.1.3", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" } }, "eslint-plugin-promise": { @@ -5435,7 +5443,9 @@ "dev": true }, "synckit": { - "version": "0.8.8", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "requires": { "@pkgr/core": "^0.1.0", @@ -5464,7 +5474,9 @@ } }, "tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, "type-check": { diff --git a/package.json b/package.json index 2a6e507b..6ce0d1d3 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "license": "Apache-2.0", "private": true, "scripts": { + "clean-all": "set -e; ./scripts/oneach.sh npm run --if-present clean", "ci-all": "./scripts/oneach.sh npm ci", - "clean-all": "set -e; rm -rf build; ./scripts/oneach.sh rm -rf node_modules", + "compile-all": "./scripts/oneach.sh npm run --if-present compile", + "clean": "rm -rf node_modules build", "oneach": "./scripts/oneach.sh", "lint": "npm run lint:eslint && ls -d packages/* | while read d; do (cd $d; npm run lint); done", "lint:eslint": "eslint --ext=js,mjs,cjs scripts examples # requires node >=16.0.0", "lint:fix": "eslint --ext=js,mjs,cjs .eslintrc.js scripts examples --fix && ls -d packages/* | while read d; do (cd $d; npm run lint:fix); done", - "test": "ls -d packages/opentelemetry-node | while read d; do (cd $d; npm test); done", "maint:update-protos": "node scripts/update-protos.js" }, "devDependencies": { diff --git a/packages/instrumentation-openai/.editorconfig b/packages/instrumentation-openai/.editorconfig new file mode 100644 index 00000000..e1d79b1a --- /dev/null +++ b/packages/instrumentation-openai/.editorconfig @@ -0,0 +1,12 @@ +# Matches https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/.editorconfig +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/packages/instrumentation-openai/.eslintrc.js b/packages/instrumentation-openai/.eslintrc.js new file mode 100644 index 00000000..31d4a400 --- /dev/null +++ b/packages/instrumentation-openai/.eslintrc.js @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// `npx eslint --print-config index.js` to print calculated config. +module.exports = { + root: true, + env: { + node: true, + es2022: true, // Defines `Promise`, etc. + }, + extends: ['eslint:recommended', 'plugin:prettier/recommended'], + plugins: [ + '@typescript-eslint', + 'import', + 'license-header', + 'prettier', + 'promise', + 'n', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: null, // set in overrides + }, + rules: { + 'license-header/header': ['error', '../../scripts/license-header.js'], + + // Restoring some config from standardjs that we want to maintain at least + // for now -- to assist with transition to prettier. + 'no-unused-vars': [ + // See taav for possible better 'no-unused-vars' rule. + 'error', + { + args: 'none', + caughtErrors: 'none', + ignoreRestSiblings: true, + vars: 'all', + }, + ], + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-constant-condition': [ + 'error', + { + checkLoops: false, + }, + ], + 'n/handle-callback-err': ['error', '^(err|error)$'], + 'n/no-callback-literal': ['error'], + 'n/no-deprecated-api': ['error'], + 'n/no-exports-assign': ['error'], + 'n/no-new-require': ['error'], + 'n/no-path-concat': ['error'], + 'n/process-exit-as-throw': ['error'], + 'promise/param-names': ['error'], + + // Undo this config from eslint:recommended for now (standardjs didn't have it.) + 'require-yield': ['off'], + + 'import/export': 'error', + 'import/first': 'error', + 'import/no-absolute-path': [ + 'error', + { esmodule: true, commonjs: true, amd: false }, + ], + 'import/no-duplicates': 'error', + 'import/no-named-default': 'error', + 'import/no-webpack-loader-syntax': 'error', + }, + ignorePatterns: [ + 'build', + 'scripts/license-header.js', + '*.example.js', // a pattern for uncommited local dev files to avoid linting + '*.example.mjs', // a pattern for uncommited local dev files to avoid linting + 'node_modules', + 'tmp', + ], + overrides: [ + { + files: ['*.ts'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + parserOptions: { + project: './tsconfig.json', + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'memberLike', + modifiers: ['private', 'protected'], + format: ['camelCase'], + leadingUnderscore: 'require', + }, + ], + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-inferrable-types': [ + 'error', + { ignoreProperties: true }, + ], + '@typescript-eslint/no-empty-function': ['off'], + '@typescript-eslint/ban-types': [ + 'warn', + { + types: { + Function: null, + }, + }, + ], + '@typescript-eslint/no-shadow': ['warn'], + 'prefer-rest-params': 'off', + }, + }, + { + files: ['*.mjs'], + parserOptions: { + sourceType: 'module', + }, + }, + ], +}; diff --git a/packages/instrumentation-openai/.gitignore b/packages/instrumentation-openai/.gitignore new file mode 100644 index 00000000..1d4d3843 --- /dev/null +++ b/packages/instrumentation-openai/.gitignore @@ -0,0 +1,5 @@ +/openai.env +/azure.env + +# version.ts is generated during compile. +/src/version.ts diff --git a/packages/instrumentation-openai/.tav.yml b/packages/instrumentation-openai/.tav.yml new file mode 100644 index 00000000..e9043d34 --- /dev/null +++ b/packages/instrumentation-openai/.tav.yml @@ -0,0 +1,5 @@ +openai: + versions: + include: '>=4.19.0 <5' + mode: max-5 + commands: npm test diff --git a/packages/instrumentation-openai/CHANGELOG.md b/packages/instrumentation-openai/CHANGELOG.md new file mode 100644 index 00000000..dbfa8dca --- /dev/null +++ b/packages/instrumentation-openai/CHANGELOG.md @@ -0,0 +1,9 @@ +# @elastic/opentelemetry-instrumentation-openai Changelog + +## v0.2.0 + +- Based on GenAI semantic conventions 1.29. +- Instrumentation of chat completion, including streaming and tool calls. +- Instrumentation of [embeddings creation](https://platform.openai.com/docs/api-reference/embeddings/create). +- Unit tests that test against recorded responses from api.openai.com. +- Integration tests that run against Ollama, OpenAI, and Azure OpenAI. diff --git a/packages/instrumentation-openai/CONTRIBUTING.md b/packages/instrumentation-openai/CONTRIBUTING.md new file mode 100644 index 00000000..ce77b080 --- /dev/null +++ b/packages/instrumentation-openai/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to @elastic/opentelemetry-instrumentation-openai + +Thank you for contributing to this project. +Please start by reading the [CONTRIBUTING.md at the root of this repo](../../CONTRIBUTING.md). +It includes general contribution/development information relevant to all packages in this repo. + + +# Testing + +tl;dr: This runs the unit tests: + +```bash +npm ci +npm run compile + +npm test +``` + +See [TESTING.md](./TESTING.md) for more details, including integration testing, +testing against multiple versions of the OpenAI client, etc. + + +# Dev Notes + +Use `DEBUG=elastic-opentelemetry-instrumentation-openai` for some diagnostic +output from the instrumentation. For example: + +```bash +DEBUG=elastic-opentelemetry-instrumentation-openai npm run example + +# Alternative: +node --env-file ./dev.env ... +``` + +The test scripts in "test/fixtures/" can be run outside of the test suite for +dev/debugging. For example: + +``` +npx @elastic/mockotlpserver # or whatever OTLP endpoint you like to use + +cd test/fixtures +TEST_MODEL_TOOLS=gpt-4o-mini \ + node --env-file ../../openai.env -r ./telemetry.js chat-completion.js +``` + diff --git a/packages/instrumentation-openai/NOTICE.md b/packages/instrumentation-openai/NOTICE.md new file mode 100644 index 00000000..324ee93d --- /dev/null +++ b/packages/instrumentation-openai/NOTICE.md @@ -0,0 +1,2 @@ +@elastic/opentelemetry-instrumentation-openai +Copyright 2024 Elasticsearch B.V. diff --git a/packages/instrumentation-openai/README.md b/packages/instrumentation-openai/README.md new file mode 100644 index 00000000..53634aca --- /dev/null +++ b/packages/instrumentation-openai/README.md @@ -0,0 +1,163 @@ +# Elastic's OpenTelemetry instrumentation for `openai` + +This module, `@elastic/opentelemetry-instrumentation-openai`, provides automatic +instrumentation of [`openai`](https://www.npmjs.com/package/openai), the OpenAI +Node.js client library. + +It attempts to track the [GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/gen-ai). + + +# Status + +Instrumented OpenAI API endpoints: +- :white_check_mark: [Chat](https://platform.openai.com/docs/api-reference/chat) +- :white_check_mark: [Embeddings](https://platform.openai.com/docs/api-reference/embeddings) + + +# Supported versions + +- This instruments the `openai` package in the range: `>=4.19.0 <5`. +- This supports Node.js 18 and later. (`openai@4` currently only tests with Node.js v18.) + + +# Semantic Conventions + +This instrumentation currently implements version 1.29.0 of the GenAI +semantic-conventions: https://opentelemetry.io/docs/specs/semconv/gen-ai/ + + +# Installation + +```bash +npm install @elastic/opentelemetry-instrumentation-openai +``` + + +# Usage + +This example shows the OTel setup code and app code in the same file. +Typically, the OTel setup code would be in a separate file and run via +`node -r ...`. See a more complete example at "test/fixtures/telemetry.js". + +```js +const {NodeSDK, tracing, api} = require('@opentelemetry/sdk-node'); +const {HttpInstrumentation} = require('@opentelemetry/instrumentation-http'); +const {OpenAIInstrumentation} = require('@elastic/opentelemetry-instrumentation-openai'); +const sdk = new NodeSDK({ + spanProcessor: new tracing.SimpleSpanProcessor(new tracing.ConsoleSpanExporter()), + instrumentations: [ + // HTTP instrumentation is not required, but it can be interesting to see + // openai and http spans in the trace. + new HttpInstrumentation(), + new OpenAIInstrumentation({ + // See below for OpenAI instrumentation configuration. + }) + ] +}) +sdk.start(); +process.once('beforeExit', async () => { await sdk.shutdown() }); + +const OpenAI = require('openai'); +async function main() { + const openai = new OpenAI(); + const result = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + {role: 'user', content: 'Say hello world.'} + ] + }); + console.log(result.choices[0]?.message?.content); +} +``` + + +# Examples + +In the "examples/" directory, [use-chat.js](./examples/use-chat.js) is a simple +script using the OpenAI Chat Completion API. First, run the script **without +instrumentation**. + +Using OpenAI: + +```bash +OPENAI_API_KEY=sk-... \ + node use-chat.js +``` + +Using Azure OpenAI (this assumes your Azure OpenAI endpoint has a model +deployment with the name 'gpt-4o-mini'): + +```bash +AZURE_OPENAI_ENDPOINT=https://YOUR-ENDPOINT-NAME.openai.azure.com \ + AZURE_OPENAI_API_KEY=... \ + OPENAI_API_VERSION=2024-10-01-preview \ + node use-chat.js +``` + +Using [Ollama](https://ollama.com) (a tool for running LLMs locally, it exposes +an OpenAI-compatible API): + +```bash +ollama serve + +# When using Ollama, we default to qwen2.5:0.5b, which is a small model. You +# can choose a larger one, or a different tool capable model like mistral-nemo. +export MODEL_CHAT=qwen2.5 +ollama pull $MODEL_CHAT + +OPENAI_BASE_URL=http://localhost:11434/v1 \ + node use-chat.js +``` + +Now, to run **with instrumentation**, you can use [examples/telemetry.js](./test/fixtures/telemetry.js) +to bootstrap the OpenTelemetry SDK using this instrumentation. Add the Node.js +`-r ./telemetry.js` option to bootstrap before the script runs. For example: + +```bash +# Configure the OTel SDK as appropriate for your setup: +export OTEL_EXPORTER_OTLP_ENDPOINT=https://{your-otlp-endpoint.example.com} +export OTEL_EXPORTER_OTLP_HEADERS="Authorization=..." +export OTEL_SERVICE_NAME=my-service + +OPENAI_API_KEY=sk-... \ + node -r ./telemetry.js use-chat.js +``` + + +# Configuration + +| Option | Type | Description | +|-------------------------|-----------|-------------| +| `captureMessageContent` | `boolean` | Enable capture of content data, such as prompt and completion content. Default `false` to avoid possible exposure of sensitive data. `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable overrides. | + + +For example: + +```bash +cd examples +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true \ + OPENAI_API_KEY=sk-... \ + node -r ./telemetry.js use-chat.js +``` + + +# ESM + +OpenTelemetry instrumentation of ECMAScript Module (ESM) code -- code using +`import ...` rather than `require(...)` -- is experimental and very limited. +This section shows that it is possible to get instrumentation of `openai` +working with ESM code. + +```bash +npm install +npm run compile +cd examples +node --import ./telemetry.mjs use-chat-esm.mjs +``` + +See the comments in [examples/telemetry.mjs](./examples/telemetry.mjs) for +limitations with this. The limitations are with OpenTelemetry JS, not with this +instrumentation. + +(TODO: Create and point to a follow-up issue(s) for necessary OTel JS work for this support.) + diff --git a/packages/instrumentation-openai/TESTING.md b/packages/instrumentation-openai/TESTING.md new file mode 100644 index 00000000..d61b5046 --- /dev/null +++ b/packages/instrumentation-openai/TESTING.md @@ -0,0 +1,142 @@ +# Testing this instrumentation + +Before running tests, install dependencies and compile the TypeScript: + +``` +npm ci +npm run compile +``` + +Most commonly, run the unit tests with: + +``` +npm test +``` + +Run the unit tests against multiple supported versions of the `openai` package: + +``` +npm run test-all-versions +``` + +There are also integration tests: + +``` +# Set appropriate envvars... +npm run test:integration +``` + +The integration tests run against a real GenAI service: OpenAI, Azure OpenAI, +or a running Ollama. Which is used is determined by the environment variables. + +1. OpenAI + + ```bash + cp openai.env.template openai.env + vi openai.env # Add your OpenAI credentials. + + set -a; source ./openai.env + npm run test:integration + ``` + +2. Azure OpenAI + + ```bash + cp azure.env.template azure.env + vi azure.env # Add your credentials and resource & deployment details. + + set -a; source ./azure.env + npm run test:integration + ``` + +3. Ollama + + ```bash + # Run the Elastic light fork of Ollama with a few changes added to better + # match OpenAI behavior for some tests. https://github.com/elastic/ollama/tree/testing + docker run -it --rm -p 11434:11434 -v ~/.ollama:/root/.ollama ghcr.io/elastic/ollama/ollama:testing serve + + set -a; source ./ollama.env + ollama pull $TEST_MODEL_TOOLS + ollama pull $TEST_MODEL_EMBEDDINGS + + npm run test:integration + ``` + +# Troubleshooting + +The integration tests automatically decide whether to use the `openai.OpenAI` +or `openai.AzureOpenAI` client class depending on whether the +`AZURE_OPENAI_API_KEY` envvar is set. If you happen to have this in your +environment, it may breaking running integration tests against non-Azure +services. + +# Test notes for maintainers + +Test files are `test/**/*.test.js`. They can be run separately: + +```bash +node test/config.test.js +``` + +or all of them with the driver: + +```bash +./node_modules/.bin/tape test/**/*.test.js +``` + +Really the only rule for a test file is that it exits non-zero to mean failure. + +## Regenerating recorded responses + +The unit tests include tests run against OpenAI, but with Node's http modules +mocked out (by `nock`) to return pre-recorded HTTP responses. Those are +stored in "test/fixtures/nock-recordings". When updating tests or adding new +ones, those recordings might need to be regenerated. This runs against +api.openai.com, so you must have valid OpenAI auth set in your environment. + +```bash +cp openai.env.template openai.env +vi openai.env # Add your OpenAI credentials. + +set -a; source openai.env +npm run test:regenerate-recordings +``` + +## Filtering which fixture tests are run + +The bulk of the tests are in "test/fixtures.test.js". This is one big table +test. The set of fixture tests run can be filtered via +`TEST_FIXTURES_FILTER=` to help with a quicker dev/test cycle. +For example: + +```bash +TEST_FIXTURES_FILTER=embeddings npm test +TEST_FIXTURES_FILTER=stream npm run test:openai +``` + +## Limitations + +The nock record/replay has some limitations: + +- If you look at the recordings, they are *partly* readable. However the + response looks something like this: + + ``` + "response": [ + "1f8b080000000000000...ca77f434fb3851d2fd86474", + "525af05ed4c08bf3106...ffff03007937b48a44020000" + ], + ``` + + This is because nock has received a response with `Content-Encoding: gzip`. + In this case, nock does not decompress the response. The result is that the + stored recordings are somewhat opaque. This is not a blocker, but is + unfortunate. Fixes have been proposed (e.g. + https://github.com/nock/nock/pull/2359 and links from that PR), but have not + been merged into nock. + +- Nock's monkey-patching of `http` breaks `@opentelemetry/instrumentation-http`, + so when testing is using Nock (i.e. the unit tests), the test assertions + need to *not* expect any HTTP spans. + diff --git a/packages/instrumentation-openai/azure.env.template b/packages/instrumentation-openai/azure.env.template new file mode 100644 index 00000000..61c45e83 --- /dev/null +++ b/packages/instrumentation-openai/azure.env.template @@ -0,0 +1,37 @@ +# To run the integration tests against the Azure OpenAI service requires +# a few things: +# - An Azure OpenAI endpoint and API key. +# - That endpoint needs to be configured with two deployments, one with a GenAI +# model that supports tool calls, e.g. "gpt-4o-mini", and one that supports +# embeddings, e.g. "test-embedding-3-small". +# +# One option is to: +# +# 1. copy this template to "azure.env" and fill in the variables below, then +# 2. set `TEST_FIXTURES_ENV_FILE=./azure.env`. This envvar is loaded by the +# test file that runs the integration tests. +# +# Then this will run the integration tests against Azure OpenAI: +# TEST_FIXTURES_ENV_FILE=./azure.env npm run test:integration + +# For tests to pass, the Azure OpenAI resource pointed to by +# AZURE_OPENAI_ENDPOINT needs to have deployments with the same names as the +# `openaiChatModel` and `openaiEmbeddingsModel` values in +# "test/testconfig.json". +AZURE_OPENAI_ENDPOINT=https://YOUR_RESOURCE_NAME.openai.azure.com +AZURE_OPENAI_API_KEY=... + +# Using `openai.AzureOpenAI` requires the API version to be set. You can +# use this or a different value. +# https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation +OPENAI_API_VERSION=2024-10-01-preview + +# Set this to the *deployment name* in your Azure OpenAI endpoint that deployed +# a model that supports OpenAI tool calling, such as "gpt-4o-mini". +TEST_MODEL_TOOLS=YOUR_TOOL_COMPATIBLE_DEPLOYMENT_NAME + +# Set this to the *deployment name* in your Azure OpenAI endpoint that deployed +# a model that supports OpenAI embeddings, such as "text-embedding-3-small". +TEST_MODEL_EMBEDDINGS=YOUR_EMBEDDING_DEPLOYMENT_NAME + + diff --git a/packages/instrumentation-openai/dev.env b/packages/instrumentation-openai/dev.env new file mode 100644 index 00000000..5cc33ba3 --- /dev/null +++ b/packages/instrumentation-openai/dev.env @@ -0,0 +1,8 @@ +# Some environment variables that might be useful for development of this +# instrumentation. It is not intended for general use. + +# This enables internal debug logging from this instrumentation. +DEBUG=elastic-opentelemetry-instrumentation-openai + +# Enable the `captureMessageContent` instrumentation config option. +OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true diff --git a/packages/instrumentation-openai/examples/openai.js b/packages/instrumentation-openai/examples/openai.js new file mode 100644 index 00000000..c63ea2da --- /dev/null +++ b/packages/instrumentation-openai/examples/openai.js @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { OpenAI, AzureOpenAI } = require('openai'); + +const OLLAMA_PORT = '11434'; + +/** + * Returns an OpenAI client and model to use with it, based on env variables. + * + * @typedef {Object} OpenAIAndModels + * @property {OpenAI} client - The OpenAI client instance. + * @property {string} chatModel - The chat model name. + * @property {string} embeddingsModel - The embeddings model name. + * @returns {OpenAIAndModels} Contains the OpenAI client and models to use. + */ +function newOpenAIAndModels() { + let clientCtor = OpenAI; + // Default to models available in both the OpenAI platform and Azure OpenAI + // Service. For Azure, however, this "model" must match the Azure "deployment + // name". + let chatModel = process.env.MODEL_CHAT ?? 'gpt-4o-mini'; + let embeddingsModel = + process.env.MODEL_EMBEDDINGS ?? 'text-embedding-3-small'; + + if (process.env.AZURE_OPENAI_API_KEY) { + clientCtor = AzureOpenAI; + } else if ( + process.env.OPENAI_BASE_URL && + new URL(process.env.OPENAI_BASE_URL).port === OLLAMA_PORT + ) { + process.env.OPENAI_API_KEY = 'unused'; + // Note: Others like LocalAI do not use Ollama's naming scheme. + chatModel = process.env.MODEL_CHAT ?? 'qwen2.5:0.5b'; + embeddingsModel = process.env.MODEL_EMBEDDINGS ?? 'all-minilm:33m'; + } + + return { client: new clientCtor(), chatModel, embeddingsModel }; +} + +module.exports = { newOpenAIAndModels }; diff --git a/packages/instrumentation-openai/examples/telemetry.js b/packages/instrumentation-openai/examples/telemetry.js new file mode 100644 index 00000000..6754d85c --- /dev/null +++ b/packages/instrumentation-openai/examples/telemetry.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Re-use the OTel setup module being used for tests. +require('../test/fixtures/telemetry.js'); diff --git a/packages/instrumentation-openai/examples/telemetry.mjs b/packages/instrumentation-openai/examples/telemetry.mjs new file mode 100644 index 00000000..68eaace6 --- /dev/null +++ b/packages/instrumentation-openai/examples/telemetry.mjs @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// OpenTelemetry bootstrap code, suitable for use with `--import`, e.g.: +// node --import ./telemetry.mjs app.js +// +// **Experimental**: This uses the new `createAddHookMessageChannel` functionality +// from import-in-the-middle. `@opentelemetry/instrumentation` needs some work +// to support using that new functionality. As well, if an app has multiple +// import-in-the-middle installations in the `node_modules/...` tree, then things +// are likely to break. + +import os from 'os'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'; +import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { OpenAIInstrumentation } from '../build/src/index.js'; // @elastic/opentelemetry-instrumentation-openai +import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; + +import { register } from 'module'; +// TODO: @opentelemetry/instrumentation should re-export this IITM method. +import { createAddHookMessageChannel } from 'import-in-the-middle'; + +// const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api'); +// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + +// Note: If there are *multiple* installations of import-in-the-middle, then +// only those instrumentations using this same one will be hooked. +const { registerOptions, waitForAllMessagesAcknowledged } = + createAddHookMessageChannel(); +// TODO: `@opentelemetry/instrumentation/hook.mjs` needs to re-export initialize +// register('@opentelemetry/instrumentation/hook.mjs', import.meta.url, registerOptions); +register('import-in-the-middle/hook.mjs', import.meta.url, registerOptions); + +// At time of writing, NodeSDK does not automatically provide a LoggerProvider +// so we must manually pass one in. Note that this simple implementation does +// not respond to the OTel specified OTEL_EXPORTER_OTLP_LOGS_PROTOCOL and +// OTEL_EXPORTER_OTLP_PROTOCOL envvars. +const logRecordProcessor = new BatchLogRecordProcessor(new OTLPLogExporter()); + +const metricReader = new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), +}); + +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter(), + logRecordProcessor, + metricReader, + instrumentations: [ + new HttpInstrumentation(), + new OpenAIInstrumentation({ + captureMessageContent: true, + }), + ], +}); + +process.on('SIGTERM', async () => { + // Force at least one metrics export, if script run was shorter than metrics interval. + await metricReader.forceFlush(); + try { + await sdk.shutdown(); + } catch (err) { + console.warn('warning: error shutting down OTel SDK', err); + } + process.exit(128 + os.constants.signals.SIGTERM); +}); + +process.once('beforeExit', async () => { + // Force at least one metrics export, if script run was shorter than metrics interval. + await metricReader.forceFlush(); + // Flush recent telemetry data if about to shutdown. + try { + await sdk.shutdown(); + } catch (err) { + console.warn('warning: error shutting down OTel SDK', err); + } +}); + +sdk.start(); + +// Ensure that the loader has acknowledged all the modules before we allow +// execution to continue. +await waitForAllMessagesAcknowledged(); diff --git a/packages/instrumentation-openai/examples/use-chat-esm.mjs b/packages/instrumentation-openai/examples/use-chat-esm.mjs new file mode 100644 index 00000000..0dabecaf --- /dev/null +++ b/packages/instrumentation-openai/examples/use-chat-esm.mjs @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// An example using `openai` in ESM code. +// See ESM section in README for details and limitations. +// +// Usage with OpenAI: +// OPENAI_API_KEY=... \ +// node -r ./telemetry.mjs use-chat-esm.mjs + +// Dev Note: Not using local ./openai.js utility for now, because CommonJS. +import { OpenAI } from 'openai'; + +const CHAT_MODEL = process.env.CHAT_MODEL || 'gpt-4o-mini'; + +async function main() { + const client = new OpenAI(); + try { + const chatCompletion = await client.chat.completions.create({ + model: CHAT_MODEL, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + }); + console.log(chatCompletion.choices[0].message.content); + } catch (err) { + console.log('chat err:', err); + process.exitCode = 1; + } +} + +main(); diff --git a/packages/instrumentation-openai/examples/use-chat.js b/packages/instrumentation-openai/examples/use-chat.js new file mode 100644 index 00000000..fb5a6d7e --- /dev/null +++ b/packages/instrumentation-openai/examples/use-chat.js @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// A very basic example using the OpenAI client. +// +// Usage with OpenAI: +// OPENAI_API_KEY=... \ +// node -r ./telemetry.js use-chat.js +// +// Usage with a local Ollama server: +// ollama serve +// ollama pull qwen2.5:0.5b +// OPENAI_BASE_URL=http://localhost:11434/v1 OPENAI_API_KEY=unused \ +// node -r ./telemetry.js use-chat.js + +const { newOpenAIAndModels } = require('./openai'); + +async function main() { + const { client, chatModel } = newOpenAIAndModels(); + try { + const chatCompletion = await client.chat.completions.create({ + model: chatModel, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + }); + console.log(chatCompletion.choices[0].message.content); + } catch (err) { + console.log('chat err:', err); + process.exitCode = 1; + } +} + +main(); diff --git a/packages/instrumentation-openai/examples/use-embeddings.js b/packages/instrumentation-openai/examples/use-embeddings.js new file mode 100644 index 00000000..cb41a52e --- /dev/null +++ b/packages/instrumentation-openai/examples/use-embeddings.js @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Usage with OpenAI: +// OPENAI_API_KEY=... \ +// node -r ./telemetry.js use-embeddings.js + +const { newOpenAIAndModels } = require('./openai'); + +async function main() { + const { client, embeddingsModel } = newOpenAIAndModels(); + const embedding = await client.embeddings.create({ + model: embeddingsModel, + input: 'The quick brown fox jumped over the lazy dog', + encoding_format: 'float', + }); + console.log('Embeddings:'); + console.dir(embedding, { depth: 50 }); +} + +main(); diff --git a/packages/instrumentation-openai/examples/use-stream-with-parallel-tool-calls.js b/packages/instrumentation-openai/examples/use-stream-with-parallel-tool-calls.js new file mode 100644 index 00000000..7f34a953 --- /dev/null +++ b/packages/instrumentation-openai/examples/use-stream-with-parallel-tool-calls.js @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const assert = require('assert/strict'); +const OpenAI = require('openai'); + +const MODEL = + process.env.MODEL || + (process.env.OPENAI_BASE_URL ? 'qwen2.5:0.5b' : 'gpt-4o-mini'); +const tools = [ + { + type: 'function', + function: { + name: 'get_weather', + strict: true, + parameters: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + required: ['location'], + additionalProperties: false, + }, + }, + }, +]; + +async function main() { + const client = new OpenAI(); + const messages = [ + { + role: 'system', + content: 'You are a helpful assistant providing weather updates.', + }, + { role: 'user', content: 'What is the weather in New York and London?' }, + ]; + const stream = await client.chat.completions.create({ + model: MODEL, + messages: messages, + stream: true, + stream_options: { include_usage: true }, + tools: tools, + }); + + let n = 0; + let mode = null; + function switchMode(newMode) { + if (mode === 'content' && newMode !== 'content') { + process.stdout.write('\n'); + } else if (mode === 'tool_call') { + process.stdout.write(')\n'); + } + mode = newMode; + } + + for await (const chunk of stream) { + // console.log('\n'); console.dir(chunk, { depth: 50 }); // debug print + if (n === 0) { + console.log('# response id:', chunk.id); + console.log('# response model:', chunk.model); + } + if (chunk.usage) { + // A final chunk included iff `stream_options.include_usage`. + console.log('# usage:', chunk.usage); + continue; + } + assert.equal(chunk.choices.length, 1); + const delta = chunk.choices[0].delta; + if (chunk.choices[0].finish_reason) { + switchMode(null); + console.log('# finish reason:', chunk.choices[0].finish_reason); + } else if (delta.role === 'assistant') { + if (typeof delta.content === 'string') { + switchMode('content'); + process.stdout.write(delta.content); + } + } else if (delta.tool_calls) { + assert.equal(delta.tool_calls.length, 1); + const tc = delta.tool_calls[0]; + if (tc.id) { + // First chunk in a tool call. + assert.equal(tc.type, 'function'); + switchMode('tool_call'); + console.log('# tool call id:', tc.id); + process.stdout.write(tc.function.name + '('); + } + process.stdout.write(tc.function.arguments); + } else { + throw new Error(`unexpected chunk: ${JSON.stringify(chunk)}`); + } + n++; + } +} + +main(); diff --git a/packages/instrumentation-openai/ollama.env b/packages/instrumentation-openai/ollama.env new file mode 100644 index 00000000..678e06d1 --- /dev/null +++ b/packages/instrumentation-openai/ollama.env @@ -0,0 +1,8 @@ +# Env to run the integration tests against a local Ollama. +OPENAI_BASE_URL=http://127.0.0.1:11434/v1 +OPENAI_API_KEY=notused + +# These models may be substituted in the future with inexpensive to run, newer +# variants. +TEST_MODEL_TOOLS=qwen2.5:0.5b +TEST_MODEL_EMBEDDINGS=all-minilm:33m diff --git a/packages/instrumentation-openai/openai.env.template b/packages/instrumentation-openai/openai.env.template new file mode 100644 index 00000000..1db83b58 --- /dev/null +++ b/packages/instrumentation-openai/openai.env.template @@ -0,0 +1,24 @@ +# Some testing requires using the OpenAI service (api.openai.com): +# +# - `npm run test:regenerate-recordings` uses OpenAI to record responses to +# specific requests. These recordings are used in subsequent unit test runs +# (`npm test`). +# - to run integration tests, `npm run test:integration`, against the OpenAI +# service. +# +# These require OpenAI credentials. One option is to: +# +# 1. copy this template to "openai.env" and fill in your auth details, then +# 2. set `TEST_FIXTURES_ENV_FILE=./openai.env`. This envvar is loaded by the +# test file that runs the integration tests. +# +# Then this will run the integration tests against OpenAI: +# TEST_FIXTURES_ENV_FILE=./openai.env npm run test:integration + +OPENAI_API_KEY=sk-... +# OpenAI org and project IDs, if applicable. +# OPENAI_ORG_ID=org-... +# OPENAI_PROJECT_ID=... + +TEST_MODEL_TOOLS=gpt-4o-mini +TEST_MODEL_EMBEDDINGS=text-embedding-3-small diff --git a/packages/instrumentation-openai/package-lock.json b/packages/instrumentation-openai/package-lock.json new file mode 100644 index 00000000..bf9bb5be --- /dev/null +++ b/packages/instrumentation-openai/package-lock.json @@ -0,0 +1,4981 @@ +{ + "name": "@elastic/opentelemetry-instrumentation-openai", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@elastic/opentelemetry-instrumentation-openai", + "version": "0.3.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.56.0", + "@opentelemetry/instrumentation": "^0.56.0", + "debug": "^4.3.6" + }, + "devDependencies": { + "@elastic/mockotlpserver": "^0.5.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.56.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.56.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.56.0", + "@opentelemetry/instrumentation-http": "^0.56.0", + "@opentelemetry/sdk-logs": "^0.56.0", + "@opentelemetry/sdk-metrics": "^1.29.0", + "@opentelemetry/sdk-node": "^0.56.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "@types/debug": "^4.1.12", + "@types/glob": "^5.0.38", + "@types/node": "18.18.14", + "@typescript-eslint/eslint-plugin": "5.8.1", + "@typescript-eslint/parser": "5.8.1", + "dotenv": "^16.4.5", + "nock": "^13.5.5", + "openai": "^4.57.0", + "tape": "^5.8.1", + "test-all-versions": "^6.1.0", + "typescript": "4.4.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@elastic/mockotlpserver": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@elastic/mockotlpserver/-/mockotlpserver-0.5.0.tgz", + "integrity": "sha512-g0Gr73YkUtv/jSPnK44s5NMalcn0x9NPe4KdTdvCeQf2AXBiiRkDLL0ikPNdBKSRhsP1GhHjG02ngNGRlBGWag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.12", + "@opentelemetry/core": "^1.26.0", + "dashdash": "^2.0.0", + "long": "^5.2.3", + "protobufjs": "^7.4.0", + "safe-stable-stringify": "^2.5.0" + }, + "bin": { + "mockotlpserver": "lib/cli.js" + }, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.4.tgz", + "integrity": "sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@ljharb/resumer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@ljharb/resumer/-/resumer-0.1.3.tgz", + "integrity": "sha512-d+tsDgfkj9X5QTriqM4lKesCkMMJC3IrbPKHvayP00ELx2axdXvDfWkqjxrLXIzGcQzmj7VAUT1wopqARTvafw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.13", + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.56.0.tgz", + "integrity": "sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.29.0.tgz", + "integrity": "sha512-TKT91jcFXgHyIDF1lgJF3BHGIakn6x0Xp7Tq3zoS3TMPzT9IlP0xEavWP8C1zGjU9UmZP2VR1tJhW9Az1A3w8Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.29.0.tgz", + "integrity": "sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.56.0.tgz", + "integrity": "sha512-/ef8wcphVKZ0uI7A1oqQI/gEMiBUlkeBkM9AGx6AviQFIbgPVSdNK3+bHBkyq5qMkyWgkeQCSJ0uhc5vJpf0dw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/sdk-logs": "0.56.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.56.0.tgz", + "integrity": "sha512-gN/itg2B30pa+yAqiuIHBCf3E77sSBlyWVzb+U/MDLzEMOwfnexlMvOWULnIO1l2xR2MNLEuPCQAOrL92JHEJg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/sdk-logs": "0.56.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.56.0.tgz", + "integrity": "sha512-MaO+eGrdksd8MpEbDDLbWegHc3w6ualZV6CENxNOm3wqob0iOx78/YL2NVIKyP/0ktTUIs7xIppUYqfY3ogFLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-logs": "0.56.0", + "@opentelemetry/sdk-trace-base": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.56.0.tgz", + "integrity": "sha512-GD5QuCT6js+mDpb5OBO6OSyCH+k2Gy3xPHJV9BnjV8W6kpSuY8y2Samzs5vl23UcGMq6sHLAbs+Eq/VYsLMiVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-metrics": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.56.0.tgz", + "integrity": "sha512-1FZvTmgIts5crkVIETIpIJ9Gyp7dFqgNWeZmzAzmYzWBX2QBK9fdvxs9ZWbLFKR1j9nN0Urh/w/J+lDJgbSGNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.56.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-metrics": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.56.0.tgz", + "integrity": "sha512-9hRHue78CV2XShAt30HadBK8XEtOBiQmnkYquR1RQyf2RYIdJvhiypEZ+Jh3NGW8Qi14icTII/1oPTQlhuyQdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.56.0.tgz", + "integrity": "sha512-vqVuJvcwameA0r0cNrRzrZqPLB0otS+95g0XkZdiKOXUo81wYdY6r4kyrwz4nSChqTBEFm0lqi/H2OWGboOa6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.56.0.tgz", + "integrity": "sha512-UYVtz8Kp1QZpZFg83ZrnwRIxF2wavNyi1XaIKuQNFjlYuGCh8JH4+GOuHUU4G8cIzOkWdjNR559vv0Q+MCz+1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.29.0.tgz", + "integrity": "sha512-9wNUxbl/sju2AvA3UhL2kLF1nfhJ4dVJgvktc3hx80Bg/fWHvF6ik4R3woZ/5gYFqZ97dcuik0dWPQEzLPNBtg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.56.0.tgz", + "integrity": "sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.56.0.tgz", + "integrity": "sha512-/bWHBUAq8VoATnH9iLk5w8CE9+gj+RgYSUphe7hry472n6fYl7+4PvuScoQMdmSUTprKq/gyr2kOWL6zrC7FkQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/instrumentation": "0.56.0", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.56.0.tgz", + "integrity": "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-transformer": "0.56.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.56.0.tgz", + "integrity": "sha512-QqM4si8Ew8CW5xVk4mYbfusJzMXyk6tkYA5SI0w/5NBxmiZZaYPwQQ2cu58XUH2IMPAsi71yLJVJQaWBBCta0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/otlp-exporter-base": "0.56.0", + "@opentelemetry/otlp-transformer": "0.56.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.56.0.tgz", + "integrity": "sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-logs": "0.56.0", + "@opentelemetry/sdk-metrics": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.29.0.tgz", + "integrity": "sha512-ktsNDlqhu+/IPGEJRMj81upg2JupUp+SwW3n1ZVZTnrDiYUiMUW41vhaziA7Q6UDhbZvZ58skDpQhe2ZgNIPvg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.29.0.tgz", + "integrity": "sha512-EXIEYmFgybnFMijVgqx1mq/diWwSQcd0JWVksytAVQEnAiaDvP45WuncEVQkFIAC0gVxa2+Xr8wL5pF5jCVKbg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.29.0.tgz", + "integrity": "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.56.0.tgz", + "integrity": "sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/resources": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.29.0.tgz", + "integrity": "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/resources": "1.29.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.56.0.tgz", + "integrity": "sha512-FOY7tWboBBxqftLNHPJFmDXo9fRoPd2PlzfEvSd6058BJM9gY4pWCg8lbVlu03aBrQjcfCTAhXk/tz1Yqd/m6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.56.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.56.0", + "@opentelemetry/exporter-logs-otlp-http": "0.56.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.56.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.56.0", + "@opentelemetry/exporter-trace-otlp-http": "0.56.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.56.0", + "@opentelemetry/exporter-zipkin": "1.29.0", + "@opentelemetry/instrumentation": "0.56.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/sdk-logs": "0.56.0", + "@opentelemetry/sdk-metrics": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0", + "@opentelemetry/sdk-trace-node": "1.29.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.29.0.tgz", + "integrity": "sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.29.0", + "@opentelemetry/resources": "1.29.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.29.0.tgz", + "integrity": "sha512-ZpGYt+VnMu6O0SRKzhuIivr7qJm3GpWnTCMuJspu4kt3QWIpIenwixo5Vvjuu3R4h2Onl/8dtqAiPIs92xd5ww==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.29.0", + "@opentelemetry/core": "1.29.0", + "@opentelemetry/propagator-b3": "1.29.0", + "@opentelemetry/propagator-jaeger": "1.29.0", + "@opentelemetry/sdk-trace-base": "1.29.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/glob": { + "version": "5.0.38", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.38.tgz", + "integrity": "sha512-rTtf75rwyP9G2qO5yRpYtdJ6aU1QqEhWbtW55qEgquEDa6bXW0s2TWZfDm02GuppjEozOWG/F2UnPq5hAQb+gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "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/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, + "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" + } + }, + "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/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, + "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 + } + } + }, + "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/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 + } + } + }, + "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/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/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/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/@ungap/structured-clone": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/after-all-results": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/after-all-results/-/after-all-results-2.0.0.tgz", + "integrity": "sha512-2zHEyuhSJOuCrmas9YV0YL/MFCWLxe1dS6k/ENhgYrb/JqyMnadLN4iIAc9kkZrbElMDyyAGH/0J18OPErOWLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-diff-stream": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ansi-diff-stream/-/ansi-diff-stream-1.2.1.tgz", + "integrity": "sha512-PaKs34INoKpTzcjyKd2GM/CCEeTyDgWKuHSgF0z7ywjpbBFj/pzQf/30v+TR6VBBLia6Mso+W2ygU22ljqbi6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0", + "buffer-from": "^1.0.0", + "through2": "^2.0.1" + }, + "bin": { + "ansi-diff-stream": "bin.js" + } + }, + "node_modules/ansi-diff-stream/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.every": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/array.prototype.every/-/array.prototype.every-1.1.6.tgz", + "integrity": "sha512-gNEqZD97w6bfQRNmHkFv7rNnGM+VWyHZT+h/rf9C+22owcXuENr66Lfo0phItpU5KoXW6Owb34q2+8MnSIZ57w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "license": "MIT" + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha512-jyweV/k0rbv2WK4r9KLayuBrSh2Py0tNmV7LBoSMH4hMQyrG8OPyIOWB2VEx4DJKXWmK4lopYMVvORlDt2S8Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dashdash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-2.0.0.tgz", + "integrity": "sha512-ElMoAPlrzmF4l0OscF5pPBZv8LhUJBnwh7rHKllUOrwabAr47R1aQIIwC53rc59ycCb7k5Sj1/es+A3Bep/x5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=10.x" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.4" + }, + "bin": { + "ignored": "bin/ignored" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.23.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.5.tgz", + "integrity": "sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "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/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-dynamic-import": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-dynamic-import/-/has-dynamic-import-2.1.0.tgz", + "integrity": "sha512-su0anMkNEnJKZ/rB99jn3y6lV/J8Ro96hBJ28YAeVzj5rWxH+YL/AdCyiYYA1HDLV9YhmvqpWSJJj2KLo1MX6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.3.tgz", + "integrity": "sha512-tNpKEb4AjZrCyrxi+Eyu43h5ig0O8ZRFSXPHh/00/o+4P4pKzVEW/m5lsVtsAT7fCIgmQOAPjdqecGDsBXRxsw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.0.tgz", + "integrity": "sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.0.tgz", + "integrity": "sha512-qfMdqbAQEwBw78ZyReKnlA8ezmPdb9BemzIIip/JkjaZUhitfXDkkr+3QTboW0JrSXT1QWyYShpvnNHGZ4c4yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.0.tgz", + "integrity": "sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.0.tgz", + "integrity": "sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "gopd": "^1.1.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.0.tgz", + "integrity": "sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.0.tgz", + "integrity": "sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "has-symbols": "^1.0.3", + "safe-regex-test": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mock-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mock-property/-/mock-property-1.1.0.tgz", + "integrity": "sha512-1/JjbLoGwv87xVsutkX0XJc0M0W4kb40cZl/K41xtTViBOD9JuFPKfyMNTrLJ/ivYAd0aPqu/vduamXO0emTFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "functions-have-names": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.2", + "isarray": "^2.0.5", + "object-inspect": "^1.13.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "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==", + "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", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm-package-versions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-package-versions/-/npm-package-versions-1.0.1.tgz", + "integrity": "sha512-iEiETp11ZuiWKAlLRBaUD3zKjZTzAco3xrkCbIJaxs+iu2+zIbaKXdVpzmsjyDCe0r7IpDW55iAyVHzktTg5DA==", + "dev": true, + "license": "MIT" + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "4.76.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.76.1.tgz", + "integrity": "sha512-ci63/WFEMd6QjjEVeH0pV7hnFS6CCqhgJydSti4Aak/8uo2SpgzKjteUDaY+OkwziVj11mi6j+0mRUIiGKUzWw==", + "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": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-env-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-env-string/-/parse-env-string-1.0.1.tgz", + "integrity": "sha512-nozPS2x3SzOI6K2TUeeK/KwxMcg22SpAYVY75JeOXHu0M33L+Zo38NkNBlvUqajGwBGKYEbVTI4WemZJFo/OAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.8.tgz", + "integrity": "sha512-B5dj6usc5dkk8uFliwjwDHM8To5/QwdKz9JcBZ8Ic4G1f0YmeeJTtE/ZTdgRFPAfxZFiUaPhZ1Jcs4qeagItGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "dunder-proto": "^1.0.0", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.2.0", + "which-builtin-type": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.4.0.tgz", + "integrity": "sha512-X34iHADNbNDfr6OTStIAHWSAvvKQRYgLO6duASaVf7J2VA3lvmNYboAHOuLC2huav1IwgZJtyEcJCKVzFxOSMQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-npm-install": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/spawn-npm-install/-/spawn-npm-install-1.3.0.tgz", + "integrity": "sha512-5iW1s+ez/NImhEfdO5mGJ27Yy+8hkn6oT5RGItw45cWvp2KlJQ1IC+F6PyurH4xv4Qn3UkWRKUXTYHSAsSBqug==", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^4.0.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/tape/-/tape-5.9.0.tgz", + "integrity": "sha512-czbGgxSVwRlbB3Ly/aqQrNwrDAzKHDW/kVXegp4hSFmR2c8qqm3hCgZbUy1+3QAQFGhPDG7J56UsV1uNilBFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/resumer": "^0.1.3", + "@ljharb/through": "^2.3.13", + "array.prototype.every": "^1.1.6", + "call-bind": "^1.0.7", + "deep-equal": "^2.2.3", + "defined": "^1.0.1", + "dotignore": "^0.1.2", + "for-each": "^0.3.3", + "get-package-type": "^0.1.0", + "glob": "^7.2.3", + "has-dynamic-import": "^2.1.0", + "hasown": "^2.0.2", + "inherits": "^2.0.4", + "is-regex": "^1.1.4", + "minimist": "^1.2.8", + "mock-property": "^1.1.0", + "object-inspect": "^1.13.2", + "object-is": "^1.1.6", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "resolve": "^2.0.0-next.5", + "string.prototype.trim": "^1.2.9" + }, + "bin": { + "tape": "bin/tape" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tape/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-all-versions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/test-all-versions/-/test-all-versions-6.1.0.tgz", + "integrity": "sha512-yHToQ8ydEEFLu7ockssvozXofpUn6KDU5l+XpblsDuOtZz+KZL8TsdUvoUEpBDa/p7lzubI62LFjh1fB8yWeiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "after-all-results": "^2.0.0", + "ansi-diff-stream": "^1.2.1", + "cli-spinners": "^2.9.2", + "deepmerge": "^4.3.1", + "import-fresh": "^3.3.0", + "is-ci": "^3.0.1", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimist": "^1.2.8", + "npm-package-versions": "^1.0.1", + "once": "^1.4.0", + "parse-env-string": "^1.0.1", + "resolve": "^1.22.8", + "semver": "^7.5.4", + "spawn-npm-install": "^1.2.0", + "which": "^2.0.2" + }, + "bin": { + "tav": "index.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.3.tgz", + "integrity": "sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "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": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.0.tgz", + "integrity": "sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.0", + "is-number-object": "^1.1.0", + "is-string": "^1.1.0", + "is-symbol": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.0.tgz", + "integrity": "sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz", + "integrity": "sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/instrumentation-openai/package.json b/packages/instrumentation-openai/package.json new file mode 100644 index 00000000..a3eb83ee --- /dev/null +++ b/packages/instrumentation-openai/package.json @@ -0,0 +1,89 @@ +{ + "name": "@elastic/opentelemetry-instrumentation-openai", + "version": "0.3.0", + "description": "OpenTelemetry instrumentation for the `openai` OpenAI client library", + "type": "commonjs", + "publishConfig": { + "access": "public", + "provenance": true + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/elastic/elastic-otel-node.git", + "directory": "packages/instrumentation-openai" + }, + "keywords": [ + "elastic", + "instrumentation", + "opentelemetry", + "observability", + "openai", + "llm", + "genai" + ], + "author": "Elastic Observability ", + "engines": { + "node": ">=18" + }, + "scripts": { + "clean": "rm -rf node_modules build", + "precompile": "./scripts/gen-version-ts.js", + "compile": "tsc -p .", + "prewatch": "npm run precompile", + "watch": "tsc -w", + "prepublishedOnly": "npm run compile", + "example": "node --env-file ./openai.env -r ./test/fixtures/telemetry.js examples/use-chat.js", + "lint": "npm run lint:eslint", + "lint:eslint": "eslint --ext=js,mjs,cjs,ts .", + "lint:fix": "eslint --ext=js,mjs,cjs,ts --fix .", + "lint:eslint-nostyle": "eslint --ext=js,mjs,cjs,ts --rule 'prettier/prettier: off' . # lint without checking style, not normally used", + "test": "npm run test:unit", + "test:unit": "tape test/**/*.test.js", + "test:regenerate-recordings": "TEST_FIXTURES_MODE=regenerate-recordings node test/fixtures.test.js", + "test-all-versions": "tav", + "test:integration": "TEST_FIXTURES_MODE=integration node test/fixtures.test.js", + "test:integration-ollama": "TEST_FIXTURES_ENV_FILE=./ollama.env npm run test:integration", + "test:integration-openai": "TEST_FIXTURES_ENV_FILE=./openai.env npm run test:integration", + "test:integration-azure": "TEST_FIXTURES_ENV_FILE=./azure.env npm run test:integration", + "test:all-integration-tests": "npm run test:integration-openai && npm run test:integration-azure && npm run test:integration-ollama" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts" + ], + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "dependencies": { + "@opentelemetry/api-logs": "^0.56.0", + "@opentelemetry/instrumentation": "^0.56.0", + "debug": "^4.3.6" + }, + "devDependencies": { + "@elastic/mockotlpserver": "^0.5.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.56.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.56.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.56.0", + "@opentelemetry/instrumentation-http": "^0.56.0", + "@opentelemetry/sdk-logs": "^0.56.0", + "@opentelemetry/sdk-metrics": "^1.29.0", + "@opentelemetry/sdk-node": "^0.56.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "@types/debug": "^4.1.12", + "@types/glob": "^5.0.38", + "@types/node": "18.18.14", + "@typescript-eslint/eslint-plugin": "5.8.1", + "@typescript-eslint/parser": "5.8.1", + "dotenv": "^16.4.5", + "nock": "^13.5.5", + "openai": "^4.57.0", + "tape": "^5.8.1", + "test-all-versions": "^6.1.0", + "typescript": "4.4.4" + } +} diff --git a/packages/instrumentation-openai/prettier.config.js b/packages/instrumentation-openai/prettier.config.js new file mode 100644 index 00000000..d22a2f1d --- /dev/null +++ b/packages/instrumentation-openai/prettier.config.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Matches https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/prettier.config.js +module.exports = { + arrowParens: 'avoid', + printWidth: 80, + trailingComma: 'es5', + tabWidth: 2, + semi: true, + singleQuote: true, +}; diff --git a/packages/instrumentation-openai/scripts/gen-version-ts.js b/packages/instrumentation-openai/scripts/gen-version-ts.js new file mode 100755 index 00000000..d2d2da4f --- /dev/null +++ b/packages/instrumentation-openai/scripts/gen-version-ts.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Based on opentelemetry-js-contrib/scripts/version-update.js + +const fs = require('fs'); +const path = require('path'); + +const TOP = path.resolve(__dirname, '..'); +const header = fs.readFileSync( + path.resolve(TOP, '..', '..', 'scripts', 'license-header.js'), + 'utf8' +); +const pj = require(path.resolve(__dirname, '..', 'package.json')); + +const content = `${header} +// This file is autogenerated, see scripts/gen-version-ts.js +export const PACKAGE_VERSION = '${pj.version}'; +export const PACKAGE_NAME = '${pj.name}'; +`; + +fs.writeFileSync(path.join(TOP, 'src', 'version.ts'), content, 'utf8'); diff --git a/packages/instrumentation-openai/scripts/semconv-gen.js b/packages/instrumentation-openai/scripts/semconv-gen.js new file mode 100755 index 00000000..1b2cc155 --- /dev/null +++ b/packages/instrumentation-openai/scripts/semconv-gen.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/** + * A small script to aid with maintaining/generating src/semconv.ts. + * + * Currently this assumes that `@opentelemetry/semantic-conventions` is + * installed, which isn't a great assumption. + * + * Usage: + * 1. Update any used semconv constants in `names` below. + * 2. Run `./scripts/semconv-gen.js`. + * 3. Use that output to help update `src/semconv.ts`. Nothing fancy. + */ + +const incubating = require('@opentelemetry/semantic-conventions/incubating'); + +const names = ` +// stable +ATTR_SERVER_ADDRESS +ATTR_SERVER_PORT + +// unstable +ATTR_EVENT_NAME +ATTR_GEN_AI_OPERATION_NAME +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_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 + +// not yet in published @opentelemetry/semantic-conventions package +ATTR_GEN_AI_REQUEST_ENCODING_FORMATS +` + .trim() + .split(/\n/g) + .map(line => { + const commentIdx = line.indexOf('//'); + if (commentIdx !== -1) { + line = line.slice(0, commentIdx); + } + return line.trim(); + }); + +for (let name of names) { + if (!name) { + process.stdout.write('\n'); + continue; + } + const val = incubating[name]; + switch (typeof val) { + case 'undefined': + console.log( + `exports const ${name} = ???; // not in the installed semconv pkg` + ); + break; + case 'string': + console.log(`export const ${name} = '${val}';`); + break; + case 'number': // e.g. RPC_GRPC_STATUS_CODE_VALUE_OUT_OF_RANGE + // falls through + case 'function': // e.g. ATTR_RPC_GRPC_RESPONSE_METADATA + console.log(`export const ${name} = ${val};`); + break; + default: + throw new Error(`WAT semconv "${name}" type: ${typeof val}`); + } +} diff --git a/packages/instrumentation-openai/src/index.ts b/packages/instrumentation-openai/src/index.ts new file mode 100644 index 00000000..1b94c8db --- /dev/null +++ b/packages/instrumentation-openai/src/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +export { OpenAIInstrumentation } from './instrumentation'; +export { OpenAIInstrumentationConfig } from './types'; diff --git a/packages/instrumentation-openai/src/instrumentation.ts b/packages/instrumentation-openai/src/instrumentation.ts new file mode 100644 index 00000000..2d0b5bc3 --- /dev/null +++ b/packages/instrumentation-openai/src/instrumentation.ts @@ -0,0 +1,752 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// 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 { + 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_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, + EVENT_GEN_AI_ASSISTANT_MESSAGE, + EVENT_GEN_AI_CHOICE, + EVENT_GEN_AI_SYSTEM_MESSAGE, + EVENT_GEN_AI_TOOL_MESSAGE, + EVENT_GEN_AI_USER_MESSAGE, + METRIC_GEN_AI_CLIENT_OPERATION_DURATION, + METRIC_GEN_AI_CLIENT_TOKEN_USAGE, +} from './semconv'; +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'; + +// Use `DEBUG=elastic-opentelemetry-instrumentation-openai ...` for debug output. +// Or use `node --env-file ./dev.env ...` in this repo. +import createDebug from 'debug'; +const debug = createDebug('elastic-opentelemetry-instrumentation-openai'); +(debug as any).inspectOpts = { depth: 50, colors: true }; + +export class OpenAIInstrumentation extends InstrumentationBase { + private _genaiClientOperationDuration!: Histogram; + private _genaiClientTokenUsage!: Histogram; + + constructor(config = {}) { + 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 <5'], + (modExports, modVer) => { + debug( + 'instrument openai@%s (isESM=%s), config=%o', + modVer, + modExports[Symbol.toStringTag] === 'Module', + this.getConfig() + ); + this._wrap( + modExports.OpenAI.Chat.Completions.prototype, + 'create', + this._getPatchedChatCompletionsCreate() + ); + this._wrap( + modExports.OpenAI.Embeddings.prototype, + 'create', + this._getPatchedEmbeddingsCreate() + ); + + return modExports; + } + ), + ]; + } + + // 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: (...args: unknown[]) => any) => { + // https://platform.openai.com/docs/api-reference/chat/create + return function patchedCreate(this: any, ...args: unknown[]) { + if (!self.isEnabled) { + return original.apply(this, args); + } + + debug('OpenAI.Chat.Completions.create args: %O', args); + const params = + args[0] as any; /* type ChatCompletionCreateParamsStreaming */ + 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: Promise = context.with(ctx, () => + original.apply(this, args) + ); + + // Streaming. + if (params && params.stream) { + // When streaming, `apiPromise` resolves to `import('openai/streaming').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 => { + self._wrap(stream, '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, + 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. + * + * @param {import('openai').OpenAI.ChatCompletionCreateParams} params + */ + _startChatCompletionsSpan( + params: any, + 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; + } + if (params.max_completion_tokens != null) { + attrs[ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.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.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: any) => { + // `msg` is Array + let body; + switch (msg.role) { + case 'system': + if (config.captureMessageContent) { + this.logger.emit({ + timestamp, + context: ctx, + severityNumber: SeverityNumber.INFO, + attributes: { + [ATTR_EVENT_NAME]: EVENT_GEN_AI_SYSTEM_MESSAGE, + [ATTR_GEN_AI_SYSTEM]: 'openai', + }, + body: { + role: msg.role, + content: msg.content, + } as GenAISystemMessageEventBody, + }); + } + break; + case 'user': + if (config.captureMessageContent) { + this.logger.emit({ + timestamp, + context: ctx, + severityNumber: SeverityNumber.INFO, + attributes: { + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + [ATTR_GEN_AI_SYSTEM]: 'openai', + }, + body: { + role: msg.role, + content: msg.content, + } as GenAIUserMessageEventBody, + }); + } + break; + case 'assistant': + if (config.captureMessageContent) { + body = { + content: msg.content, + tool_calls: msg.tool_calls, + } as GenAIAssistantMessageEventBody; + } else { + body = { + tool_calls: msg.tool_calls.map((tc: any) => { + return { + id: tc.id, + type: tc.type, + function: { name: tc.function.name }, + }; + }), + } as GenAIAssistantMessageEventBody; + } + 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': + if (config.captureMessageContent) { + body = { + content: msg.content, + id: msg.tool_call_id, + } as GenAIToolMessageEventBody; + } else { + body = { + id: msg.tool_call_id, + } as GenAIToolMessageEventBody; + } + 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: + 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. + * + * @param {OpenAIInstrumentationConfig} config + */ + async *_onChatCompletionsStreamIterator( + streamIter: AsyncIterator, + span: Span, + startNow: number, + config: OpenAIInstrumentationConfig, + commonAttrs: Attributes, + ctx: Context + ) { + let id; + let model; + let role; + let finishReason; + const contentParts = []; + const toolCalls = []; + for await (const chunk of streamIter as any) { + yield chunk; + + // Gather telemetry from this chunk. + debug('OpenAI.Chat.Completions.create stream chunk: %O', chunk); + if (config.captureMessageContent) { + const contentPart = chunk.choices[0]?.delta?.content; + if (contentPart) { + contentParts.push(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 (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 (!role) { + role = chunk.choices[0]?.delta?.role; + } + if (!finishReason) { + finishReason = chunk.choices[0]?.finish_reason; + if (finishReason) { + span.setAttribute(ATTR_GEN_AI_RESPONSE_FINISH_REASONS, [ + 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', + }); + } + } + + // Capture choices as log events. + const message: Partial = { role }; + if (config.captureMessageContent && contentParts.length > 0) { + message.content = contentParts.join(''); + } + if (toolCalls.length > 0) { + message.tool_calls = toolCalls; + if (!config.captureMessageContent) { + toolCalls.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: finishReason, + index: 0, + message, + } as GenAIChoiceEventBody, + }); + + this._genaiClientOperationDuration.record( + (performance.now() - startNow) / 1000, + { + ...commonAttrs, + [ATTR_GEN_AI_RESPONSE_MODEL]: model, + } + ); + + span.end(); + } + + /** + * @param {import('openai').OpenAI.ChatCompletion} result + * @param {OpenAIInstrumentationConfig} config + */ + _onChatCompletionsCreateResult( + span: Span, + startNow: number, + commonAttrs: Attributes, + result: any, + config: OpenAIInstrumentationConfig, + ctx: Context + ) { + debug('OpenAI.Chat.Completions.create result: %O', result); + try { + span.setAttribute( + ATTR_GEN_AI_RESPONSE_FINISH_REASONS, + result.choices.map((c: any) => c.finish_reason) + ); + span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, result.id); + span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, result.model); + 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: any) => { + let message: Partial; + if (config.captureMessageContent) { + // TODO: telemetry diff with streaming case: content=null, no 'role: assistant', 'tool calls (enableCaptureContent=true)' test case + message = { content: choice.message.content }; + if (choice.message.tool_calls) { + message.tool_calls = choice.message.tool_calls; + } + } else { + message = {}; + if (choice.tool_calls) { + message.tool_calls = choice.message.tool_calls.map((tc: any) => { + 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, + } + ); + + 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) => { + 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: any) => { + // https://platform.openai.com/docs/api-reference/embeddings/create + return function patchedCreate(this: any, ...args: unknown[]) { + if (!self.isEnabled) { + return original.apply(this, args); + } + + 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; + + /** @type {import('openai/core').APIPromise} */ + const apiPromise = context.with(ctx, () => original.apply(this, args)); + + apiPromise + .then((result: any) => { + 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. + * + * @param {OpenAIInstrumentationConfig} config + */ + _startEmbeddingsSpan(params: any, 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 }; + } + + /** + * @param {import('openai').OpenAI.CreateEmbeddingResponse} result + */ + _onEmbeddingsCreateResult( + span: Span, + startNow: number, + commonAttrs: Attributes, + result: any + ) { + 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(); + } +} diff --git a/packages/instrumentation-openai/src/internal-types.ts b/packages/instrumentation-openai/src/internal-types.ts new file mode 100644 index 00000000..8389ba5a --- /dev/null +++ b/packages/instrumentation-openai/src/internal-types.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Some types for https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events + +export type GenAIFunction = { + name: string; + arguments?: any; +}; + +export type GenAIToolCall = { + id: string; + type: string; + function: GenAIFunction; +}; + +export type GenAIMessage = { + role?: string; + content?: any; + tool_calls?: GenAIToolCall[]; +}; + +export type GenAIChoiceEventBody = { + finish_reason: string; + index: number; + message: GenAIMessage; +}; + +export type GenAISystemMessageEventBody = { + role?: string; + content?: any; +}; + +export type GenAIUserMessageEventBody = { + role?: string; + content?: any; +}; + +export type GenAIAssistantMessageEventBody = { + role?: string; + content?: any; + tool_calls?: GenAIToolCall[]; +}; + +export type GenAIToolMessageEventBody = { + role?: string; + content?: any; + id: string; +}; diff --git a/packages/instrumentation-openai/src/semconv.ts b/packages/instrumentation-openai/src/semconv.ts new file mode 100644 index 00000000..56963717 --- /dev/null +++ b/packages/instrumentation-openai/src/semconv.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/** + * Semantic-conventions constants. + * + * This instrumentation uses unstable semconv: + * https://github.com/open-telemetry/opentelemetry-js/blob/main/semantic-conventions/README.md#unstable-semconv + * OTel JS advice is to *copy* relevant definitions, rather than take a + * dependency on `@opentelemetry/semantic-conventions` and use its + * `.../incubating` entry-point. + * + * There are a small number of stable semconv defintions used, so the main + * entry-point *could* be used. However, for now we opt to copy those + * definitions as well. + */ + +// -- Stable semconv + +export const ATTR_SERVER_ADDRESS = 'server.address'; +export const ATTR_SERVER_PORT = 'server.port'; + +// -- Unstable semconv + +export const ATTR_EVENT_NAME = 'event.name'; +export const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name'; +export const ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY = + 'gen_ai.request.frequency_penalty'; +export const ATTR_GEN_AI_REQUEST_MAX_TOKENS = 'gen_ai.request.max_tokens'; +export const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model'; +export const ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY = + 'gen_ai.request.presence_penalty'; +export const ATTR_GEN_AI_REQUEST_TOP_P = 'gen_ai.request.top_p'; +export const ATTR_GEN_AI_RESPONSE_FINISH_REASONS = + 'gen_ai.response.finish_reasons'; +export const ATTR_GEN_AI_RESPONSE_ID = 'gen_ai.response.id'; +export const ATTR_GEN_AI_RESPONSE_MODEL = 'gen_ai.response.model'; +export const ATTR_GEN_AI_SYSTEM = 'gen_ai.system'; +export const ATTR_GEN_AI_TOKEN_TYPE = 'gen_ai.token.type'; +export const ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens'; +export const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens'; +export const METRIC_GEN_AI_CLIENT_OPERATION_DURATION = + 'gen_ai.client.operation.duration'; +export const METRIC_GEN_AI_CLIENT_TOKEN_USAGE = 'gen_ai.client.token.usage'; + +export const ATTR_GEN_AI_REQUEST_ENCODING_FORMATS = + 'gen_ai.request.encoding_formats'; + +// The JS semconv package doesn't yet emit constants for event names. +// TODO: otel-js issue for semconv pkg not including event names +export const EVENT_GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message'; +export const EVENT_GEN_AI_USER_MESSAGE = 'gen_ai.user.message'; +export const EVENT_GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message'; +export const EVENT_GEN_AI_TOOL_MESSAGE = 'gen_ai.tool.message'; +export const EVENT_GEN_AI_CHOICE = 'gen_ai.choice'; diff --git a/packages/instrumentation-openai/src/types.ts b/packages/instrumentation-openai/src/types.ts new file mode 100644 index 00000000..199209e6 --- /dev/null +++ b/packages/instrumentation-openai/src/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +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 00000000..d92bc15c --- /dev/null +++ b/packages/instrumentation-openai/src/utils.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +import { diag } from '@opentelemetry/api'; +import type { Attributes, DiagLogger } from '@opentelemetry/api'; +import { ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT } from './semconv'; + +/** + * Read a boolean from an environment variable. + * + * https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#batch-span-processor + * + * @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; + } + } +} + +const SERVER_PORT_FROM_URL_PROTOCOL = { '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 as any)[u.protocol], + }; +} diff --git a/packages/instrumentation-openai/test/config.test.js b/packages/instrumentation-openai/test/config.test.js new file mode 100644 index 00000000..e100ff14 --- /dev/null +++ b/packages/instrumentation-openai/test/config.test.js @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const test = require('tape'); +const { OpenAIInstrumentation } = require('../'); // @elastic/opentelemetry-instrumentation-openai + +test('config', async suite => { + suite.test('default config', t => { + const instr = new OpenAIInstrumentation(); + t.deepEqual(instr.getConfig(), { + enabled: true, + captureMessageContent: false, + }); + t.end(); + }); + + suite.test('param captureMessageContent', t => { + let instr = new OpenAIInstrumentation({ captureMessageContent: true }); + t.deepEqual(instr.getConfig().captureMessageContent, true); + + instr = new OpenAIInstrumentation({ captureMessageContent: false }); + t.deepEqual(instr.getConfig().captureMessageContent, false); + + instr = new OpenAIInstrumentation({ + captureMessageContent: 'some-truthy-value', + }); + t.deepEqual(instr.getConfig().captureMessageContent, true); + + instr = new OpenAIInstrumentation({ + captureMessageContent: 0, // a non-bool falsy value + }); + t.deepEqual(instr.getConfig().captureMessageContent, false); + + t.end(); + }); + + suite.test('envvar OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT', t => { + process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = 'true'; + let instr = new OpenAIInstrumentation(); + t.deepEqual(instr.getConfig().captureMessageContent, true); + + process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = 'false'; + instr = new OpenAIInstrumentation(); + t.deepEqual(instr.getConfig().captureMessageContent, false); + + // envvar wins over param. + process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = 'false'; + instr = new OpenAIInstrumentation({ captureMessageContent: true }); + t.deepEqual(instr.getConfig().captureMessageContent, false); + + // Bogus envvar value is ignored. + process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = 'bogus'; + instr = new OpenAIInstrumentation({ captureMessageContent: true }); + t.deepEqual(instr.getConfig().captureMessageContent, true); + + delete process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT; + t.end(); + }); + + suite.test('setConfig', t => { + process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = 'true'; + let instr = new OpenAIInstrumentation(); + t.deepEqual(instr.getConfig(), { + enabled: true, + captureMessageContent: true, + }); + + instr.setConfig({ + captureMessageContent: false, + }); + t.deepEqual(instr.getConfig(), { + enabled: true, + captureMessageContent: false, + }); + + delete process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT; + t.end(); + }); + + suite.end(); +}); diff --git a/packages/instrumentation-openai/test/fixtures.test.js b/packages/instrumentation-openai/test/fixtures.test.js new file mode 100644 index 00000000..657ad55c --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures.test.js @@ -0,0 +1,1346 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +/** + * Run test fixtures. + * + * A test fixture is: + * 1. one of the entries in the `testFixtures` array below, which defines + * 2. a script in test/fixtures/ to exec, and + * 3. a set of assertions to make on the telemetry from that process run. + * + * For each fixture, the script is run with OTel instrumentation and a mock OTel + * collector, and then assertions are checked. + * + * This test file is used for "unit" (TEST_FIXTURES_MODE=unit, the default) and + * "integration" (TEST_FIXTURES_MODE=integration) tests. The unit tests use + * pre-recorded responses from OpenAI (see test/fixtures/nock-recordings/). + * The integration tests run + * + * # Configuration environment variables + * + * - TEST_FIXTURES_MODE: "unit", "integration", or "regenerate-recordings" + * - TEST_FIXTURES_ENV_FILE: Set this to a *.env file path to have it loaded by + * dotenv. This exists as a convenience because using dotenv on the CLI is + * cumbersome. + * + * The following envvars are only used for integration tests. + * + * - TEST_MODEL_TOOLS: The name of the GenAI model to use for most tests. It + * must support tool/function-calling. + * https://platform.openai.com/docs/guides/function-calling + * - TEST_MODEL_EMBEDDINGS: The name of the GenAI model to use for embeddings + * tests. https://platform.openai.com/docs/guides/embeddings + * - `openai` client library envvars: OPENAI_BASE_URL, OPENAI_API_KEY, + * AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY. If AZURE_OPENAI_API_KEY is + * set, then the `openai.AzureOpenAI` client is used, otherwise + * `openai.OpenAI` is used. + */ + +const test = require('tape'); + +// On semconv imports: +// - Read "src/semconv.ts" top-comment for why this instrumentation does not +// have a *runtime* dep on `@opentelemetry/semantic-conventions`. +// - However, we *do* use the package in tests. This effectively is a check +// that the constants in "src/semconv.ts" match the published package. +const { + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} = require('@opentelemetry/semantic-conventions'); +const { + ATTR_EVENT_NAME, + ATTR_GEN_AI_OPERATION_NAME, + ATTR_GEN_AI_RESPONSE_FINISH_REASONS, + ATTR_GEN_AI_REQUEST_MAX_TOKENS, + ATTR_GEN_AI_REQUEST_MODEL, + 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, +} = require('@opentelemetry/semantic-conventions/incubating'); +const { + ATTR_GEN_AI_REQUEST_ENCODING_FORMATS, + EVENT_GEN_AI_ASSISTANT_MESSAGE, + EVENT_GEN_AI_CHOICE, + EVENT_GEN_AI_SYSTEM_MESSAGE, + EVENT_GEN_AI_USER_MESSAGE, +} = require('../build/src/semconv'); + +const { + assertDeepMatch, + runTestFixtures, + findObjInArray, +} = require('./testutils'); + +// Convenience to load a .env file. +// TEST_FIXTURES_ENV_FILE=./my.env npm test +// +// This test can be configured with a number of environment variables. It is +// nice to specify those in a .env file. However, sometimes it is a pain to +// execute a node script or test file with a .env file. +// - This works with Node.js v20+, but not Node v18. +// node --env-file ./my.env script.js +// - This works with all versions, but is wordy and obtuse: +// NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=./my.env npm run something +if (process.env.TEST_FIXTURES_ENV_FILE) { + require('dotenv').config({ path: process.env.TEST_FIXTURES_ENV_FILE }); +} + +const UNIT_TEST_MODEL_TOOLS = 'gpt-4o-mini'; +const UNIT_TEST_MODEL_EMBEDDINGS = 'text-embedding-3-small'; + +// Configure the test fixtures based on the test mode. +const testMode = process.env.TEST_FIXTURES_MODE || 'unit'; +const isUnit = testMode === 'unit'; // shorthand for terser code below +// Nock is used for HTTP-mocking in some test modes. It interferes with +// `@opentelemetry/instrumentation-http`, so we skip assertions for HTTP +// spans when using nock. +let usingNock = false; +let targetService; + +switch (testMode) { + case 'unit': + // Unit tests. + // "unit" mode is for running using pre-recorded OpenAI API responses. + // This is implemented using `nock`s "Nock back" feature. + usingNock = true; + // https://github.com/nock/nock#modes + process.env.TEST_NOCK_BACK_MODE = 'lockdown'; + // OPENAI_API_KEY needs to be set to something to avoid OpenAI + // constructor error. However, because of mocking, it isn't used. + process.env.OPENAI_API_KEY = 'notused'; + process.env.TEST_MODEL_TOOLS = UNIT_TEST_MODEL_TOOLS; + process.env.TEST_MODEL_EMBEDDINGS = UNIT_TEST_MODEL_EMBEDDINGS; + targetService = 'openai'; + break; + + case 'regenerate-recordings': + // Regenerate the recorded OpenAI API responses use by "unit" test mode. + if (process.env.OPENAI_BASE_URL) { + throw new Error( + 'OPENAI_BASE_URL is set. To regenerate-recordings, it must be empty so that recorded responses are from api.openai.com' + ); + } + if (!process.env.OPENAI_API_KEY) { + throw new Error( + 'OPENAI_API_KEY is not set. To regenerate-recordings, it must be set. Set it in your environment, or use TEST_FIXTURES_ENV_FILE="./openai.env" (see "openai.env.template").' + ); + } + usingNock = true; + process.env.TEST_NOCK_BACK_MODE = 'update'; + process.env.TEST_MODEL_TOOLS = UNIT_TEST_MODEL_TOOLS; + process.env.TEST_MODEL_EMBEDDINGS = UNIT_TEST_MODEL_EMBEDDINGS; + targetService = 'openai'; + break; + + case 'integration': + // Guess the target service based on the config in the environment. + // This is necessary because there some small differences between Azure + // OpenAI, OpenAI, and Ollama that test assertions need to cope with. + if ( + process.env.OPENAI_BASE_URL && + new URL(process.env.OPENAI_BASE_URL).port === '11434' + ) { + targetService = 'ollama'; + } else if (process.env.AZURE_OPENAI_API_KEY) { + targetService = 'azure'; + } else { + targetService = 'openai'; + } + break; + + default: + throw new Error('unknown test mode: ' + testMode); +} + +// ---- helper functions + +function isPositiveInteger(val) { + return Number.isInteger(val) && val > 0; +} + +function isExpectedServerAddress(val) { + const baseUrl = + (process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_ENDPOINT) || + process.env.OPENAI_BASE_URL || + 'https://api.openai.com'; + const expectedHostname = new URL(baseUrl).hostname; + return val === expectedHostname; +} + +function isExpectedServerPort(val) { + if (targetService === 'openai') { + return val === 443; + } else if (targetService === 'azure') { + let port = new URL(process.env.AZURE_OPENAI_ENDPOINT).port; + port = port ? Number(port) : 443; + return val == port; + } else if (targetService === 'ollama') { + return val === Number(new URL(process.env.OPENAI_BASE_URL).port); + } else { + return false; + } +} + +function isExpectedResponseModel(unitTestVal, requestModel) { + return val => { + if (isUnit) { + return val === unitTestVal; + } else if (process.env.AZURE_OPENAI_API_KEY) { + // The Azure OpenAI API accepts a "model" argument that + // actually refers to a deployment name. That deployment name + // *might* match the model in that deployment, but that is + // not at all a guarantee. + return typeof val === 'string' && val.length > 0; + } else { + // Typically `$MODEL-$RELEASE_DATE` from api.openai.com. + return val.startsWith(requestModel); + } + }; +} + +// ---- tests + +test('fixtures', async suite => { + /** @type {import('./testutils').TestFixture[]} */ + let testFixtures = [ + { + name: 'chat-completion (captureMessageContent=true)', + args: ['./fixtures/chat-completion.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'chat-completion', + }, + verbose: true, + checkTelemetry: (t, col, _stdout) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_REQUEST_MAX_TOKENS]: 200, + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'], + [ATTR_GEN_AI_RESPONSE_ID]: isUnit + ? 'chatcmpl-AaroVIDuvKJDRS0l540Oxc4FSgIux' + : /.+/, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit ? 24 : isPositiveInteger, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: isUnit ? 4 : isPositiveInteger, + }, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + if (!usingNock) { + t.equal(spans[1].scope.name, '@opentelemetry/instrumentation-http'); + t.equal( + spans[1].parentSpanId, + spans[0].spanId, + 'HTTP span is a child of the GenAI span' + ); + t.ok( + spans[1].attributes['http.target'].includes('/chat/completions'), + 'looks like a .../chat/completions HTTP endpoint' + ); + } + + assertDeepMatch( + t, + col.logs, + [ + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'stop', + index: 0, + message: { + content: isUnit ? 'South Atlantic Ocean.' : /.+/, + }, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + ], + 'log events' + ); + + // Metrics + let metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_OPERATION_DURATION + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_OPERATION_DURATION, + unit: 's', + histogram: { + dataPoints: [ + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_OPERATION_DURATION) + ); + metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_TOKEN_USAGE + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_TOKEN_USAGE, + unit: '{token}', + histogram: { + dataPoints: [ + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_TOKEN_TYPE]: 'input', + }, + }, + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_TOKEN_TYPE]: 'output', + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_TOKEN_USAGE) + ); + }, + }, + + { + // Same as the previous test case, except for the captureMessageContent + // setting. Ensure that Opt-In telemetry values are *not* captured. + name: 'chat-completion (captureMessageContent=false)', + args: ['./fixtures/chat-completion.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + TEST_FIXTURE_RECORDING_NAME: 'chat-completion', + }, + checkTelemetry: (t, col, _stdout) => { + assertDeepMatch( + t, + col.logs, + [ + // Expect to *not* have a 'gen_ai.user.message' event. The only + // field on that event is the "Opt-In" `content`. + { + attributes: { + 'event.name': EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'stop', + index: 0, + message: { + content: undefined, // This must not be captured. + }, + }, + }, + ], + 'log events' + ); + }, + }, + + { + name: 'streaming-chat-completion', + args: ['./fixtures/streaming-chat-completion.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'streaming-chat-completion', + }, + verbose: true, + checkTelemetry: (t, col, stdout) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['stop'], + [ATTR_GEN_AI_RESPONSE_ID]: isUnit + ? 'chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap' + : /.+/, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: undefined, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: undefined, + }, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + if (!usingNock) { + t.equal(spans[1].scope.name, '@opentelemetry/instrumentation-http'); + t.equal( + spans[1].parentSpanId, + spans[0].spanId, + 'HTTP span is a child of the GenAI span' + ); + t.ok( + spans[1].attributes['http.target'].includes('/chat/completions'), + 'looks like a .../chat/completions HTTP endpoint' + ); + } + + assertDeepMatch( + t, + col.logs, + [ + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'stop', + index: 0, + message: { content: /.+/ }, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + ], + 'log events' + ); + + // Metrics + // Should not be a METRIC_GEN_AI_CLIENT_TOKEN_USAGE metric, + // because this response does not include usage data. + let metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_OPERATION_DURATION + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_OPERATION_DURATION, + unit: 's', + histogram: { + dataPoints: [ + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_OPERATION_DURATION) + ); + }, + }, + + { + name: 'streaming-with-include_usage', + args: ['./fixtures/streaming-with-include_usage.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'streaming-with-include_usage', + }, + verbose: true, + checkTelemetry: (t, col, stdout) => { + const spans = col.sortedSpans; + + // Only bother to assert a few fields, because we expect mostly + // the same telemetry as with "streaming-chat-completions.js". + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit ? 24 : isPositiveInteger, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: isUnit ? 4 : isPositiveInteger, + }, + }, + 'spans[0]' + ); + + // Metrics + // Compared to the `streaming-chat-completion` test case, we + // expect to have a METRIC_GEN_AI_CLIENT_TOKEN_USAGE metric now. + let metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_OPERATION_DURATION + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_OPERATION_DURATION, + unit: 's', + histogram: { + dataPoints: [ + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_OPERATION_DURATION) + ); + metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_TOKEN_USAGE + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_TOKEN_USAGE, + unit: '{token}', + histogram: { + dataPoints: [ + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_TOKEN_TYPE]: 'input', + }, + }, + { + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_TOKEN_TYPE]: 'output', + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_TOKEN_USAGE) + ); + }, + }, + + { + name: 'streaming-with-tee', + args: ['./fixtures/streaming-with-tee.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'streaming-with-tee', + }, + verbose: true, + checkTelemetry: (t, col, stdout) => { + const spans = col.sortedSpans; + + // Only bother to assert a few fields, because we expect mostly + // the same telemetry as with "streaming-chat-completions.js". + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + }, + }, + 'spans[0]' + ); + }, + }, + + { + // This test script aborts after receiving one token. We expect to + // still get the GenAI span. + name: 'streaming-abort', + testOpts: { + skip: usingNock + ? 'Nock back record/replay does not work for this mid-response abort.' + : false, + }, + args: ['./fixtures/streaming-abort.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + }, + checkTelemetry: (t, col, stdout) => { + const spans = col.sortedSpans; + + // Only bother to assert a few fields, because we expect mostly + // the same telemetry as with "streaming-chat-completions.js". + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: undefined, + }, + }, + 'spans[0]' + ); + }, + }, + + { + name: 'streaming-bad-iterate', + args: ['./fixtures/streaming-bad-iterate.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + TEST_FIXTURE_RECORDING_NAME: 'streaming-bad-iterate', + }, + checkResult: (t, err) => { + t.ok(err, 'got an error from iterating twice over chat stream'); + t.match(err.message, /iterate/, 'err.message includes "iterate"'); + }, + }, + + { + name: 'tool calls (captureMessageContent=true)', + args: ['./fixtures/tools.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'tool-calls', + }, + // verbose: true, + checkTelemetry: (t, col) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'], + [ATTR_GEN_AI_RESPONSE_ID]: isUnit + ? 'chatcmpl-ADhWWspuIro8PA6qaATjihQjkj5QM' + : /.+/, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit + ? 140 + : isPositiveInteger, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: isUnit + ? 19 + : isPositiveInteger, + }, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + assertDeepMatch( + t, + col.logs, + [ + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_SYSTEM_MESSAGE, + }, + body: { + role: 'system', + content: + 'You are a helpful customer support assistant. Use the supplied tools to assist the user.', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: 'Hi, can you tell me the delivery date for my order?', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_ASSISTANT_MESSAGE, + }, + body: { + content: + 'Hi there! I can help with that. Can you please provide your order ID?', + tool_calls: null, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: 'i think it is order_12345', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'tool_calls', + index: 0, + message: { + tool_calls: [ + { + id: isUnit ? 'call_VPRh9L0Z20gNj9DIZQJqHN7O' : /.+/, + type: 'function', + function: { + name: 'get_delivery_date', + arguments: isUnit + ? '{"order_id":"order_12345"}' + : /{"order_id":".*?"}/, + }, + }, + ], + }, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + ], + 'log events' + ); + }, + }, + + { + name: 'streaming tool calls (captureMessageContent=true)', + args: ['./fixtures/streaming-tools.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'streaming-tool-calls', + }, + // verbose: true, + checkTelemetry: (t, col) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'], + [ATTR_GEN_AI_RESPONSE_ID]: isUnit + ? 'chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ' + : /.+/, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit + ? 140 + : isPositiveInteger, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: isUnit + ? 19 + : isPositiveInteger, + }, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + assertDeepMatch( + t, + col.logs, + [ + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_SYSTEM_MESSAGE, + }, + body: { + role: 'system', + content: + 'You are a helpful customer support assistant. Use the supplied tools to assist the user.', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: 'Hi, can you tell me the delivery date for my order?', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_ASSISTANT_MESSAGE, + }, + body: { + content: + 'Hi there! I can help with that. Can you please provide your order ID?', + tool_calls: null, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + body: { + role: 'user', + content: 'i think it is order_12345', + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'tool_calls', + index: 0, + message: { + role: 'assistant', + tool_calls: [ + { + id: isUnit ? 'call_GqsvoRkHMjAlIhoSWKP6D2lw' : /.+/, + type: 'function', + function: { + name: 'get_delivery_date', + arguments: isUnit + ? '{"order_id":"order_12345"}' + : /{"order_id":".*?"}/, + }, + }, + ], + }, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + ], + 'log events' + ); + }, + }, + + { + name: 'streaming parallel tool calls (captureMessageContent=true)', + testOpts: { + skip: + targetService === 'ollama' + ? 'The test model used with Ollama does not typically result in tool calls with this test.' + : false, + }, + args: ['./fixtures/streaming-parallel-tool-calls.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: 'true', + TEST_FIXTURE_RECORDING_NAME: 'streaming-parallel-tool-calls', + }, + verbose: true, + checkTelemetry: (t, col) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + assertDeepMatch( + t, + spans[0], + { + name: `chat ${process.env.TEST_MODEL_TOOLS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + [ATTR_GEN_AI_OPERATION_NAME]: 'chat', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_TOOLS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_GEN_AI_RESPONSE_FINISH_REASONS]: ['tool_calls'], + [ATTR_GEN_AI_RESPONSE_ID]: isUnit + ? 'chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ' + : /.+/, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'gpt-4o-mini-2024-07-18', + process.env.TEST_MODEL_TOOLS + ), + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit ? 56 : isPositiveInteger, + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: isUnit + ? 45 + : isPositiveInteger, + }, + events: undefined, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + assertDeepMatch( + t, + col.logs, + [ + { + body: { + role: 'system', + content: + 'You are a helpful assistant providing weather updates.', + }, + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_SYSTEM_MESSAGE, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + body: { + role: 'user', + content: 'What is the weather in New York and London?', + }, + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_USER_MESSAGE, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + { + body: { + finish_reason: 'tool_calls', + index: 0, + message: { + role: 'assistant', + tool_calls: [ + { + id: isUnit ? 'call_MDti4mtc0TKeNC0HyE8wy9nn' : /.+/, + type: 'function', + function: { + name: 'get_weather', + arguments: isUnit + ? '{"location": "New York"}' + : /{"location":.*?}/, + }, + }, + { + id: isUnit ? 'call_eA8ose7WzOz5tFM3vdFNGf71' : /.+/, + type: 'function', + function: { + name: 'get_weather', + arguments: isUnit + ? '{"location": "London"}' + : /{"location":.*?}/, + }, + }, + ], + }, + }, + attributes: { + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_EVENT_NAME]: EVENT_GEN_AI_CHOICE, + }, + traceId: spans[0].traceId, + spanId: spans[0].spanId, + }, + ], + 'log events' + ); + }, + }, + + { + // Ensure that Opt-In telemetry is *not* captured with + // captureMessageContent=false (the default). + name: 'streaming parallel tool calls (captureMessageContent=false)', + testOpts: { + skip: + targetService === 'ollama' + ? 'The test model used with Ollama does not typically result in tool calls with this test.' + : false, + }, + args: ['./fixtures/streaming-parallel-tool-calls.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + TEST_FIXTURE_RECORDING_NAME: 'streaming-parallel-tool-calls', + }, + checkTelemetry: (t, col) => { + assertDeepMatch( + t, + col.logs, + [ + // Expect there to *not* be a gen_ai.system.message event. + // Expect there to *not* be a gen_ai.user.message event. + { + attributes: { + 'event.name': EVENT_GEN_AI_CHOICE, + }, + body: { + finish_reason: 'tool_calls', + index: 0, + message: { + role: 'assistant', + tool_calls: [ + { + id: isUnit ? 'call_MDti4mtc0TKeNC0HyE8wy9nn' : /.+/, + type: 'function', + function: { + name: 'get_weather', + arguments: undefined, // This must not be captured. + }, + }, + { + id: isUnit ? 'call_eA8ose7WzOz5tFM3vdFNGf71' : /.+/, + type: 'function', + function: { + name: 'get_weather', + arguments: undefined, // This must not be captured. + }, + }, + ], + }, + }, + }, + ], + 'log events' + ); + }, + }, + + { + name: 'embeddings', + args: ['./fixtures/embeddings.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=./fixtures/telemetry.js', + TEST_FIXTURE_RECORDING_NAME: 'embeddings', + }, + checkTelemetry: (t, col, _stdout) => { + const spans = col.sortedSpans; + + // Match a subset of the GenAI span fields. + const commonExpectedAttrs = { + [ATTR_GEN_AI_OPERATION_NAME]: 'embeddings', + [ATTR_GEN_AI_REQUEST_MODEL]: process.env.TEST_MODEL_EMBEDDINGS, + [ATTR_GEN_AI_SYSTEM]: 'openai', + [ATTR_SERVER_ADDRESS]: isExpectedServerAddress, + [ATTR_SERVER_PORT]: isExpectedServerPort, + [ATTR_GEN_AI_RESPONSE_MODEL]: isExpectedResponseModel( + 'text-embedding-3-small', + process.env.TEST_MODEL_EMBEDDINGS + ), + }; + assertDeepMatch( + t, + spans[0], + { + name: `embeddings ${process.env.TEST_MODEL_EMBEDDINGS}`, + kind: 'SPAN_KIND_CLIENT', + attributes: { + ...commonExpectedAttrs, + [ATTR_GEN_AI_REQUEST_ENCODING_FORMATS]: ['float'], + [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: isUnit ? 8 : isPositiveInteger, + // output_tokens is not expected in Embeddings response: + // https://github.com/openai/openai-openapi/blob/1.3.0/openapi.yaml#L3413-L3422 + [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: undefined, + }, + events: undefined, + scope: { + name: '@elastic/opentelemetry-instrumentation-openai', + }, + }, + 'spans[0]' + ); + + if (!usingNock) { + t.equal(spans[1].scope.name, '@opentelemetry/instrumentation-http'); + t.equal( + spans[1].parentSpanId, + spans[0].spanId, + 'HTTP span is a child of the GenAI span' + ); + t.ok( + spans[1].attributes['http.target'].includes('/embeddings'), + 'looks like a .../embeddings HTTP endpoint' + ); + } + + // Metrics + let metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_OPERATION_DURATION + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_OPERATION_DURATION, + unit: 's', + histogram: { + dataPoints: [ + { + attributes: commonExpectedAttrs, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_OPERATION_DURATION) + ); + metric = findObjInArray( + col.metrics, + 'name', + METRIC_GEN_AI_CLIENT_TOKEN_USAGE + ); + assertDeepMatch( + t, + metric, + { + name: METRIC_GEN_AI_CLIENT_TOKEN_USAGE, + unit: '{token}', + histogram: { + dataPoints: [ + { + attributes: { + ...commonExpectedAttrs, + [ATTR_GEN_AI_TOKEN_TYPE]: 'input', + }, + }, + ], + }, + }, + JSON.stringify(METRIC_GEN_AI_CLIENT_TOKEN_USAGE) + ); + }, + }, + + // TODO: see Python's test_all_the_client_options, do something similar + // TODO: test with a tool response from user after a tool call (to test https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#tool-event) + // TODO: test a case where stream fails before completion: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/gen-ai-events.md#choice-event says SHOULD have an event with truncated content and finish_reason=error + ]; + + // For-development envvar to filter down which test cases are run. + if (process.env.TEST_FIXTURES_FILTER) { + const filter = new RegExp(process.env.TEST_FIXTURES_FILTER); + testFixtures = testFixtures.filter(tf => filter.test(tf.name)); + } + + if (testMode === 'regenerate-recordings') { + const recordingNames = new Set(); + testFixtures = testFixtures + .filter(tf => { + if (!tf.env.TEST_FIXTURE_RECORDING_NAME) { + return false; + } else if (recordingNames.has(tf.env.TEST_FIXTURE_RECORDING_NAME)) { + // Multiple test fixtures can share the same recording. + // No need to record it twice. + return false; + } else { + recordingNames.add(tf.env.TEST_FIXTURE_RECORDING_NAME); + return true; + } + }) + .map(tf => { + // Run the fixtures *without* instrumentation, and skip + // assertions. We want as pristine a run as possible for + // creating recordings. + delete tf.env.NODE_OPTIONS; + delete tf.checkTelemetry; + return tf; + }); + } + + runTestFixtures(suite, testFixtures); + suite.end(); +}); diff --git a/packages/instrumentation-openai/test/fixtures/chat-completion.js b/packages/instrumentation-openai/test/fixtures/chat-completion.js new file mode 100644 index 00000000..768f2aab --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/chat-completion.js @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const messages = [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ]; + const chatCompletion = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + // `max_tokens` because AzureOpenAI does not support max_completions_tokens, + // as of `OPENAI_API_VERSION=2024-10-01-preview`. + // + // A value of 200 was chosen to be large enough to not accidentally be a + // `finish_reason` when using a model that ignores the "3 words" limit + // and actually uses many tokens in the response. This case was + // anecdotally with `ghcr.io/elastic/ollama/ollama:testing` and + // model "qwen2.5:0.5b" twice, but was not thoroughly investigated. + max_tokens: 200, + messages, + }); + console.log(chatCompletion.choices[0].message.content); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/embeddings.js b/packages/instrumentation-openai/test/fixtures/embeddings.js new file mode 100644 index 00000000..889e3b99 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/embeddings.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const embedding = await client.embeddings.create({ + model: process.env.TEST_MODEL_EMBEDDINGS, + input: ['One fish', 'two fish', 'red fish', 'blue fish'], + encoding_format: 'float', + }); + console.log('Embeddings:'); + console.dir(embedding, { depth: 50 }); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/chat-completion.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/chat-completion.json new file mode 100644 index 00000000..9c7b95dc --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/chat-completion.json @@ -0,0 +1,69 @@ +[ + { + "scope": "https://api.openai.com:443", + "method": "POST", + "path": "/v1/chat/completions", + "body": { + "model": "gpt-4o-mini", + "max_tokens": 200, + "messages": [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains the falkland islands?" + } + ] + }, + "status": 200, + "response": [ + "1f8b08000000000000038c92cd6ac3301084ef7e0aa1b31d94c42121b74029b43d141ae8a514a3c86b5badac15d2baa494bc7b91f36387b6d08b0efbed8c6657fa4a18e3bae46bc6552349b5ce641be9f1f9eea6fb78b8bf79da0ab3c8c5e35ee5b7dbfaaedbf3342a70f7068aceaa89c2d619208df688950749105da7cbf97cbe10623eed418b259828ab1d653966adb6", + "3a9b89599e8965365d9dd40d6a0581afd94bc218635ffd1973da12f67ccd447aaeb41082ac81af2f4d8c718f2656b80c41079296783a408596c0f6d1b7d851c33664a425add8a3026927e35e0f551764cc6b3b634ef5c3e57283b5f3b80b277ea957daead0141e64401b2f0a848ef7f49030f6da0fd95de5e6ce63eba8207c071b0d67f9d18e0fab1de09911923423cd2afdc5ac2881a43661b423aea46aa01c94c34265576a1c816434f2cf2cbf791fc7d6b6fe8ffd009402475016ce43a9d5f5bc439b87f8effe6abbacb80fccc36720688b4adb1abcf3faf8ea952bc4522c76d56aa9044f0ec937000000ffff03003c9974df03030000" + ], + "rawHeaders": [ + "Date", + "Wed, 04 Dec 2024 22:07:11 GMT", + "Content-Type", + "application/json", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "171", + "openai-version", + "2020-10-01", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9999", + "x-ratelimit-remaining-tokens", + "199782", + "x-ratelimit-reset-requests", + "8.64s", + "x-ratelimit-reset-tokens", + "65ms", + "x-request-id", + "req_14e70641d65a385d7b5a82f52be19252", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8ecf1b9f1f4c2dbf-YVR", + "Content-Encoding", + "gzip", + "alt-svc", + "h3=\":443\"; ma=86400" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/embeddings.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/embeddings.json new file mode 100644 index 00000000..81643e6e --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/embeddings.json @@ -0,0 +1,91 @@ +[ + { + "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": [ + "1f8b08000000000000038459cb8e5d4911dccf57b4bc9e4119916fff0aab196c212318167881343f8fea36309c280bf7a217d5d5f5cac888c83cbffdf0f6f6e1efbffce5f39fbe7ef8f8f6e1af5ffef1f5c38f67ecd3cf5f7ffef0f1ed8f3fbcbdbdbdfdf6fafd98f9f96fbf7cfef4e9cbaf7f7e4d7ffdf1cbaf9f3efff3c3c737fbefc8ef93feb3d2f9f9c9fe60669688a4e1c7e7788fb3777f1f7d4dde19cb9ec7283ddc9f1319f44ace734d9fce4c848c6e72a3ebb10066a35b06cdc6dcba269e73bd3731cfb9e53b72256f562cfab9a6a3b2", + "59fe1cdd4667c8492da61d1621772def0997b9bb5153f51ca55739a973931d81799ecc77daa3286f3d9ed427e4ecc6f673aa632df23911b1dd725702b5eecf9822979e53fa00d66139f95c20270b589d5b35537252a0230bcf05dacae17254b6a1ad24d49d8dd1357d1910ac6160947d58986c79654f9fd088789e63e6b5518d25151545fa663ff74221b92660a3c190f99c5acd18bd94e50beacfeb77276002d5f16e456ac5d95d52021d9c9437c176af351e836991b6ff0bbdd7f5fba4bfa0a71b27599e33236a961e82676b1b397c5bd862e5f9c944954948bf09542e52b31733859e0bfa8b9574ca2dfa54cb931411802237905b17259485cb02d63ef0e74ee1d9a02614b22b97ca7dee08c966649294389111b1d1f2fa797254060dacaa982b4b333cd205146e3d99cf574ddac428a66a6ba764d5a86542e99f95061345d9c388fbdc29d60ba5f1631d429450d1a353ce646e6546dd3e8ce8b8397dfbe8d77334c38abd4249ceea09e5744c77f6c8dca0c30cd76e6eb515756516fb92e0ccd832993ab99b5b4a57d95e254a9196594a6c6e09282f7629532126a63cf55607027665a00fc529d087365704d2d99a425e51a4e2b2038d442b08c23d8544d87dd852f285555ccb1b44c896a37a978e253310f22609ab52faf7de8130ba79db64e75e3a5fcda45c2a3939a3f25140a6a85fb4fbb6dcc99cd9b69aefc17e46e59d04dbd28544cce90d6a00ac1dd517b3c1f505989dd98b2b54a0f23d67362a64afb41ae3cd5859f06bb2f118134d4ddbd96aa390cb6e9b5c173bd62d8477b0cad213c09146a887c34c3eccd66b6e75c605f8da613cf3d293d366973af9702e6b11bd4171169953d39284662f7d9dc7c3bc54cb7a11a5cf05d48bfac4c99a354db560ecd86b4de52d763ca706860aee73ae29aa43384b968a098f13ef74d5485b52e175bbe39fdeeb0e12abc1cdea4ba301df783ccc3b9574d58a9ef40cf76252034f32cbffd3dc47346e62096577370b30af64ae0d3ef4e8dd11d40984c2d30812ea90c3d182d92c67bbfa598f6cf396886d0c1f487e617e1bab326f8be64ad2251aa4ea74d26a35b2ac8a884b238feec4a5de35bef02b0839392a87036ea820632c17894bbce3e63ef71a74429cae255d78da58e3bdaad2a7205e4a7a597a3054289113ead4d3f8b2ff57a19c83a0e66c1a4d84e25cabecf27a51b090fa21d01be6a55e3d88d25cf6756b35b6c3be78ab73cb6d054427e5b5fc20c7c5131e3dee4be50259a13402b73c8af61de7f3c21aa7340531d197c55878406bd1c009b35abc05569a11b6d6cc0edd3cfc3afaf10d5277d59a956201074997912126c5370339d66af1b34e25a8d2e4202afdaa3abf5510317d3bfaf26de3b1ae06217db6f75af79d80a4a2f84689bbe5cdb1ef5544ff2ebc9375355366a69802e78a805ff914e1a1ef62137e55e34c32639ed13aae650c4a7573d0a208c2eee9128c1a1ff66c2ba945b0558573cf1164d1e3a853d98bc7e4565dc5f3315379315d344d7d8f1fe74cb5580ae2f7b7b6830e49e92ee7852c9e52d1446f2d7db097027417e0974f3ffcad95cfee0c785986b5d573e529544b17b0696b537761c5c8c8ab7d3749ec6ac6d34f11a1c25251bdea126d2777aeb61ce6186371afc6e8d27e0b377ca9f8ccf4648b6bb0e329a97d8c63d677b5df59ceb8ba13f16a0f5c4ebb1c1923dd562bbffcfb2c16baff316ddda616cb32d54cc16da985914d35987ba5826152ab482276ae1a885e6ba2b8cef44767fa5d2fa7402a626ca2b6adaeee407b5e3d0303c1c37fe23e0b416881c89c406b8b658e6e4b338ddcd9abf161de51a5fda0d30c2ad70a3f22ce7edfa9c55feda4b1c6a877cbcd0a8181cf9caeada6336a3b29c58227735c06d18b7c94412fc47699f6c1bffd81817cf52d", + "644d9ccedf958974573a0cc0caea79a9e61e8e924a7c3dca2fe6edacedbd2be96a359e6183bdbad6772dfedecf3a0d2265ceaca15f42656e2b2dd243e55b17bd7431b6a5c56035db97afb0e350efb0ae3bd05a4159876b166435d5555890b6a58938e750d2118df3914589c4728a96f2dd2260ee4a456866259fb79a1d6f682fa47944599e7a3266b50f80d898ab218cd37d8d2b2c55792a0d890b6260addebd7d34b1507bda13c28fe6e75bd85d4266c7b9882cebde50550e4319d4419caf11d9da3bf3d846dd3545cfa6669c4d689bc92b2c56e517a75483abce46d1af8c99cc155b14365daded54cb080eaf12e2bd2cc1ff37562f64db546e688ba50a7bdbb5f3b45d19ab3abd55a2286e3e49fd56facdcf67d18cf3b8d77323e2ea2bb28e0d54f9b4ca72f104ff020000ffff", + "8c5d59722b49923b518f856fe1eef7bfd81892ec3113906fa48ffa6155896232163836d9f3eb8a02187e97a6b5bb9b2194c3ad4ad52fe761aae8c14664340b60d353ebccc474ef95650c74bb67e58003b9ca4fa08f834f6126e4e0426169610141f832e8a9a043075851a0f129204e9eeca3332e63484c687709c4be1e84e7029190d80dce8c25dd73a1d7b182bd051220646763b84865367cce611203baa8f00d0e6ec2599b3b7733f58e87e0c3d480556ff2d7ed9030876f4e5c44f7083fb49d879e569c6b594cfc6574db0a5605ca38297a09e48d3e46247099c5f0c72df01326ba1da0395d3d71eff9296d7f70524cf0806375729d552f106432395766e01f268dbc65c7042e5f366744d6ee491ed1eea9e2f3111ce7cae40e25796420b6b3d521acf040e06a11c85ee4583df59e4708fe95f6f11d9fd37a0eb847082a8d8e9f5afae720cc359ee57045e87c2658fbf361aff7659518144ad0a91fbe1b2dabdbd6676577559c559e00e3301ddad150f89681d22c7f52aff29b845fa3b6735bac1c4069697473834e0be174a0d08a62e12009e8e66732f033004ddccbac5464ec087cb5c5bbcb0d9b3625f48def19e0475e57d73c89543a13378c07a08d00b266580de169e237a6fc436a557a88a2fca0fd60f237bbc08cb4d8ac4e5ab0f9a5a22027f13aeaea7b82f9bdf0e053e7f4382e7f5ef258330cee175a2f8d97797d331992c499587a08190fb1cadf42e0d8a51bcd1ba4308314b7b5f9f5e679be83c14a92237e1de89a0ff8a81da3412aeab9117821573ce4183f80686396e49c9bf7b0c6160e1942cef87550e07c16ee0008fb6f5ad287db025838fc1478df3c73abf7b00671cd2c93e713b749ff81613f66a38bdf816f6a1f1f7a3086ede5e276ab0b2a5555704b790475a6dcf93cf22efba96a7e8d44f710840222b82c19dcea2b9c9057151fb0313b39a1eb65bae4118661c2ddfb8bb3eb33f779c260f317d1e9ece2eae31331a60024ff42523fa337df68111827e5294c8733a7541eb8fe429e4d5d13c45b51245a654eb88c72bee9c137aac78dac6a615ffabeb83bb0baae6c8e7dfe0dedcf1a33668feeecb8680795976d94d6d3981079bab1989295f93abbf72d6c64e106c332dd9b5915b39bac203b14bde04332e2a4e8895f802e7bc6701d10270f2f41c6f031f7154608b098d9c6f062ce0ebf99bceefbae335710650d13168ff5b763c56b08bdd6d8125295ec288253f1d2db9f82a39620fbc9c723c2a01d8ba88f085615c917dd199c27cebe919429dd1a4086a0d5572c3c621504b5299aec02dd8acd062cde913b09de6be71ffbe8606435f21c8c28f44cb3ca4668911c284ec4035a66d78ad3f7faba7a03f244b81db983fd56f408433636632276bc599b4e54da96a89411f766a8bc0cf4cd3467197c60fcadc3661223afee382c09ec83cb6613914c2fcf63e8eb87af3bbb9d2e9409c4ac6784a46b1ca08b3ec00ed6286b027e6b97d7bd8aaf5feaafe68cd033017fa1d0b703dba2107ab6a0f6799b0f065697a91d7e369aef9583ff3e85f11b7cbbfdc36aeb0714b09cc3dda2ccc36789e954ee5dfc60f17a78ed88abb86b110d2090b2de4b26982f7d97acce84cd111a198e3619596d4e88e7a9a73bc9075089f393363556783079960ef8caebc0cfed64e9509d6fcf279d38ccb71bcc2e4a052c7c19478ffa385b0c28db3d2c9577a9fa69ecfe1848e1b962a2d24e968b5fdad797e1105667f7cc5f4e096b78e047573d245f3af30b6bbe0596a781a39704c62073c25fa31d96bc315c8a33d4125ad808792486bcafd6d9a55a2d6cfc255e880c2fd3977d964fa4b03342383b9484119b5d0008b01ba2e1ef2e9937bc4ae6db03a8c84c81d285dff16cc39b9d8a01f14c208ec5f5766620b0ebe3b888", + "f9902ed448cab99d8f641ce17c65419fbe794573be75d9e6f0ea4e353733364941fd135df8a1ed73d949fa360a3ac6b6c3a3fb93dc19485eecab39fe7222cef5cbb42d66ff4b11a5039f56f387c50d24fcecd94879a37b5b78a5b0baa768716215d7b2d166fb0a331a91bd4c246386c8c3270c1821371679f6c2a52c28f77125f2abaf837f021ab0b5f6200ad50c5e810d92f7e1aba3e524945db18ebf0f8286496658942bf323fadba99c1ae7f92cfa5e7156bd06caa620a71091be768b3035a0bbb1b2efe756845c531bb7d9a4feb2ab3ce356b908fba732ae400abf562c51ddf62b61bab3f1e0875f9ec873823c0888338a0e97a3dc0fe3be244681a59ac376e5e3093d4e86e3817826afbe09c3e7e22b103166b7d84196339d42f7820f1a9a4bcb225685c7c93bce3909bf7d4e0b9bd521928b234eb0c60f6b17a057301d3c593c91bc4baa30112af71638022b7ef323fde79fa21a4c37bd3ebf49fe9f5cddcd26f00186a88d337db15704f08a3b723063b27788013c03232f453764455827339588e509ebf16696f378e0b21c78fd9316f83c9434b6e73ca7f8cfa0ee37fe34fb93df7f7e5508e53cccdd6d6783cd19ef6656d766f630597110fc707277c05c3261cb49a1852aabdae13412a3fe8b3fe66b9b9d188664ee0b9a8a7904c45a78cb631fc7498e1161c283f19f50e98b2298f068356765cc0f082639492c8e60cdf4de0e71528aa5f87908e5b9292e1b7fec40b4e480738affffdb366a3e8e3930bf98c48b769a66312070f3f15f0c8f9fdd09b6d039d5d283a357cc0588a0b0453d756a3c759f383803b2b4239b667ad4e8d5f7222a22b4cfdc10fd146982f9cd12f55c88787a42311d6897483912ef06ee8821f8d9287eaad617128d2a5766ec2235783ec433d16e37459c772c588d0eac6d26336a8f4416abaa1c69dbdf9f30ed125d3c7eb3b7f6f7e82026575020121600d8a95fd0fe67d320b74607b28521e49562ed9b317145637676fb83f15638cd6f4ea336c4a8e448c056f00fb8b534eec0f4bae61212ed1b1a7e3df1731f7cce698319c37fc38a5fd2fdac8407db3078f3b121a6ec8f7519138b0424e49cfd7e5d53a296dedc52e7c669ef9f6ce91770ed14cfb31693faa278243e7a4837b39f317787479e57db6de07890b0acd575a67000acf60a23508b6890d07327d101322efff90746f0cd8698c765e1c1c1d7f4fd03c1ae54d66770eab121da1c195e67d5d82f9e0e27c94fc6d28b37f11b3108ca3dcb3c055cb277657b490eea9b2f8fcb0e763caa9f26d16715467ac935836c1673cab648c39900ec78f0387fd4b89d7f19b0de8e270dbb7e109bd9f2358b08406db36403c700db9fdcc25488477208978fb8c5803af580eb9f79830fba35e34cea41566065469f30310278c3b84ac99d7773268cac253eba40e0e2b743f35ba5d0c5240f16556bfa156686b5f397a95b49f6efdc6e693c90c2f199728143e2b3657324c412a157c1325c21bfdae230cdf516934ba4e9f8cc1ecc1cea99dd1a5bce43c1919541aa21320d5592b77ea5a873ba964a32301f5e13de1ca622d5ed3038d0b2885b088531759519cdb449596d57ab3560f6728001e9e5368db1abe7143455ef6a38a7d3980ff08bd759b88546205a1124ecc3e68a130d5ed6e4c450f2cff04b715a11330c1611e712f1856b34e6c2212b5c6580a8903b0ec53f681f50b9aa3bd9b1ef751f04c05751df6416370f767ddf3fb09076078673e6f3b30e4b1a0183081fa6881825c3c2337bebf0ae7954cbba72c4188a510e4ff0aa977f59fe8d34293a30445f64eec0f4c5cbd6568ed38cb654a864903337f9bd0c0429951d15ccdac3a6f4edd0b60c107131bc965e9d05015e62d9c7ff46176089cf7116d2dc1e794bc069dc9e95128c44d7030b56b33eaefe", + "df34138305068f6971586c9dcb791a2439d86fde68e721cd31c0cf1d6e74e86d89ddfa49640b44f0078063a4724f9b76f6e0f6e7d86b3fc5140281e5a1fc2bccaad6d90fd248bb34b419cc944ca1188a98a4f24d7d46df78478e0cdf69b83ee89a442e72e5923a23d66738640ff7c04872edbb87409b0667b4302032e165f981907c1ce7568bc8e1087188d29689049b38eb13b51a642d5c68f17ca7f96c996694212bb6da0f36d692cd4fd1091286d735fe4dfc018004487a3165bf28ff1f687d6ffd653430035522b674a963f8fa33271956b96f381ba061f3fa5982f2f17f3f4303f3aed5109bc4c4e721157dd5fdd39ef9c505185d9b1b762e103b6f2e5c7d425bb6dd1bc1bf82c181367ccf2119e721ee3c542245aeec9185918db379dd97ada7e199cbc15b2c7a80a360262f5b6ea4bbbe232522f04f8579ff810a7ccdfe6a01d47ffe0925ab6149300a0ff50abd68399121fd1bb6ce30eaf584541ffe4734472718ed188365a9b8e611ce5906b7f63303f97976b59e5a2080dbc034d08e14302b587d06064b9eef4d3fd3d37582dcb01c5b0edccaaac0189a41b94244ab30de7b329f2cef1e763f835ebca1f9cebecefd93afa18f99148fafb41d7e9ed6831b5a60ca4a37894c32ffaaef8c27be270e61566a3ea8182e979199d12dae129b01ef887a94bd0e339bdb2e81309009d20ef20dd40b1b702fde8f4e577b184f02eb378f1aa707fd9bf2fe68e00b260e0e9a85581683930423394fb3db354c882c1cf9fb6bb9c6c7a367305830cc40d5ab317b741701113e2f603ba8d0a6b189525765c58e00a593b6c23280be1b81f0603065bd8ea1c888dfabb275a6d79bebdf92e94bbbdce79b44ad9814658db19bc09e3c76484b0bc79f3e470cbe079736df7fe80fd36ed2079b06ce52588cf6192bd9ce6f755f1ab97b60d8d84dcec866203da34c16bc129ac85d38b4d475c54d04ffaa9a8269ad967784cf3dbde2921bf8aae9a7dae65bfdaee3fdc5ee3920e049b54a78535bd4925b38d5c983305575f397a9fae350439ed24440bf70d94952173a98b0bcbd66573a4c77276443fcb703941ddffe14064b2c05470383ae0ba86fec4642fac48411c5f3e216cbf7c6b188b43822d365a157e68a6934e1c1a695b03e76c51f7a273930fe14f494251b77dc5c4a7b5f472818154340f674f25cef037020b3660fd6b7d03b8d939501b9c399c93ae7d853f0cdf9b66ae9dbf1808d20fe306660b26be886c3b76ed45de9c362331484161b61bd0a3bd1a58c98d9c38f82538007fdffd7dd7cdbbd232f0f54d120b2f8b76ae4b5f9d5810020270164e9e19e44e913fc9641cf91d9cbed411d1cbcb8b384c34e61f54a82fb38eaa4b52663eeee8a2d00e56762b87d0abd86ebd5391bf68d25df1b239da5ebeb21e442a3f889e33b9d305071601bc61d311e5df72ca1960c4406f943e2fab52b4f26ae052f4d6f9f959ec3f2b112f39416127c1d45d16bfb8b3e74fec74ea21ba8c527bf2078d89f020300f714fac97ec971d53823b77f4971765368ab9795f4d1ba33595b59a49045f592d28067c1d5b99c3f9239f19fb930ece14dae5467d2e94b39568e107ca7b70e771a7f8f47a907a0f8d4b382f68970f04801f471554ec68311321418c65020c2ab08cdca228759475d3e39d4bbfec172bd765cf00e6a0779db62c587f898e3460917c362fb6719e3dce01087fe6181570806e671a5e6177ccbaebc7b95fdec6ff986b8d04e2a75089579a41ef315d8d82e668592e6849f32fd378ebff20707ec11f5241c027f6ddb61f6ba2fbe74c6cd6f04fab93b51f35259e2f3b314e2fb8d5f97229bf29fe5e6dfa66089775b3d093711c62f328e5cd5b6682394e2c14637b4d619719cf2f3f751723c64ecc65f6358f152beb4fb3d8d2746a2c92b85fbeabd", + "477ab4cbb5a41501309515d55786530338f68a5f0f84c2c8f27a69ba849f3865e323047dd547086d9d4bdedf2fcbd744a323b27cd41475017c4b5220007229f3de53362b7fff0303391398d8761e4a1e29d7e9b01d86a456906d082eb17df365bdfbfb02233e9b16ffaf405f3f425c9640711e1ca99a02c265721b85460cf025b8ff9a40f8efd9d5decccac26c6fea75c6b63927fe90068299c8cebaf0828dbf62c31355cc880ffe1f8fc017f5a4325cbf1591be662efe1baa1278d15e92c5511bf2272de61ab5478e831b623d36aa64e23e4f3e998f44a8f979d88b08c2bb64a880a2e021fe11ae787ba54cbfa58a91ec3eb59d5be7becce7b17254765768f9382a61ae88a0bd572a1f4a4b4cabec48e516cae3d188f75b65c747ff42d7912ef09cf342dc9f7bcfb12beeee842952663b7bf2ffff0b0000ffff", + "8c9dcd6e5c4514845fc5f2dea84fffb75f05b109b65090081b1648887747d5770c99fadac6ab4856127bc673ef3d5da7eaabfb27c8e8200895954720cfafd7e0f79d1919ef7509dd20e024bcbab45cd192b4e881f89017c2058763eb1551f7ef0e3d076de28a8c0b1036ff1f5c738614e8ce3b0a9e3e7a84bafaa6536949486ac9f4896c632886c928689a7d82b51f8217f8a09de70689625297ceafc9eb33e1579da196af41df21dd1c3db11a7eefc14417892b27405a4247dbcca5864a711cc8b2d371f3e35ddf35a3ae825170a67e1f30fd806fd2941f49d87bcbdd1f19c7e8d60b469e2adf20106f724434c8b823e7315dccd9b16a57e442c34977e760d50269c1182db915104af904e89cbb9451b7166d48ab0fea65050c796396455f8f804dc9c9d6b964319cf1213a3807739a6b24683f5a755494b5b4553533d847b676f9c33de6d16bcb0ef455e15837747e1617108cdc5195aefaeeabeb07456a667b7dc2d0d97b6b58ec54adf86155993502f0e1632c5b0122d0099871b8bdaae926f073f94a9ab2cad40a925646018e9042f69caf45de6bc846baed56f0f4661e13dd8053411f62cfea1ea68db1d63a7ae0b6a1199120f1935c9d48eaa0cca3b10b42a8908e5ff696fd3997ade4c1cdf309e1ec556565d66d2c8a8590489418d34f29ca73c2365165d280b4dfd74cf79ecc7370e3fa14e90c4968a7fb6adecbebd734a7c38362dc9f246e0f98158dd807cfb85c8a89548fd23e11fe966f6461ed1c53973cf45342df2f3f448c99b00dde97820fa18a52c0bc80bbcbd3b1abe6ba645320665a438c57b831bb4eb5be1ca90ac7d95cc115f371aebb3e6f55a95af8c015dc9833f17368d9f66b0651331e5d405063dec8ce31c8a9100319aeadb43acea5c7c2c5a3c551f31e68a6425ba13c2b143cc45527b3c01ae31097cd157c812cda8b9ba6729b2257fa93ee74c6492a510c5606e9008992a92e4fbf5f8c683dba05c14a654cb0c80e0d1f91effa6fb8932e31dcb14aa32ddff97227f774b4061f2bfbae76547dc6ed17ebb8c3eb17d3d23debef7a5294c086e298964ddb7634fb8247fc9a1606a260bdd018a9a51c0e95c250f585d541b4e84e9e3c7e92b3f606a3b94a7f005746d3aacad3f82a52666eae49f671d35349ca8d63e2ea33e3a5328179cd8c2baf86faaf16b5c0e6a5e4b7a7b955709c40d5cfa9cc48dcf10fc995164e1a32ac4e5f17aee911de1811c5898b0ae54ef0b5e5276bba85fb23486e86353e6188083d80928b9d59370d178c4fab80284d97277b62a77e348fb38f1c2d7d06c824cb4f4bdebc1d329a7437f89cba0c7732b70ddcbaf56b81b22c2f771dd57546c1efa14429f5863214eca72f7fb2f2f78e4656198c375a9eaa6ec5b0964bc4f582680d8b1f9d032293b4754265e91d0837ec662d9e106b95567c9f047a7a578691bab5d021ac1c0c81784b0b4bfadfc6ce19bac5c28e9fb76f951907acc256b2614637c98e3cff8c8448845a493b670869413efb43dd3f3eff6f60ded250e22c053c754fc6744512803e3d037ff503240c5d75353a3851fbf1818cc066a64bf0085dbb305eb32ea0b52e5e2f6e3b53913c78fda7b6b9eef136ccc16df255ed852765877a6ce10a391bcf1c85775bd6893fef32c68e90a0c554cfa9e5b1b84d6a20bd69ef687ccc15e123d14dad8adcb4a8214759d1fd56dbd5e80878917a9c7c28f774cbf503e818eb64d8e3913fe5b4044b70535e2aea06baffaa484ffa116e5ffb69fff9f7f557feba7df1f1f72fbfbefefcc7e3f3c3e3eb6f5f5e5f5ebe7efbe5f1ed7f79fcfaede5f5cfc7e7877fdf84effed2f3c38f36cf86500ea8b312ad11fc9aaaa43b22074b971ada3fc621b49854a7d031eacb8b0524b55a143bafd2595016b31d62f60c909d15a5da6d8d", + "51991b2e6d6379300febc6082959be7859237df8dd53319e63bdad8036a907864becfb01db40cc51520709b852efabbf6ee99d04c2bfea29171eb883861b95234113dcb49c4665b7f6a597e56d02baafba3f686d3d009c3f55f2daebcca3f59c5819d86ab1b9332f4d7840deef0e99466cc776bfe1a19f07426dfaf7f0eca84ead2f872dee2e466464cb44f65780b6286e53936cdf4c15156c12215051e147f3069d6914aaeba899d0a224916fc68161af99c565d29a5d6cd7e1b7a1344cdd79c9a1507964f8ad4a6c57bd2fe892f5b4bce54fab73e255ca5e0b44abbc7d2ad3058f5a2ab6a472410319ab6d45f1a9685e72858f1fe208667fc8ca32e19ede5933436765d55119921c0b6bfddd69b49c21ad33a38afba0286f95d12e0979b6328f32a9bb4e1bad96fb23e2d64a54d16a8703f53244f6b203f56d79e8ddbfcd5508d9c7a00943452a31d03aa871db069fd2464677cfb645f40e33a98c2c40236a3506255025ef8995404d9500ee73ea2b21942c42c0fdd6f2e98dd3a13e95cad0d35c88decfdafac03c24798d38a3ed45f1f7564734d8bd56f30048deb058a0ab479d2b63022f0b8939f1ec11b02f1b0b079f80387c80ab2b288cfa038d67c565f2a2cb85b5506929f7554045d16dd504a032aa00431301ec82ba0a99f5ba5b11ebec9ba768ab86a9f678b7b6949a462577b448c5c3fe66437c618adeee3e7f6746be7f845d0579fc68c692b10e1ac91cdd0708c99d6dfe7715ff030000ffff", + "8c9dcb6e2dc975447f45e87903997be7b37fc5d044ee86210396271e1830fcefc6ca2a1a3e1149b33511405ddd4b1e5665ee47c48aaf0d0c95818aa17ad256980690ccbb62e185a859ad85696450a437b8a334330e16b4911689bdcb36e4dd017a5f46a60329a3e3e103d37937eb2808134bcf39d965c3d7e7b672e397d3a317277630805734ca8252b08d6352f49905205b86639f232c27e7698d3e3f4428e1c62fe3de6c39bd8d63176769970da55cd7ba63ee5c869741d7fd79fadf3fef67c23c7dd25171805ab625799dd6e5a3ab707684550f5f9acd6290ea98a3ba4614c9e268e5a6ad985a65af6757e183ccb1f392fc5663c86ca98f116b5a7bc934d70d576d229fd212001fb2ae152ace4a3d1117537eedfda3cea81ee083ca28bd6bdebbea47bb793e2c14873ed622b6e644d02a57f8de6141a4247eab9b65ec3abb4b0559af39333948139baeb76cb63b072731a7a9e879e2cdd87569a9e8c291f3ca6f758d616eeda8274443cf86d96aaaaae3c4365b7003791d2a96a9f5d8e5d4ee0c8f233557aaac28aa54dc7b6ffb3d59fdf3e41c11c1255bc489aebcdb3a6790f06cdf7dcf9117e34f0e8d028ee4bb52a32a78f5a52a4588e14dcd09686a3ea35d1f3e501bd31afb8a31386d3c0e8f6dbb06a9ef662b317cb9e637c4d11afa6f753665b6799fa4ad6a67710215b797a649555175fe57191aeba68c19e8349f5ba4a63b20cc4dcb1a47f47c932ccfdd3d906801eb334edeb0d94c50f11b246c1fd49bc96d795ba48903bd4782fae705d7777aacc28039ef718a1c814afd0ed669a21e2192cbc80e1454cd028bf63051d371d786253fb548a7b9c64c675bf6b31aa84ab46b3da651a471984d03af71feeffca1baf84243ace5397e944ed187b96b89fe1ed6c7b377317d20a0bb62534efac0e2a1bbe300608d6439369b427dbff926fc2ca1eb5a53fd86d3c6caedd83afa9fe99a097bf7217cc2a571136b5db3eafc1763ab62697adbc527eb27e761ab309a92d42f62b834968594d95a6f61b1049c247aeb85db0a2bd310eb5a475ea259d9358316d657818dbfed06363fed5072441b5be1eddc65067a3f21483135f103e89289d372a22b36ffe08629628ad689b5df233c611e3ab660a6028fadf27b24fe5c723aba60705db7cb8d474b2beb0b9164b6f13f25c632301d22e6de0c5a28dfeddb4f0fcfa6bf36f9c89077d3ed1f501e931dae897e464a9fe090b5c16702ef90abfb9cf02a4d0fd23234af82d9a1d17727d3235578648b39f40c20ed620d4781ae2351b1257af38d2442bc611bc5c621b4b424828368570cf354b7cf300c50a170cde8d312e889e3ecb6ea201f1998a33b23fbb40b91654b749350b73574585b91305fb2b106609c6aeb0b1cf1d58c13392d9ea571c0a88ba9f5cd685a4f431cf9d502630f2acd2cf9756b1004641c5a6cb31a268f97720698555958d30038a19506a64efda940bab66a9bd6a730b578c0ba3ff9b54f55c7756fc70e2b3e4b28c10f62d2c9046fe193499c26ea652b42bc78fffd110ac78828dd2063df3c45f0446be8743b632c87798dbe3eeda2cf62abaca302d751f8586bdb6a9bcd92196be05b59ec03eb5f36f17afb6c2b186b70a9ab9281a8be2553e3711e39a5afd5c0fa6d72b3dd520344892d6efa0cf201a6c281c62a9720e1e8365b3866128bee2c3367991764674e2306a1db0915b5813ddfdb1276669776773214ac66bd59835006738432ddd7f5005af1a951f150f32de466f06b1e26c12077c74054bda8726bd6b9f6502448ff74039d392f4e3bebff7a51571facd6dd2cf820cfa459468963b244d65f9dfe4b27779e0f431e5a62daf40f42941a2e28414dae671756b832d31849336298c29a93cbc05ea59350eb51b0b0816c9049fb91d5fcc0271fb417ed", + "d7c9b8d22dda1a78a28da07a98dc9e36c76f46012ce48b192bbb7790d2fafc0d5e7fdd4c344a677b5379a9ba6117faded61863a99eba17ee1585b529607beb71c351401bd141caaea98d79c7c8abc0309e816d7dd6eeb194cb538fbe4b4e1f46aee669b06df3eb69ecac47b5cd61f66e83dca3c6b326fad26ef331d918903581e6f30e741966bd8420b03c0c6bf7d587820c0f1cc150a73bacd1c5fde19a3d921d97e61b1fce4e0f790d7d36f6faee8229985a53f87b33b53a63eeef5ca3367755271ddbb6ad0b308a46d58fe11bb0ccb5462ed0747cdaeadba65598fcbbabe0b82bd55e1e4c6c3d3ff604bd6e67a2a1593179c9e4cc34a1ea6625613cc271843b5a2c340b75abc12cb938b9ecb3d3bd2ed1cedd82dfd836a9a7482f16890774b87ba61d2a1b4369d129773be5715c9718860f00d468c3349bc43c0b09cce4468fa4464c0baac21d23333262c23c906a35832f1ed78075c0959c59e38e8d5cdb31df75b06d1b066e8ecfa9d38b5e99bae5389126cd925a4e3502827ca895bb4cc57a52a3605b962bbd020453a1cd6e5b83ab888fabc51a787426367ea7c41e369db239c8f310ac8360304b08eb2e4d6b23a363d9d97bdf55cc567b2fb696a8cdeeb33c8e7429b04566f0eaf2383c75384344b6beb28c07978588d7d97219d39ae9e5565d5052a62a58f751916ff7e31f0584b7449d7863b7de9595bef7454561cbfe63c1cc34e53bbdcb3257455d274b6fb89e7c1bda32839ed716f6dc59dd40f209edd91ad1c99da7710ddcccbd68314ab9e26ccd161a1ac7ccd9700b4788a2287eae9188e6ba19162c76e75eafe77a7cbf26f39c0c13e4f8c12cb8d4b46b4abea70ead6b985697818cd1bb1265b2ed47aeb7c5415b782ad8b53cabc8c12ece969b6ae3d60a4135ae6b38469c87c33c3b108debb4fe01e1a9078420a10b4013d68dcfd18d56cc5a547f3db9f91ff405de0404db521b6fc554133f0a34e5552f96ed86e8983bcf18cc7fe0b11df484afd823f9d6648ca437092a585736410b2b6628d2c5c57d8779209eb48c6107c5202fc0dcb7a384db511a64155591c504e112f9c376fe113893266fcfd3e8d487363664bfdc6b7523f63223212da3e722dfb78e27922e6dd09cd157bac87ae34f30ca71078e5f0da301d1a73b46ea2498e9df4090b73663d0d2571dcec098207e75b4bdfad4d21bdedf1c2e8c6fb3379b4dc372514008912ab328c388b76239ba1243a3b98e23f8bc54163281c158f6394fb43d0dbc1373a8ad3ea977f424db3923b4e2a9684574cb7528527ec0ce5d4cebcdc3b86ca3c8e76263ae0680c008d62795c40cc7cc608c40909372c19553743ae6ece11d094d3462fc55960a893902325cf1c33ac9c628583d878e72b0b45759b8f78a435ad3fdd077b8976cafb62c6a74ac5d0c7ea313b847c5004a5985248d53d1c71a183c9ac938c7b49206bd539ae80706958edc78098a273fb37a6ce63cdc28a9bdc016d8de8bc66ab5dbdb058cde383df4f5213d34c1ebea871cac34cdfd8ae8bda5acf8c8be4a15c7669da598bcbe7794c41e72dd2dd50c11d71a7629c00633c358012eb92dd1b3c62611c3a224a3879d90b6fafa7683cf9eaefb1c37f8b68c364172720be9bc7809dd60712d6be384b5fae66bd79c96af789b420408dbed2b6478046aec6d23b6e57f938e57a7e9b6695d6259cec36c4d190e24986b9671ec330eb549a81e38577b10ce09932c1fa1800ec76c37f4ee4347ed525393f3beddf7b02eb974891432f2ffffdebf5f5caf7ef2cc950d73d1984e1e75cbb6ac03b5818dbd71091be98380063788dccef0ab6d0a26f232836987eea0fb1ea668a36ebb4456cf25277307676f7358d040d3114bd907bb7627933151e8fd0797c7afd73ddc8b8942fe68daa701084f5fa175fa2015bfb66a9b", + "179e819463381fdba82684f6ac732f0513933fac9c498445800f6cfb8179cd6f78cca85d25a9815744a15e1731541026a699530c1c4d187e7be0ad7d7ba1d008df34df765603c31d927dead6360ace0b377f0ff4bf72e3f428c6856be86234f92e312deb329f27703aa1b2006e52830678ab1926cedaac028706fab56a7edd6b3b1903699109bef6c691293f2a57d032f5f788da9dac078adeb549031f80ad5d2ba28c301825bdaaeab447c656575a6f20e8c47831e22cc8ad531fcdb0c188a2f3d37dfa159bce9e592e1156145d11937d4d93b4b2eb8c2d67be4fdade03babadd0bee52ec4b2a056e0e2354524f293d66adbdd4d3ce3364e4cf18ab8d08a7de2ff7a3f4f9698bbbb7eea43f528fd94481afda0357b1fb758b21bfda15ed3dfe767e425c137478fdbdd078cbc9da9164ea968f96cae2a230d04d0b1b87ebd7bb1b3669a3db252b3022352b51f3aa5ed51bea304da96b2759d321866bd960a771405a495f4e58b595ff2cdb2c4332b0073403a5f11aea53c016bbb93076b6c1204cbedd3a497696f2830c0c55f74f5abd0c9b2f90426e5ec6f3c72d9f945984f5d6134abe42e3a1c5ca3b7f3d7410902f83cd325b68d51609e39854b5d76092e812b5c7f368f22c8482969dcba87a598e533dd973b6fb11dffad381e025b7127250bfeb95401f6f14f7330dd5edf04516ec2ba2b7ddc5cdaf6bb6bee86c4d4cc2105dd1503b904238451df5a1277f65cce5f3e3958e6941a3b7bae596ce568c087ad89b068a994f12a624ea2644fefa4367f19508bc96b919ae4303901233752f4818f936804de004b49a9dd6308b3a178929c8e91af5d64cd2c2304625a0571f1d2ec05a358c07f79839e3b2f6de8a5e098c2d74bec1cf5af49e5d9e458fb8c400446de37e536577519444ec4dd2a5a7c30fd2e18d0e36c60e63ef555e0c0751d79869418783f1d232fb16de191b25e1b45bda5c2513d1aa6b86850ea0feb4417ee88e5cb332782cec0e35adc52492cfccac6ccd9561b0fea93c7d8d0c0b2f8315eb37210340dced361926414df598f6c557f3be58d6aa57eb53f2f0c8790a9277f93b5798eeb400fa8faacc6df0c3aa1adc513ff19adf6e16af9dd2fb9eda55beb681eb03969fa61a3424f44b3f69b2955a984b8ba1a73bd53087684d4e3ae02a8a8fe89fc8d2b7b15e5d6fc793ff596afe99a7a2f51cee67372de6531e70e3183410e09012f203f35e77f4d3764e713d1e6dedcc0866ed765afb0ef191a0a35fb47399b2c73eaf00cd63a7ddee6bdac74514546b1657d38ac045af77c0af570ac9237658a6ecb0afbdf7286c14ebb7507557e5a260111a0e820bed0a66ed965de76fc6f706a7ba0673503bb4bf31da63cbb3941557513ceab16c1176e5ee11595cc1cbdcd3e159dc5a7615a2ab0c3d35a0c6dba09db7c67d4675acd48924730beb38aff3ff324948d6d80fd492a84e74d9a112a3371602d5bff920cf2e5c597466a47bbaeece6b2ab701a32b650adc1025a824aabd8a2c16c27e039b955d7af0d56452628143fc0c4babdfb3dbb09a00409e491d0607b2713db1426f735d52684efd6058376c5d82d4a5b159d723aae2ffea3ed23cbc38fdac99f32bc87db26e4f5764549d3c955cd4bec6b4c44c3fe55d4efeda62564cb675b6a8a82ba615fae8e867b83a7a56abd32170ae29bfc1c1a644b5097c2d6581d33875ad7269ac1e4c315deb891977c0579844eb607a08a6902e18d083aafb6111683d52c681af992e48595aa71e3c69855ae411c9ad9fff68d3bed3089a758dcbe203b5169cdd8b26a8dc7770ecb97cc83bd05f696edbb1ecb66299e6260b7d0ae25cd61653256fcb563906c8b43fbbf1b16cc7ce18610e014651806a5f7b77eb9c36c7955554493cebb68a9eb477ed75f95d9b668e888762177f4986fd5dc6", + "080093bbf5bae8946a35fcd328d50fec396269578c455ccf5637aebfd74b1be686a92768dd306dca0e78aaffb22fb45c719ebd4fe1da9e8434b2a7deaefd5853e54f0269f29d4c86634a60bdfbb8e2c993ca66f8554c46b6f23ba875ed6978b73ce289980fdd41a024bf6021716e34db4ea29cb21d0024686d6293ff77d34cc6abc8fdc4648da275356b109bba5d679f13f7cb562b395e6a73dfd3c394fc010af328cfe228c9f574e6d576ba32649cd54c221a337c1d7a422857eace0d89bcc772502318f598bb286d6bb8504468a0136248b3e9b1e22fd506c03d3d8098ebacdb197bb4bd2e59a4b0efbab7446fe88f07b4771f01276ab4e6cda4e0419ef12b82a9a11386bec34153393f7fe9bf7e3b14addc9ca6637299ff13e7a7aaaf47e69fbbc6b0b00a348436e72b8d41faaa8edc9dab9a69a39c38953dccef7d930230c58c2d2344e0fd7daa166631af75201b579bda1141355b69995cd02ece3da11df6e8e08bd3bb0925f65689246efd344e329494de3c28e7c64a22b68ba3f8cf30353ade7a6d96916e6d77e0e1e83401dd55e291cc71f4b45a5049f5235051dd63f84765b67fa002be529e8870210d4a574b0d5e4c462daa81f7c9c4a4d565b211880334e5806d84911cca097654f7313fd2aec6e6c4d3965b11cf0c076d3d3e28bbac21d6bbf1ed85962b4e81985393eb9e2856330a125f6c069c51fbd7ebcc643769173c8dba6e378dbaf6fa3d30ac6b2d8124ce86f64cd2b64e4dd75ac887f477b89b67913a91ee8d27ab166803b253a57627c0dec81bd7e6b926cf90c9d4b8565ce355d9a919ea8518926aa97ea5532566fbe9afb8ea60bfc4d63bcd39d3daa80a38860d5f67f5ef00498c1a061780a366907c2e166bd5f1e3781cf62e9f82e8a7a700b3652be62c111e3f5e551193c494eb08865f56358c107328d59e5c6ca0f71adda9155f39870027e520a0c330028d134e9f84803934e4af74389e4e12ad3d7b5b36f01209dc1ba21c29153dbfe8ed1192761bbe2c5418f41a56cc96c172d7c8078a61cd0badb62b5661c4eacb595b7c1abf1fbac020ee43e9022d2e51b297ed41b07d4a173e1cd4a3ed4e8ae6aae036f82c5a6f30c4d772bb7a4cb7ee0fc605b6830d64996940f14d07a69070421adaa557f4f6e5f8bb87f9072b63725b0b5440f1250d92303e41035f0dcc220fce44defe2e302577026545dca5de97794a6fcb5b46e7af874b1c85b14d5db266310065ccfab987c746fb995ff98ecc76370f8731785e13eaaccb29a48882a47a19005962fe64827d6d7edc87ba7e60af6c9e5dc8a2a0e6a5a6a3aa55204a9c6740cd8a9561a66a4c6efebc1bb82481b29bd7925895a2526efeb319f02c23bdf888b91cfca0feac0d954db7953f60bc6d7c2b46fa654d7b0f749efbc89d32aac2d3da21fb78f38416cea06c35d051294487635675a75758cd5d4d11d17651f36339b41fc5d73089d14c46088ad656df20dce0756bfec4457a8d575946d70290d545bdd0edd9435ae4033a21f7a4ea40f529b84fecfad08b63d666f29f81604cc65e8168771a102d59049a98d1c81eefe53756fb5334fd0a266c188fecc67c8a06f1c117af4ccf87be35c0417ad5fe04a79c99f06aa58050ec142df9b085ee17d1d5c6d5d74a96162f7b339ed58861703f76baba8662d2746e3c83d029f1f9fb892f125a5c68fa9e27e7ba0d6afa885df5c34cde74a73ce51e56081cd49567e7e5eecde0519d665b3dec57983632d616cbd7cdb8af3cd073d452758e30600b0f935770d4d9cf70929c15e1ce9ecb9e866fdca60ed33b30c3342223fb5373c1de192c0e8a7ced2cd5a252af6c9e9aac73e459cc319aa62459bbfe8e614828b30960b5407693857e7b4eb1b59954483644d96151dca7f5298617201b396dcf59b91bbb6fc6090dd8e68c", + "e36837a9624daa67b35d2e0020f923b8e1da463ff53f2bd054a036ebaab69dcc19594d69632945cf689144eecbf43a9b685a5c4bf09a0e73af557f70023e7f27e9af0e4dd8a4849a9307b7ffc53679c1a9a15f5c4682b9fb6f890233bc67d67adcb2ea82e11b93456627172fbbf9ab20c4d8ec073ba30e93f1df57fbae6eee598aa13e1ddf13d85bac4c0a06e55d46fda40a3964e21b3ee69823b78eaa8ede5fbb8d4d2cc952852d8c9ee2cb21f9eaa37deea14b14724a5b687cdc5e83d65b0f4f0c4e4a408c43bfaabaa0e42eb67e0188a4e6c25fd57158094969367d2bc58b714cc826b26a79b7ad057babb400db2b5d546766b5a5dfe98ed891b1fdb703eb1bdcae1c1fc52ef6864370b2cd33db0b7b115954981e807e3c348880d4cb7a617fd0895a59cdcdb0ff943fff36c545978f924b3f178a0853b29984ff7d69377373fd0c816e8a31fb9b508d810940b51a37f31740e3524cc2a441404fd3399d716072caa7b91bc516341566fa72d4c3254a62f2cf87b5dd1cddf668d396a50eeb6a6299df16bc00373675aec7dec832f65e0aa83ae568ed6cc95b5183ab3394dac0432f678c6ed89f016bab2edb9fe38a3428b0f24c7efd0a71b73aec3aeb526ce5f3ca6f93d492eadea761a218a3377313f1b0150563e7292e2de64b09796f331f9a217c1c6d656b9c148fc02eaefcac19323a6a8c7e8af6e144aa1944adb160d6c2ea446fe8eaef763cc72406bb58abc5583bf570ba492129dff8b4ed370d7b4b0f372539be6150900c740fbd2e7082c0c6ac4506c8d758978c7684afb60ea430b4692f8b056d1573d121e83f464a61ef5abc6c1438da4d253b04531f2421ccd5bed76a6f00a19c6dda86ea3aae2e9ba1a03ed987b1e9a05614383ac7e718374b5e1263a434e277bbfe63ebf954ab9533bb2b1a4f7291dfe6f7e83af407231aba7579e751e9b64b49c3e262ff896f37705637ab3d08611de6f919a535ddf97b99f06ce9d6f6ac889342e88908607486de7cd1d0ceaa33731fda927c88d474b62c1fb5da170ba75e5723c0a0cfb6286e429caa7bb65996d71fbc6fafd4a4ccd6227f12327d656b54ebca4fece5d467e64efe2a104cdc0d714d7785e56e152054273dcecc4dfdd4d0f119cef7d2ddcb18aa1d25af5001ce40ceaafb802fdc32cf26b98ac2bf493878e5cf04d52fcb37cd3abacd7a09b3d242e9161d500b15b02e9472852594c68c53a9e8ba9e8c866a639d5e8ce07acb174db61bd6577066599e1c4bae1ee6249bdd409694b9e6a4c3a2b88d5371318ae24cde451f1456dc46b08d4db0a25548b7d0b46f9c2cb74f9fa18e7b431a8f9015bac9856e14fc7bce878751bd1b716cf6e6d09fb282b8120a5079efe9f9c86dc278992eca0f5d4b047fd0f860104e7374fd1df0540e5566a04fac53c5942716653aa94d42a75e17c5d8553f818e76765ac8eff82442bc56f4017ae0e7570823feb02616dd259c032d341f4d9a21f32539edddc8b6cf9cd8a78963626acb89dbb61d0c52340b5922186755f74080b575c8f918466cc148b2d3b29f3d3ce21df4ece278bdc5944219d1d6893ecd7c34651a740a76438c6f06df36bdbfa7151beee2294d570db3953165b7710802da652707b3587585652599d1d8c6b779482db089ade11ac0eacd1d6b2be0679a8bc162bac082c02df591eebe7b6ada481023ad0135ebd094f4e021174073167c88f92d81b69e51573175382f88bdcdd788d1180dd6b8efce6e43f1ebf60d6c4a365de2043e1b57069fcbdea3652fcaf3e006a8db7a1c264072875d592a7c0a2315357057b6e5d99129b01341c9ee3fa5623d8bf81d8ad7f80ab27fbff6d7f3dffffdfc91ff7abff8cbbfffed5ffff8e7fff8e5b7bffcf2c7bffded8fdf7ffffb3ffee597afbfe597bfffe3f73ffef397dffef2bfdfc1", + "fff943bffde59f640149b9e5923de884253de5276ae8ca77c2cb52477540fd70675064ecd13ce579cdad7cf7b6fad6428242bc197b196d217e0d9981b1ed3034d77120ebe83d67d65eec879dc0a6ba6ea70edc41813041c68a7c2c680eb597a1c1085b5fac916b54a5e9cf4ff8cf3b5419869264b5d8150cdff06918f56490a56893a286fa466eeb56bb892eea8e93bb228207f84b0a8c5c44daaba3840db1fc96719a9907341f2188f0a307f5920e6bd9b7ceaa531642ac0d859a00ad142b8d3f1862a2def315e58fbeec289e9765a91fa393b96f36bbf36da5c6398e3f7fae83909371406b6b2eb97827f4bfa56a13ce14cb266107d99ddada18974b0b074f5013373272ec5ba25c9baa10eab3110461099798ca864de4fc31abd8fc55e8899e31869db547e7a0547144efca77de5c2bc594c53b86a5a66682e6b4490a503ea782a3dcdb26c81ba7dc9d467ded8b92595a5ecfbd196b06144bd15ecc5d976516f5932cd115e835dab828c44012db411998437401fc19b9fefe50fc56a512e9494a81cef3eaa8c388667df265575ad2454fdd6b0097d6c29386dd669ae5a096a711f5d295f0097ccc0a914e0686b75124c15b13c3d2d2385dc960b86ad583b3bbea782602b891cabb3611ee6e47409d1e17b6be797bfb8e54d81e070346a2bc58ab875d6b08064cb571166443854c102c9a8d5567a3efd74a9b3423892ef81f000000ffff", + "8c9d596e1c410c43afd4924adbfd2f16b0663e62b203e737400ccfb8bb168a7cfc88744895fcb2617db537d402cbb89f07a706db0a9fd972b1b19996e282f6f228d42d82712ddebd3bc32d9080d0d03a0602632aa8ad61ec957054c278c86cc301d742a36f70def18631a785df60c031b8c2a731c7108a6c0b1ed5cfe2776208e67914a41b88de693f2a3cc8c9e1de8b25e27da0ea9460be912e4809a91c776935bf4e8764eba6033f28b20db2707c92476e2434b872ba4bf63c1427b6e896d680ab88a70cb72706b62442cb925d44eb6e898e80bba21f31b561797af19e3ae003743a84f2aa863b6fc4f1e488626628f470c5f8f188101d9c3a200a8c9d6655959be231b5e1ec1021ed210df9521149a85110a91acf7dc94fc037a3f51d38e58b87f472f757db55b0248ab5a1ce4a20bc17113941c581f6c1d686fbd5f2dc06109310a05055ad46c9c0a5d64b2c220d29965b783a5f0a6851d722772d8c24473a3a51f81c3c56c54cf5e1bf439d8ea5b9784247d45fa0065a2ae7f9e24c31efce4f3667631331f1d51eefea23314f5a643e8584f04a084530f665d6df99479becba259f0c5f2323b5d145efc1b49507a0db54390c2217fb907e3e7e5fc321b77d5e4ec78ba5018f69f0870722390f1f074eaeaf36d9d38cf46303879f5795f1830274de0dec3c219a2a52fa425fb8d8a2142e06eabf1e1e58cc420717ffc140f872512e41499211e5b150864bb82523ad6c413b7f9903713fdef7d52c7381b007bcf8a3b9c3aa3c8c031db4d47176b6020e7745d8c1aa47573e0c5ee8f8517ec9a7bc0c8d838de26c14349c42456e5ed4492ae6a98597ddb06ff7efba4c7be7f0501d87a77c4ab47d4c179b876671711d341f096c19527dbc660286d445e46397c80d1a31beea4230a91f01eb473aee7b62d444c3568a73715beab6a11363c0219b964dedf9f5d2fc79e9e01665f3e12d6ce59b0162ceabd3ac39d2cb5d37cac909288f976a1b9bd9232f78244ec11cf5815f61b48deba09c8db9910eca0dadafa84a193e238c01f6ccaa43c1a9aa353c5bea4e422f2d1ff6e067e41ac3b2aae00dcb6f1b85c433406dd039d0d91a15cd60c0e850e033a2f22680f0b572f516a33dc0fe4f4bdd7a32ec458077ee950577150141790e137037d7d180060b1c56681e17275e5a85a11402082f265659756e0241d0e978e99f31d6ea6db21566e2684de67b93635353a126ab77b917214c32a1884699368bdeba1789f72fdaf6d8d13563c6e3bcc60a5d6cc5cec5d88ad94399ee7c15c9312dc1415abf8acd63095215ed5020958803a40ec800b4edc0b827b32df49af748e53d1aa659d3bdac6ade090f4a01a5c37d504ddb2289a58fcb620cd7548ac1bd6fc88863c6133216c21939c4ab5a839785c5f73bdde3d26f6c30ed5a6b182960ede7965e49fc18aafe2878beb2c51df4aa56beff807fdc811c52915425c0bd97922bcf7a1e9d3317ae31474edb26c9d3010a5c0aa60de704ee8a78385e15798c23b690845dda5af6d60ef3be3f886771cd84a53303023a16a7858e239925d651399f7d499c21322578192ba444d0170ef3cbf123833fd3ada157544f9c76ed9ab9b154bed903c4b29cf6b2bc965c3680449de493fec0bdfabc55365b887f03ff2e93067fa4881dbe16d336f8d7cb0a400126446f994bdc951acba73440edd387f381f08dca637a41858faeea803d0a9bbf3c471c34b8adf1527d1decb47ebc0afdf7882c59c82c104b8457829e17bed3a2e37667d9ad16e5fd088f78a461f0f53b45f34ef2f423419ee67adfc4f542207b396fbd1f060392b090e0e6966637b8485784cb8367d546647a842558a2f52bd3ca63d9cb718f80c03dbcfb676b532cd4834bd0f8794543e1836855f8fc6c4243dc685c3a0fe3693600c09a1a", + "9c4800c1f630913d003210f60420877dd496687a17838f58befdc8d0bb1420e542c8c745a2a4180e95ea8f7886fd7ac5445a9481ca6755336b6efb955de6f3fcefbcf40c7c2f287c38466fb366ff06c04dd6312310e1609e17c0156c8008944caf0858381a1b73ebec72d679b0b7d95af6e6b7b390fdad8d878b5b17514d66b56c04acc195482cd26596ea86eec7652864754ec84e0822b78c0ab2e6622d2440993fad30df263eba27deb7db3119e33533a4d1f37c3c83623bed580eabd78da628b2d8bc2557603e68b91469758c8d90138f301690635af153a16860d8f28866b2b3729bdb803b92e4393c15c9f6ce73311be4b7001e9a68ab08426b4d32c2752da8fb23465a07f421f94143d6aa72d8b1e429058618dbacc6f90bec95926eef2c97050fc5c0cd7f91b8a47b5ec82adcd8400306835438ca1fe4a35060ade16d4c37e16f3e12470e364f1ec310966370d3ec38c31001fbc511c8dbe2bde2e50ad7402d6886eb8d1dd65659d28bf079a684f1860db7786dadd0a98d7b3fdad650487f8cdcda700d4e3641bd2a17980a3e470c2608635acb087461541437fd1dacf292bd5054689403465db2048aff1dacaa5d8bfcdf8acc1f000000ffff", + "8c9d5b8e642bb244475412e038e0f39fd8d522e2b49466d4cdfaafee1319b137f8c36cd9e7cd0a24b7535fadedc6d5ab6fb6757ba1f9b556300921096b1b9faa8f83374a5183502df4b4f54ef0a9cffacc4272ef9488a1bc21df4bfffee9363e765e3f93023e0d72d95471cceca72cb0638f6d84bb8973416d80f0b9155564a5e8d7b73b745f7645fb5a49cde25254110909cb764f3169afd696054602849dae2d9cddd311196ff5e197fb5a88d7f416493a07ed1cd70aee0caf66bbe6dd018b1ccaa54c2487b6f440c3d094c179f9cc3ec51e4c8bdd3fc1e46f9d7f5ab3faeff8dd05dc9495745c906ec57c53f559c50cdd305e5f991ec5ef1de1de87d5a9ed99699f956285afcb3a7da8bed3b6e2c4366e4bb06673a8e607b0db3f53929e838a3f5f3cd75ca5cbf61eb33c6589545ba305f73b5db7c0f0625ea7a5f6e83045fc15235aa37baf7d8e7b1a581c45f45ff4ab1fa92afb426ba1986fcbb86fdc0037bb52031bcfb1bbebceb14d12b4eebe77dae6823493a5f5e32c25d1fce591c596daccdecefac8331d276669339c50026c23cd72f21f518da3606b96dab17022e91cf9666e7b6135093836953f2dd7f0132962abac88c99e6d5ca154987d14d41c735b5502673f7a18c406dbe537cd2d640d2836961710c1656543d30e564a8a78b22ecb3117244d5aaff2f116db2c0c77b9711c51261d4d0d8e012949ededbcfde6277b7e0587598e3e86a320d51d33c5027bd327961d8fca46fb3847a7e1c487760b4b0568d79b49211ad61eb56e40d9353a41cbd2ec1e36e0d340314f2d81cdf2bf72a269509a016c4c8f5862e7ed7bee3b487c73ef0ab21d73ad8992f503e15cd9a7c9ba10a32b8834ae1e59063c2c320cf7c9b0c0f382cf6ca663e9516c55d5858010c2b2bc9f737cd600952a17004f91dd0f52e07fca6adcb58ef3155890454fd32dc7ea1eac85572896cd78919f4dc346219d9eba50bbfe2ea572054b49db08be9a31a2b99667c0c2577580cbc4435b229b5c7dff040d7f46e27b2d8b4d632469471e2aca6131b8507efd9aedc8d48caf1a63ff8c85fab437cbf26a81c11e3b9f679fc864ec73f56366e579a0fc3a337e3e8cfc45ac8fc1626234d31a0dc2dc0c34f21e1e909279ba873e5c5f82ea925694896c66d0d3abaff44695b8df0156f2b6b57d21ffd1bfac36e32c8b5b6200a1e3de3a4d71b803a0869e06a86cad17e62cd005ee7328fe167947c4d2148b111b17bd3e6b3973992b86c1a161407dd5f23f1e46d73ba74f820bf4b95aa7690dcce906b956cb86733ccb0f29b48665b54d5cd5718366a5375d64385936311dde695ebbe9e7fa5efb544f1ea9d24e5330b53b96fefcb59f2757a3c26a8fd5cb720a62b5d51e45e565915ba80bf5f681b4e02bbedb6818e9f512306449f45933fc835ae7a6d439310f3dc2dcfa238f112b2dda024a942572f194436fb5efe7324a35b715a2946e3fc13441395f3e9f9edb3820375e5e1f343c7adb50afc7578ae3dc76cf0c956526f13107c463b33fe569665ddc55fc36d21547f5668c9ccd45215f61b039084b93cae53e6882196cb8c167ea161346c7b2d27627b5c7c3fb7ec6625df82f9266f7b5dd2f7b0d53e32d7238326c2f45ebd57ceac6f36d434a93837d466c7073cc02b6730ccdaf22b0af4a71bdb4e6691eaa83e0c1626ea87dadc7599417e12dff45c4a84cae033e35235cef8a2a22157ba6e3f2ebe6ddca9b346a9aa71e188b8973b2761a3cf3160c2a78885bd19aa13fbb39efd1960d9bb2fa22fa036a1a47238d295854b1c40b9b61c856301cced5b26bfd0b02e1b2c8df56b6dfa962c5b111c4851c3555c6d4830cc7907c5be807d5746aba210293b41502729becb6acba1953aad0ea95cb631ce6b475130adec7d21f108aaab0d1639e47aacf", + "cb02d557cf4c5feff2d1d4cb8a372234ee0cc9db7e30284ea4c25263cd7893e04a076f3839cc35c972784efb5e63cdd6cdee8641b3551fbf4dc63fd30a54cffa74d3c40efb9771146638afdaf668c668426bb571cf01f4d2758c44f8ae8aacc6c616a421a30c9ab7c71ec294d227362b4fb891eb6969eebc783606f2e6e1fb7ea0612d235d72a0e9c7dda40d1f634a9e66e939746a46e2c5a352fa1df0702f95844612d874fc02ddd3f48bcc6be65018cb20bed4621a995c9633cf09dbfcb567ff228cfa32180a39975b3fd65bf5cd647b6898832b70bf211995b6787fbc4857bea89f9f91cf32595a3f09cf464719a07a74494c28d248a3bbf5b201254f500ccfa92b7ed9a61d54cd6d4dcd63bd16e3800bd3d106a77ccf6336e166c7265ad3d0e09638bb99546ccf51a42a99057e85e73d27e6579dd93c451e80778606038e05885dfff79824c8078b7f70c4de0c2c6de1dedbd887876a9c7d2c4109daeeb133efa141ce51cb027807109b6507c89c3b741208a24995c20dc785fad4b392db41fff28d25cabe7bf44fcd32cc6edee32f9ad84ff59c48b8d5a1d2fbb4a46080f0b5ad997c08d0eebeda4412f060663afe16db94c7e466d7e5cfdc7b9f30a92a4980dbfe532c020c0c3dd9a0e97ee285a9b91906a77ef774be7716a3b859652f38791a55548b66c332441914ee6dbd2a56d96925475f6bff4418dd23f5a6f78edffe7f3fd70aaa7ea39a644d8beaf4d7ff3f46574dcfa83dd1942f957ba60b9bcf1e86d37f2ab8411a871103a1240cdb8433bfd295b729caffc33f2027d38a23c11d2be9c6e48e7fdf751aaee6a39e8295634725fa0875d943913a694532c94d69d9324f712daa97a177f08890a0d3cf8195b987396636e159cb783500ca97ff5b3c4d728a32f731e6d599696d2d34d6a68a0e97d77fb40470576d80c652d66a0384d97689458256b12affd5ab4eec4ddb0e4d6efcad3de460136e206f366f600b5d2e32106729777ce63269951192beb9a28d71aa292e63f46e1b898059e186f43ad135f4999163b34041b3c17d1ebbb51d9eb3d93ec86f3e579a5ae929fe60893a79f9e2171be2fdb741504ce8999c31abf49c1f89bb51430283745ecdb519d19bc9db12a8ee347a1b89887acd336734f349cc9bafa162dedc5d3ddd01384a69c5d631df793e6c412d7116219b6a4db44fffbd255b4e7b3b8804b5279ebd782c950b0df204748feae6812f7d61b56d9feb00d4d1bf6031add54cadd15bb7749574cd22dde3d1f1d6b51d47d74515eb18948fc69ee3a23906c1186b984291d45f60d67a9eeebd8c035d7720655683be7e72defef2e7b69d442c96e9d4631962fcb9e860afb987fbe4c9af8c7f60e4bc9f979e27b74985187245795791b5554906a4a79480d1c7bc65b09cb024103d0266622b3c8990efb647ff1d2743a75c6db9e811cad034e9b88002bee4975d4bb96cfec6ff3fe26f9886c7ea02d26e0ddf4a69bccb3cf10b5290e6c68ce9daae7613ca9494c4a0ad69a0f5663ed47d4353dd46d51ded6bfe2a2efbd235871ee5932cea6dbbe1c0726a12701b35ff79d6864fdec3a765c81cbe31de6cb90c67ddd723ac2e32fbb13d220003cbb6452864137032de963228b10ceafdb04e5faf043d46adf39f4041b0d89b91539f5f2c8bbbe64ae37953ac4c4cc0d2cb16e1dc0771ba1ae393348cfc17ebd029ba614f449b7d2c9591af9a9472ae12084081bf2d58bf29046728aa1d08eef15b8504bee1202da146dd71297928fa43e4c139a0938f91b399e408da3828028b7c3f559edffd40effca7523079d52603d366ce735753daec8b9c767558369a85c45c53fbdab3029b828e1546d7a1567610d106d362fed575651c67d51e1e0d88bc2d7ea3a7fecda3c0de275bea6f380e993456056d680e2a36d88589de705e", + "6b64da1357e8394d2d4bf4c6306a7eecf600aba28bb0d5e8bc58c163169abec244247b4e8a545bf557db2b7d5b07badf42ea77c771627629a572fc79f15abe7e953a7eb5a4e1cadf1cdbab3fd5c2b115d90376b0db7df7972de89fbf6157c9d905dca5e74c9e343b342aa9b451260e16a65cd35afcf829e2fc9c6b939c76ab327044ce651fe3d4d8ba41e8e306a53b27a929f21bece3b166b825a6741daad7a402b649fbc4ffa89ee67bef98ff3c73d99b5e7c937dd8c6a8d25c1facb6c8f6308246cf632ebf0ba7d376a34eac87b315998c0dcbaac7b0ba1e37896b1b409daec76911645f9beaf08585defcbbf3cb81ff2d53469d7484434e2d6c13fdfbb424e5e9193d640af498bf21249e7edd4fcbba92f049596c6862e947bd74535f8c8c12bb8cf6ba2974a4317aad7bc6d1a90b9aa561a4d998285c159512e5395b6d05b0402d5db9695247953732e5d843d103098efe7a87b816b7c7f45b152b01b8df9fc03c27abbad1ba6f928da5fed42420c864d64471f8406914837c553c2e16a1fa9d59dbff29fe720fe5007b77750717187cd48d93d5a746a1fc856aba73afb011891aa23e1516d69dd0f2247993b537430a652c0d4ad2dcd9354ee70cbc9df62becb27c908e24e084bd5ca742351c4a22fca8ff6b8eae9ebe58dd831af8b686da359f3bc7ec5c8a53d7fcb3b2f99c11e2bd8845489ea8ae65e773c1dcdb2142563d0d2cf47f5e7ce8315ce88f203df52a793b6bdfd774bb3ac3e5c29a323a60cf1311face06137bcdaf457830721af6e721c167c6b7d445c8a37af4f7270fdb883793b460ddcfb51b1e26bfe96e19db32ae4e281d6724833013d9b7051dc9845d8be8b032195a2565bf7cac0756d8cac727dfeb4ba62e9f97f0f0fbb402e6d4af58dfafdeae4676a7dfd29fa47a456b1d8d5479eb8768592235e32cf196eb2d86a0a55bea27ad6fd5b654fae48236a54da0dd34994b1eae585ddc1d3a4d4b66c69ca544dd19a449e8bf14eaeae70c18796acaa01b1cc9b0ef85565065800c43add4be04e2b64659c17f990ff2703fdfe22733813dc33cd3d159cb68f64c7f67f783b9a5158e172e61d687fb6438508c5499b07a36b41fbe89df46ed400f337e82f2defba94f9ac8446aa0ead9d3c2bae42755eda9ab7eec35bed5c45ea9a024abe7fe4acf790b140d0bf7cd42dfc6bf71cfd837c67b6bac592702d0941615e7980599b5e929efc71fdcf691107dd4bab1cfaab1c6ff010000ffff", + "8c9dcd8e2db971845f4598fd004cfe2449bd8aa18dac81210396375e1830fceec6c7aa6bf844b0ddbd1230d0ccbddda74e311919f185be33e66749c0b357cb5d14903016ac39eb3abe488995fbc8b49a8653a6651bf90ec67beb0a2462f66d3fc085b67deb14a1027ca59b3e677307f8c0696e1fea89466b490efd477a31064e3a434d35f584e96da5704be2111aa46556bf03bc5b3dab52c74863e49f5451f41f0055a2f214182a0839dd33ae7db66eccf492305ef59e586718fabff071773ba34f48b2590b4de1fe18c6b6ac87cff9a35602b0fcbb0cdb906e605c668320a9e92d18a717ceb681958a3ebd1d9579b2d5fad38d20edac76aafed4a5fe64e5b159ff5ae9c6c1a6eb46928c8629278be35ea1e574035b97d5c6993d7f62391fd14675f86af6d0c521859a7da89bb3233b6a84154d5c5b756bb9e4b3ae8679aa2eb64e4594b7b53235945b77a8b88ae5c940bbef9a3fd4520c9ed42cae9ddc9f82dc2b9035cba8f836f4da21f225b4bdc579a37de34f7a4bbb47bbc4fb924a3b7da85adbb51af9fc0a97b6da9df7e410abf6bb1f6cd35ea88e0e7d023508d4dbbe6ff8e8bfcd893ec219dafbb4b9723687c8f496ea32ad5c82f585a116fa17bb4efda89af891f3bb4270f8f676fd9b72393ca45b53fea6823a4b568a4eb78336bbadb630da9562950ea4416b0bdd0feee13125bc9be39b0cff33eae10a9feac6f2ccc40bea2dc6890dfa7fad0e8f5de05efa62133df45d260778053979761f61ee713a11d7b8c8899461eae278ec628ef00b3db171914b23f2d5384f9b1eb3185d3594d66948529fb415277de9682ce4768631f2602a176be821f9d4f48c450662236d5ed3692d81bcb33763a0bedf60296881448ba5d73068fa9f2aeb3bd4f0ce56a765edd354da561771477be98270b57de96ed3aab63a210cdfd5118b5457774d643fed37878531bc829796203ddebd56f2b171c550467d43134e4bf01239358deb56b5f615682da2455a3f352251b7ba96d9bd29e5f67785e46e8635aace946977af3ab9ba7d2a5aa4f6858e36475784eb9db679de8e7d1b4dc316155ffe0df06b3161d994073acdbe06e0459719e963e6e1e8fd040615088526dadfe5f9d841c3a50e4aaceddde776b5b3e391ecc3ba2d3e79526ff751bdc0905b8cec1ab7232f1a56cce0608e1708d18b118a3bb533e969fe3942ad297ce6dcb2ea77cbe8b7bd7b778d39e558554d24af50317ee23aaeb0b694323b4b6e6313b1f2b09a74bae7fb65b8ccdecbc53d10fbecfaed029adb2ad0994f99a4755ca087de0c5960b1a7bfa9b9dfec6f04f6475f1eb93d404519479877f9a6610458b3ec9794e8a2e9c4d1c27d5ad21481b0d86ef6562ec4d9dabde563b45e0da34ea6975e71fddcf26952d23bbfd0ef1e45961480e72b66d39d10611d930dafb1ffd38d648f7274ad9188b27194ed6f1fa3676a235a570c92947558b89c7d7bdadd250252920b87daeff02268c9c799c6d8b79e8d85d6b3ea5db610aacd0f0553d2faed1dbaf18b839636365c5d529c581906fe5e45ff214c46429a76596492c950894a1de5d75fccf31fe8c983646e4ecbea568e8a699f8ce1ca4eeeaba6d59930371160b5c8fccd8959cb4922e8404b4797cf7385c96dd46ae83afd1c9ff975ce4f33e3f3dbddabaa039d4592955a3051af36cc8e292696e70e9a699bb82f0497be634e25b5c02cab4311e63ea7bd8e003c28beb73cad5efa2c039ddc0e258e6979ab6b58158d63f666194edeb5dd9a2a09a5ccad0377e317ae2566577416b3aadfa34e398e63c6d6c69c222f35565d0a4be8fd10b88d4f05cf55df346e6e7e6c61ac155523b079ef1a087fecf1f2473da3f58eb59daed3d6dcc6d41592d4eba2233960a43d8ed7a6190123193df17bdf4917f47dabfda67fad", + "94fc9197f2da2f4c98c3162fe3bc2accab50d3a5f01b44e2be16253dd4a69269f5d2f7a636c34c347882cb349f02b089e6fcdd953eabf718a6b1441d633b4693886459e658990b51cb26acd3c6f70d72e3f9b5d4a1ddc6bffab3dc3143e6cc0e40496b7d5131052781835965aa4d146fbdffec2fe77ffffbf9bffcd7fb0f7ffbf7bffeeb1ffffc1fbffdf94fbffdf16f7ffde36f7ffbfb3ffee5b75fff95dffefe8fbffdf19fbffdf94ffffb0cff9fffd39ffff44fba254ad7c519949bb6ac6372a37640bd440dd7981a9d038284853c5b2f4bef569d75c6920fa1d62493bb4d901898e496af2e4cd449ae66dd908de421a35883c41ead98685d012369d6e5a42c57d54cf76609ab22e2827aa82b3438d8d58a7968b8ab5b016423f9da2bdd8a35ac55addfffab185f34584d51986d406366b669dbd64a86ddead65631874ec5665834bad9f98e5a4d028c9ea5d940000246bb38651f32b88fba87b7d3c194cba26744eec0196ea73a115ed551e72ccda5d146fbf3b2d52a76906e6ac0c85493711fa781c3943592946a184cce399d8d1a95d4da47b18c215227ee71ab7b3aa960ddcb56886e5b4b36479bd615837955bd19b17688eb86e2dd625bf9b56668c32690bbd06021f6180c272a7dd5b30c34e92c8bd2b768231dd651321b8796bef147ae70bd00809a66c2ca9e31ad20b3f495d59a96671bb6c298c19d417ecd7b51a02b1fd3ecf45b2aa511b1c37837a094b71a9c28edec1604867e59ed9acfb651850ae2ab53df7375f7ae1b983c89f8adf0b9313d88c45bd6a4877a92c1e14b896afb46da599d33d02805335479766cb266efcbb552c7807e6429bd27b03937583c4ff570baee02c067f33c355dc6cf59443d1ce845a8df5ed4ac2df535917802f48d72cc7d862622a1d6bc828819cd781d87265da7f767b7baec6e5681b8d845850bb2f8c7db6869226bc1173f0ca78c57c4a0bb310b9d2a5e088b603d95388237481b3bd347bc1aa7db4dd7d1dcea8c02186b8ea6a37f234260b89fd340e317d6f312bd483d66080403d99dc4bbc236460db746189eb20cab4240612e5377a66bd491974ea5554c37661c2ef6099e66aa5a4a779fc534581f24146f8cefeb6c53e54f6b9de89bfec516db4445dbf6e428f7e966ef6d5d5c07f666793802665ddf5c830a6aedc10accaa7eeee439d0744587f97075dd8fa0414d030447aa59b53e4b62a7d5d7d55a38de7154b1f0264c77dd03b18cd46a6e3f4cde5fd580a06a645624dfbd7d0340e18c4aaedc4dcb34bf765b765daa2495b4a5f6b0a3558d797a71cc5a822f3acd047fae70467d5cabce56ccd99273adf402104421e14accdd4a57730727e2b6a6df67ac80d52959f746876e35257475c3eb93105886966bfd4068f47ebb5ba9d661ccd754a56f08f9c5aaf546e55a63785decad7a05c2da315511225fd03c16c5b539b782dd80f9ea793d731b2b8c0ec20b2b156b853a76ed487aae866575b327f3775f666dec3c9bc50606ecf82ac98fc38411e33c180c91ce1a5b3b431fd7b61b53b4fc550fede8c224fd6c9378b07482157b0a85f012698763f8060a0763182d9de043555ce089ca76a3e4b4e9a2c7e0f454b704f85b03e6af519584097cb7598573590bccb465a85bac629882b26a0d7f7e299db212694cd04389cbb51fd08063a74f4653b5ed39764c37e2cc5ddcf9559791098f0cbe4d1cc7cfd87d99a0a7e709793404eb695ea63ea76a4c233f7d83efcc3cf1fecb3314247a4c123cd1647d0a28a990b776bf8091b9d7c750d7796d5c37ed219a249b95023f7891eadd8eda07dfb16fe0d4567356173113fdba11a8d06ef57edaea8cf08d4fd1629c1853aa6f6388bf983691d115f9369b797050fafbe792ec1174eb98b55889d1de6607ad91b519eda35350b50da376117c4837", + "f84f5a0f36ce9eebc5436818c6e23e5b965c9f62e0ebbf585e3ed72ac97475088c5d744a6a8d4e2251dc50a44b513d7712a65002762e66190339d93b0c889d65dcc044365b55b38f1d7bbbae4234d5b2d1380c4c702c351df61dc650287377f8b0765743f4348703d931f372e250b6f021216a743455ab20ff9a451474c6b62037e9026b2fac31cbd2057c6084ab1e9c3904344de36435cde5d453abef83fac4d4970d2a9e29ccae4d3ebf2d866f7d002fa7285998d64a383a64cfecfa0063dd7663c4de36a1221acf36cc614283ecb658e564c6d51f6c0811e5cb47f3d43e6bf488d5f736790110526abff4a86b77a747d30fdf87764b1cfbba1d98400087d596cf70ea3e39864fdcf7f30e5aacdb74c15b9a25347a8c167b1b2e87db87aab3f03db6ce57139551165bd74310ab95c5effa84c73ced02db90122c4dd39b56b69407fb653a0239ec62f0b1c41d37d4a64a6dbd8d533ee0fe322a3bf8a4621c729d7606d47b8b0b76ace99af3d9bc78e48758904096d9b56154150d22f3f556432b6b8e3e9c73c694aa6a5063eb2c9896c66f40e4c32c63a48a3ee106c923fb6f03e9776ed0d3849cb2b39a6a767d131f45d42413a8dec19066fa658ea8368f06114079386202a50dadd81dd2397b7f691cf9b1e63601bd7270ad699c9acb29459ea85890b9cc58c3ac4873b9841b80036c7b34260e7235d274c4cfb0dc5098490d33966ec47aefb52fed63a864764383d4514905ea9e0e6ba71b60b2afd4f5c9c47cabab0a7489a643c6587cb1d41b9acb84d371ec24fa767f32d366dfe9c47bf4c11cbdad25ef7c5c8a0690aa33ebf02a757ecdfd529b386a2d96103a155cbb6e1bc889261a570ac6d8340661349dfd77edd603d0363e437d006bb83bf6baea7ab797f653b53d943b317692c5b7b9e534752a158a021f7d595d7482ca9e4dbf298d2ae025475ed28160489201b945a9b94412edfd3bf65948aa4c54d69c0a933be499cffeabe7b93a5a6afe4423e68fdb66f06b7019a35a0942a422bcdb24436135e3898920f5a10abf65c382d008474747d648e6f532543a9013157fa03384ad03c04059847c3bd4687060777fd35390aacc0058edd61e701aaf346c7552d1a95794553dd48bbabedcdef96c158d8f056acd98231b4a4f5115779fd6450fc5b694f7dae818be355eb8a936f69621fe7d6b928e3e125eba23dc70b4e97f63610db4a232b6094a84e058fd44c19ed705509c30b1e9e4f34d987d7ab6e5231f67ecd11c50c3d637542ec5c137bf596b3da3bb0f6d75cdd98c2ddb59825633b41d8677759ed90077692ba458610f7da3e5da528a3338e22cfbeaa6810a61606a3868765e7b3ab8a24d5a9f5b6b6bba3dff9c8f56e23481162e5ff0d6d4df01e98bdd2ef5f5655990fbd4c12df74a9fd1c75ee6621b7aa4b9ce916ef911fe79f3b0e99cee025fad3413bd631df3ae2e08c2103af3d8fe53adf1a2783e767be080bee16e34b529206d8009f09848b1d2b39359b0e4dcedcbc90167c0f8d3fa28071458c2629a316243578213b67ced8260c936f51d6093f86bec15fccbd7fb665ec7f373bbf08b6a58bd17787520327acadb79f2a5eaed3bb56716db66a8401c6bfa72210fd39c7b301bb9a865bb981cb625abd9721bc5be910bd7af4b2396af64ac9ea3a9b395a2eb55f58b49e22c8c3f83b74a7ffe5e766c03a5c034b7d01d10327c226950159c863a265c5fd0674b94e9623868dbf5cdc2f8f11461b1d0df36a479cb4a336215f9ba8cd1cbe786e31934c0346e439af4ae4bba0cc4022347f43e3f7b471f1503c29e09f47839541c893e7777d3f0ced58b56ef90079c2e107fbe5b1f33c9b477730c1c07cdf7ed2ca5f495af5ff957a29dd37e2d0b7fb7c1c93ae9511b7ef4e5f22bb638736d7525515e635b56d06ee14d596b", + "58a7151d15336c7edd3b33ec5e47a755b5b59c4d9be7a6c57b3cec3bc7211de61e596d6f654437ca1dbae676723a803de0ca5b8b02601b93e463d758d37eae1ca4fbdcb73d9ab9872875d4c9430fd2675739ebb800deb27827e7e42bb7bd3ef2b6c1048d0a3cc27e0779029dc6a5bc78defcc37d84ab4d17ac11478fa8abb1f9289fbeeb97049dc5d861f6d9fc3ff33643869fb4c468c25c2258199a3de110b59a3e09642af6706a3f6273510f11b0f154ba3dac0c0fc15119a628d2ba1164d4fbd2aae5e2dce1f15c6f87fa7d1b366a1b1507d9eea5e470ba00f539243ea37ff8d814fea86183055a1717432cd8277248075b91346854c0b69631ab1f7488da4b397874d5779ffd7cf87aedb5b53b6cbfed58b3cc9f2cd1073621055e9fd22dcb4fdf1d9a5cce66b7de3cbde33f91fdf5e9cf7f26f54ef158ff468efefd7a31fa72d783b9df17be2c024f99827a0ef072da2bea925c382485a98202b1e5b286d72e243d1d5e90d0e7e7b4f6420afa9aba8b6f74fca695af7b29f4313e8e4b3892a7697623485fe695ca8e5db55e8eeb116a05e4b31956c54af371d78b6f2bbb0f2df46532b052c7cee6b4faebe92911b25405a6433740ed358a470df4def536c3c0cad0ff6e42445817247b7643b2bbd6f03c38c4a2d5335f136e50fdd17882fd409de8388ed42e86e256e7d273a29d2648fbea60e2b1d3327aede6d7651bd3d7a5fdaa4e4b9842721b61dee0b6e3534679bb7af934bf7587c5ee506d957415396c9985fa51f4f27b3787653dfc5ff9874f99b93e46137ca8ed67f66760e5c5fdd76a2b3e08ca53df5eed1c0ccdd479ae59db9e42ae6a3ab883c233fcd8a49e4eeba0a815d9c6cca3decfabd9f6aa6176d011767eb400f05eacd704b3cfd46ab93e7a35d1f1620d78bee1a32dbb2bc7e65e29979fec655be51c2adaa9b475e0dda77ff645557008aa6c189327dee2c8a86bd30edda410d56ef110fe141a8cf9107de847ee410445034341f018cd76452bb641d7a8169bdd34af9b77ac509e14b12ca87070430e26523ff9cb4ea8c32c9ca6d43e8a7da6192be3a0d4543b44966e1612e5058cb86ece22dab5fc0d1cbbd8ce09c4ea3644eb31d0db5144f8d4fcdc0d9898aea7c008f9379576b29906d628bb861bf03b528b9d438b1e00ed58bd89adcc4ec515d449de5aafce73b248fc911597ddfda5dd848a602bfa99b8d8d22ef4b02af41d4a41a86d7e1bf662718b5c8d218dc6cb62551f6d048fb876faec518bd5dc807b26b761f7fc9c21630a724a8b61668f5621cbcd1f6cd94a4b2a919d9a1dc39b5cae415598ed31a2eb0d85be76cbae6c37257037ccd4febcd667eafd06faac958a0068998ae9025f3797e9b06c65fd2d4e16c69412a8f9cb9b66f171f90b90549a8dec2db08eda90c39774080abb8fde57aad99a699b2faf4ef29dce7727af8cf5b95c7c458c99c5d9536d15656bc0cc8db4b36881b8d272f7967599e9a7757c001ec11a84102c038647c6c0406301c8b5f29c09c0dedc8a24b1337586c56eab4cac05efa8a84ec0fa44838bb731bc66d0456f7bb10d3e54a3366ccbc23a77b92595e9537fa34f59232db72df3dd85096cc39821ee807b92af172c0e62010e5593ad2aeaafcd14648566d73cc5b1248501f6d597f5aed7a62d0847cef2b9c5fa05c6348a01ed2bdbb2aeac3259deaa1f780fcbc3d5556d04838667294bf0c43af2f3ea29762926e3b3f531ac8808b6d8a1e1d82aba92ca123b0090ca558a01e0cf7dce140f4c41662786bca5f5f559a3f8d176358db25768c5b3da2ded8272d769d9048751fedcecf21ec41878ec79cb92dd2944267afcc25ad73af5fd9779381d3a48e7a8c580c29b1e379363c8a04edd9dd22fdfd2386e8056aa0e5f6355e58ab58191c22f0317a5b675289adaa8d2b01fd809", + "90f41ba88c92a0d24ca56254aeddcbd19be56d4a471568c656eadd32ac05a55a4f855e16d75a5d9b7197586618636ba3e65f3839d98deb96ebd378f9d58e918f3f35c1810b65aeeda906792e9eb44bb5a212740263809efea2d424ff957811cc0ca1d9d3454b87529ea942d075cba0db4df15e609bd30a8550750086dac3de584fa9bd143c7031a276b11038d468e3d5f741758d8aa4b5a8745b5722de68007a4fbcb43a6fd5a48941a587b93e99a0cfdb39d3979e33bb21f047a65b9629ab748370d9f3101ac4da448250212e93916ad9ce71f7f4f5242886657f55b62fa99827315e3c1758661433ec52e953b731c2932d47185cad5f82e110dd7995abf69148015a375be131e80263227f9bbc5aa74f5ad0dd8af9272aabf7d418ccc04dae8d28273bbdad26910a25a3298f468c4f8681b6e76762fea5ffedae87cb3516c03471c2c7facdba3146226abff40ea2d87a4b050857b3de0675075587584bb63f2b798c542e8dd2fed80c32916d68c3a0a397bf1ad9b1686f2f52be3825ae4493dadbf67662727c50f3f4d9d8f3841054e02ec38038c77bbccc4a36a8c0515f4103255a3575040ed5f02f0ed4f199f0eb157546fb143c7fbfa6679fdb6f733feddd6178778537087fd662119b8595c5493bfe206f4a4f7a162d14bccde94c21796f3e37d125678c89bae768c35ee81cf0ea148e3163a8130c21d758bb4008e0aae88cb67658a2b4224f99c98b7aa75d8d0a45a3c9f4db7e673cd68926d61ee6d946f7afe149c9863b484e759c3c2673bf6920e3a2f49c692ec3ba3eb794cf4b9d0dccea56057b3984cb3159792a0c644d68b9285aff6adddbd2c25a84db7104694b97874dddbbf8a27876d3225774ecb934581a4c2bb6cde4d6147a565596cafee1d2e50ace47610694d5e9771f9dcfd465283816dca98bb8a5e2b07a6c24721be30f9148dfdf2c0432d2e159d1b619ad10f4f4fbd4f75a2647f5dccd3c51eed27e86838901496b94a3fb45997b2aed6eea4e01a925ee94b5bcb61ac7f06739e82f6efcac4da79e43a8d02b0337fa6a1610203bb6c6d248c663f8bdae7367522ba1563d0621fd7ad6deecefcf6cdd968691db584b0ff5d51615f4f28b1ea5284e9c39bc6b4e8fed485bbe581b4b6139247c96caa976337c1e28342dfd987d4173beeba7e6de644f2a9a8d665840e1958b2f73387777e4ae96fd7151e5512962a84ecc5ea454bb72a158da263d778dbdb52ea8cc6659417a7986b6a725b3659a56d722ada8cf2291f7edfceba3e1a9b28879c3572afa82f955de6b13150d16bea083a7dac2c46804afe6391d290324a268ae2df80e5bf9c58ab14b7777bbfe16dfb56f6fcd207890d28afab600164ec37d926bba145a7d9115a13a0185528de2ad8e6df7092ca21ad6188c5c3a4324ceca4bff59faf50b28c56e5a7b4367a13aad1aed7a56efd76384dae963a7aa8bb7774e993c88a15eba5ce19ee1c9365d7ef69e6c36dc484d47ae9155756bf68acb657e36fefd6a8d520b27153acd98da7d2c65d65638b6b56bddfd01ab2a2f91b8b209904cabb8244c11cba8e502f5e83e4f0c0032361c93ae4b8785e3516ba605e39fd1f045d43df632aac24d34c7f5559b6910f4f898da79a55321ab0e733420ab4cb3e4d3e2de5d5bc95a9a6d6f297bb30b4add9fb08557ad4483bc54ad49c7e8f3318cc612dbc4773953ef2698670bc5dd57dbded66a5357c5944ce685bb3586a351a8362fd63c0fc74517e80addfafdbdcc440fb5c3659d78be6cd9d44ec98f0a2e756cb50684ae6d63f0a79b329a7d5ddc9a34500c35b6d23731758586b4a7ad4d1d61de6f881d6ab2f6bd73c02c857c72c9ee0e14d02ceab38ba5eeda963cb8e32c75d892a23ae5ad27ad8c66356682d9df5a51de6be3c84f2df45d935019a1c928f00b1ab923b3", + "578d6636abff60ad2389687ea3b009b6406dd095063d5975c339f448a7856328a50c4e41966e56a5303193c3a8db27633bfd979540a6da862d8e38ed7fcd9a7ad5a0d1ad87c6e707b507ba098949954a58fc9d1ac6a162eae9f16853f726f4e9b4ae7dd134a157cbc5f06157bfd9e06fd66e46c5873febe5313c08c812799b6f721307506a26f7d351aab52140d4b1b2a35e6c6d4e8574a60c39dc41baa1199515f7fb6d9bf0049b88479accd263cf6df52eb7d51d7ee9e64ea06cc364eb58630ce344d10962baf56df3430a0ea547ee3cabdfa008d06d14d6715f8beb62f7a14ccda1801fcf23bf41956511acb20a5982d09072d83a02f862d9962a9a0b87acecb2987b3ffe740702bc7d7c03e4d177abc0e7a6d159c758e4948338ad8f6967baa4b857ead9604fc4dbf63eacb96a21d79b8a83dd534764fa49a656bb1939f399a601806bc29bb6b2390c34c08c6e6fdf9abbdac1ba4deb47fe9ea31930771487542eb4312d9d0afe7d2559d546245f35d2de410f5bb6e2c5a1d9347cf961a3712c593b14f5bab6fafb2afd0f7f3a32f2db43e04a3efc85cf6e0ad118ea40a3c2a2a541d02d77fe52b3286cb5e241c69ef0436c2deb0bbd43745a671ab6d4efacc5dcb6759f509759eb66aea55cd2d5e6e7fbf6edb331e39365749e353308d76a00979beb911dfdbc1ceef05eb64add7dedb6a6ef47c6d64fb1829f365bae373bfcf269c5cdfa896ba5d93aaa9981a096595bd333bfb619cd87b9e56e2a7780fc0f000000ffff", + "8c9ddd6a5c471084eff514cbde2f4cf7f4fce95d8c51d0624224cb446b0804bfbba999b390d3d527d29541d808bcb333fd53f5d5b29c0ce12d6174e94df6d760be3ce499d987b24582f4901d007d26399a903d5e49a1844979f7a2f178518bcbdc8ccb39689d3dbe8d20cff7a43464d309e7d734ec1fa959287bf1d4259a795cc2fc9a5578554cea7c3a9f8ce1c3136c24ce7a07071526d98f1f6e66b76c7ab612bcbc5946c17ac31d03c512a6d0a60f5b32afb22771f9e6ea36bfb68a3a5029866472421b94917a401087c69fac3c794ec884a3cdc93574804c0f0c13eb9a4486328d3d22eb6c0c280419c7287f27d222409cd99544e711eb4660e52b7e078dfe01932e3f3dabf059950f186aeb1ccfd12af99fa154a3114987766b7c70551fb94820705562d0e5043299b2af637a07fcd50766811fa5c0c332647c829faaf0a6109c1dd7f418343a425885cf5fe0b8a0d519681bcd8f423012ce8cc47538ca4376a6cda679b09d01a96c7e99d3baf23807509ea48357ee300fb91363963114a6d1365439540b47c05b24020c9f2f134275a422e0c6cf226caf675ec5c6d86bdab6da487cc54773e17829bee6084503b2982007c8186fc794189bf17d94ee53ac95c21cfa70fea973a59dd84c555be35db5b5d47c8332cc3a3d6a90229792789a806165a59d52aa4d062dc590549a49a166d860fa942cad3672a5341129b5740af0911ae44e4628e729d3ed41523db01e18b4bb3bd1233c374a55e7d48a194fd13a8775364d4d2c880c62e8261eeda29df67b803bf8320f4e05bed7d62ad8cbf2561a9e130086349934cce015a0bd5d84acd3e9e0a2497b1463d64ba9d5bca9538d373378c90087f71b59afbfdb8c28c878a247cf75d84ba20e34b7af7daa0e6a5ce36c1ee882e164262ef2804cc02731e0d1f2f3bca88dc1eb06eb917f1f234aac7887c0662caef868a9b0ce4d185c5a0bbf0e90bf815f439a088427e4ffe74d6d5c812254d4ceb19b7a2137165c89222660f8e68025ca88dbe81eb65f161c7a9fc446b7440b046ffe5a642b903d7ce110f82cd09b0ddada9141738544f424d446111a6209c7e654d95738890dcf3ab023f605a98c9ebdb0351b52ff3e7083add1b5ec6de50b3aa335271a9d48a6920988215aa0216db764af2187b44c29df118a1ebfd0a132ec725c361362f872a88b12981469787e174ad44f89fc30beaf949a152167b269a78521ef808e3922135f6cad7b628b1f9ecfb30e21bc17532f5fce20b9aacf78985fe2619d600632b0b4f49e48ad9535e21055297d90948b778fc0837a9c64c462cde7ad6265db7c7c8c6287e43f03b02e32932b0df9ba1e5a6fc05d79870332428c1b02dbcf55b73c6be0e5fc04961464698cdc1850d47add73c9b705b7ec413ec7fe236873929727328d7235854d0bbd19e104733257742f4edbe6394235222ec29a78b111b565a6ad679f3b1ca2606c3292c8868608293289cfc44d5f6540bbc0e74d1ad2c60b2d5e0381fa3dc9d2db80a12ff717bc5a2a2ca592ac830742bdf461beaeb33aaa242fa8ac05941d9f248f4959f34e64512ca9bd3d99e8ce076495453dc8aa4605b9e49273abd447869240c8d93d4ffc6e7af19f1a608ba4ffa42cc30d33d783434a19b74b9f02a60d1787adc1a8e72892e891338d39d28c5f2a9fe925ab8d7d7ad5b6184ffcd5a192ef72580b2b2c9824088313a5f87221300e21a12551692698610dffe2571372bd3524dcd3b11d6d4e933dd6b0742a4c8093c88307b4add4d24871e05aec0dc6d51149f4b10a9489abc78f4a9a8c1ff174cc70ff3bb745d6e8bb4076a0a5e81a859846c85cefb4dac3b56cf4ab6c1f4b7607ed1521e02662db957c6a07c9893d0b271581bf9cc9a90989b6107131cc56c37fe3cc37f0b70f7279dc39ae5d", + "18491c067e8332d646a67071c40812aac95ae11eba6630d3dd51ae79bae0dcf599b182a88c548303b17bbb5fa081e34a62bbfbaa541a41e90c7af47d2dbeb8e29d95c1757f096da887da570e3998bcbd4e801a989371537e622e84e75dd53f37bc8cdc80b0a3513557a150a0843f6411789b50474f43b2596c8e13912b9044cbc1a0e12d49d911772840697479010daea4b59f1a70824e16444d78da6eef1a04966a0548c1d71da5c87e46bc8d5dc01d14be128a12892a9b625d100372b69f7d997ffe7a389dbee0af9d5fdf9eaf2fe7c7d3f976fde776b9befe717d7efef3fbb74bbebcbf3ebdbc9ce75ffaf9fef4ed7a7e3cfd3bfff1f9c7df6faf3f6e5f6f6f7f5dbfbf9f1f4fdb113bdfde6e4f2ffff9f1037ed1af87df000000ffff03000bd36c77e4060200" + ], + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:53 GMT", + "Content-Type", + "application/json", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-allow-origin", + "*", + "access-control-expose-headers", + "X-Request-ID", + "openai-model", + "text-embedding-3-small", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "25", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "3000", + "x-ratelimit-remaining-requests", + "2999", + "x-ratelimit-reset-requests", + "20ms", + "x-request-id", + "req_c9ed4ef71fb365820e5e512b1d5985ca", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc093300ad02d60-YVR", + "Content-Encoding", + "gzip" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-bad-iterate.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-bad-iterate.json new file mode 100644 index 00000000..59ae449a --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-bad-iterate.json @@ -0,0 +1,62 @@ +[ + { + "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 the falkland islands?" + } + ], + "stream": true + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"South\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWV34Pm5I3bpO148YKNBCVn9DF3\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n", + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:51 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "98", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9995", + "x-ratelimit-remaining-tokens", + "199966", + "x-ratelimit-reset-requests", + "40.71s", + "x-ratelimit-reset-tokens", + "10ms", + "x-request-id", + "req_ba641cdd4b4ff977912f0d63b118cd36", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc0932679f6841d-YVR" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-chat-completion.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-chat-completion.json new file mode 100644 index 00000000..f260a16e --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-chat-completion.json @@ -0,0 +1,62 @@ +[ + { + "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 the falkland islands?" + } + ], + "stream": true + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap\",\"object\":\"chat.completion.chunk\",\"created\":1727828929,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap\",\"object\":\"chat.completion.chunk\",\"created\":1727828929,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Atlantic\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap\",\"object\":\"chat.completion.chunk\",\"created\":1727828929,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap\",\"object\":\"chat.completion.chunk\",\"created\":1727828929,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-ADhWTjZp3ejGyaOvqngmOItSb0qap\",\"object\":\"chat.completion.chunk\",\"created\":1727828929,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}]}\n\ndata: [DONE]\n\n", + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:50 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "109", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9998", + "x-ratelimit-remaining-tokens", + "199966", + "x-ratelimit-reset-requests", + "16.651s", + "x-ratelimit-reset-tokens", + "10ms", + "x-request-id", + "req_077db0ea31ff2afc6a7e281d222186f1", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc0931aa8e82da4-YVR" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-parallel-tool-calls.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-parallel-tool-calls.json new file mode 100644 index 00000000..e9903a48 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-parallel-tool-calls.json @@ -0,0 +1,90 @@ +[ + { + "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 and London?" + } + ], + "stream": true, + "stream_options": { + "include_usage": true + }, + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "strict": true, + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + } + } + } + ] + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_MDti4mtc0TKeNC0HyE8wy9nn\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"n\\\": \\\"N\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ew Y\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ork\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"call_eA8ose7WzOz5tFM3vdFNGf71\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"{\\\"lo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"catio\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\": \\\"L\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"ondo\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"usage\":null,\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"function\":{\"arguments\":\"n\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AENquPanB4iiLFrzLOCmMH9FDiZDZ\",\"object\":\"chat.completion.chunk\",\"created\":1727991644,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[],\"usage\":{\"prompt_tokens\":56,\"completion_tokens\":45,\"total_tokens\":101,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0}}}\n\ndata: [DONE]\n\n", + "rawHeaders": [ + "Date", + "Thu, 03 Oct 2024 21:40:45 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "866", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9999", + "x-ratelimit-remaining-tokens", + "199957", + "x-ratelimit-reset-requests", + "8.64s", + "x-ratelimit-reset-tokens", + "12ms", + "x-request-id", + "req_056e8b505e78d2ed76be327b4402d881", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cd0179fae8f137e-YVR" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-tool-calls.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-tool-calls.json new file mode 100644 index 00000000..f1c963e9 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-tool-calls.json @@ -0,0 +1,101 @@ +[ + { + "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 customer support assistant. Use the supplied tools to assist the user." + }, + { + "role": "user", + "content": "Hi, can you tell me the delivery date for my order?" + }, + { + "role": "assistant", + "content": "Hi there! I can help with that. Can you please provide your order ID?" + }, + { + "role": "user", + "content": "i think it is order_12345" + } + ], + "stream": true, + "stream_options": { + "include_usage": true + }, + "tools": [ + { + "type": "function", + "function": { + "name": "get_delivery_date", + "description": "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'", + "parameters": { + "type": "object", + "properties": { + "order_id": { + "type": "string", + "description": "The customer's order ID." + } + }, + "required": [ + "order_id" + ], + "additionalProperties": false + } + } + } + ] + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_GqsvoRkHMjAlIhoSWKP6D2lw\",\"type\":\"function\",\"function\":{\"name\":\"get_delivery_date\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"order\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_id\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"order\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"123\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"45\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-AYzUsU4b2gwkf0NUyCyrHCnHAP0zZ\",\"object\":\"chat.completion.chunk\",\"created\":1732902910,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_3de1288069\",\"choices\":[],\"usage\":{\"prompt_tokens\":140,\"completion_tokens\":19,\"total_tokens\":159,\"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": [ + "Date", + "Fri, 29 Nov 2024 17:55:11 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "561", + "openai-version", + "2020-10-01", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9993", + "x-ratelimit-remaining-tokens", + "199921", + "x-ratelimit-reset-requests", + "55.376s", + "x-ratelimit-reset-tokens", + "23ms", + "x-request-id", + "req_4887a849fdef2c2de4b074554e4e3cac", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8ea477962ecd8450-YVR", + "alt-svc", + "h3=\":443\"; ma=86400" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-include_usage.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-include_usage.json new file mode 100644 index 00000000..502d62f2 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-include_usage.json @@ -0,0 +1,65 @@ +[ + { + "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 the falkland islands?" + } + ], + "stream": true, + "stream_options": { + "include_usage": true + } + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"South\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Atlantic\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWU3Xrb2VvjvgOw4Z0M2P6HXV8I\",\"object\":\"chat.completion.chunk\",\"created\":1727828930,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_f85bea6784\",\"choices\":[],\"usage\":{\"prompt_tokens\":24,\"completion_tokens\":4,\"total_tokens\":28,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0}}}\n\ndata: [DONE]\n\n", + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:50 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "64", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9997", + "x-ratelimit-remaining-tokens", + "199966", + "x-ratelimit-reset-requests", + "24.673s", + "x-ratelimit-reset-tokens", + "10ms", + "x-request-id", + "req_5c391ec3893402c26add377f56fa4a9f", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc0931e9a1e2dab-YVR" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-tee.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-tee.json new file mode 100644 index 00000000..330fcc2c --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/streaming-with-tee.json @@ -0,0 +1,65 @@ +[ + { + "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 the falkland islands?" + } + ], + "stream": true, + "stream_options": { + "include_usage": true + } + }, + "status": 200, + "response": "data: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"South\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Atlantic\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\" Ocean\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null}\n\ndata: {\"id\":\"chatcmpl-ADhWV3ea6yO6vGVqJHGBmyWAQ0DXV\",\"object\":\"chat.completion.chunk\",\"created\":1727828931,\"model\":\"gpt-4o-mini-2024-07-18\",\"system_fingerprint\":\"fp_1bb46167f9\",\"choices\":[],\"usage\":{\"prompt_tokens\":24,\"completion_tokens\":4,\"total_tokens\":28,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0}}}\n\ndata: [DONE]\n\n", + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:51 GMT", + "Content-Type", + "text/event-stream; charset=utf-8", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "128", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9996", + "x-ratelimit-remaining-tokens", + "199966", + "x-ratelimit-reset-requests", + "32.699s", + "x-ratelimit-reset-tokens", + "10ms", + "x-request-id", + "req_50ac1e0fdcc58ce38aade7491555a9b5", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc0932288328413-YVR" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/nock-recordings/tool-calls.json b/packages/instrumentation-openai/test/fixtures/nock-recordings/tool-calls.json new file mode 100644 index 00000000..92ec2ea5 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/nock-recordings/tool-calls.json @@ -0,0 +1,100 @@ +[ + { + "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 customer support assistant. Use the supplied tools to assist the user." + }, + { + "role": "user", + "content": "Hi, can you tell me the delivery date for my order?" + }, + { + "role": "assistant", + "content": "Hi there! I can help with that. Can you please provide your order ID?" + }, + { + "role": "user", + "content": "i think it is order_12345" + } + ], + "tools": [ + { + "type": "function", + "function": { + "name": "get_delivery_date", + "description": "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'", + "parameters": { + "type": "object", + "properties": { + "order_id": { + "type": "string", + "description": "The customer's order ID." + } + }, + "required": [ + "order_id" + ], + "additionalProperties": false + } + } + } + ] + }, + "status": 200, + "response": [ + "1f8b0800000000000003000000ffff", + "6c525d6f9b30147de75758f7394c849291f016a90f6db575cd34ad52970939e6024e8dedda665a14e5bf5710be92960764eee19c73efb93e7a8400cf2021c04aea58a585bfbe2d9f9fadaeef8d5a3eadbfbed1f5af3d2f37fbd7fd62f31d660d43edf6c85ccffac254a5053aaee4196606a9c346751e87f1325cae6ec216a85486a2a115daf991f22b2eb91f0661e407b13f5f76ec5271861612f2c723849063fb6efa9419fe878404b3be52a1b5b44048869f0801a34453016a2db78e4a07b311644a3a944debb216620238a544caa810a3f1f9394ece63585488f4f7d3cf72f52d780983e271bfbabd7fd93cbcdd3dc63f267e67e9836e1bca6bc9869026f8504faecc080149ab965ba04b3314fc1f9a439a51875722840035455da174cd0070dc8232199a94675b48fa8f7978132db670820beec9fbecfc77128ec1bcb65474a975f5d3b006a10a6dd4ce5ea50a3997dc96a9416adbe9a6217bbd5beb03f5c51e411b5569973af58ab2919d47ddd261bc6b1374d5814e392a26f5450f5ce8a5193acadb450f778b5156623652036f32e247d3cf24ce6372597c50f13a25b007ebb04a732e0b34daf0f62242aed338dad128de45948177f2de010000ffff0300e8f4291f96030000" + ], + "rawHeaders": [ + "Date", + "Wed, 02 Oct 2024 00:28:52 GMT", + "Content-Type", + "application/json", + "Transfer-Encoding", + "chunked", + "Connection", + "keep-alive", + "access-control-expose-headers", + "X-Request-ID", + "openai-organization", + "elastic-observability", + "openai-processing-ms", + "420", + "openai-version", + "2020-10-01", + "strict-transport-security", + "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests", + "10000", + "x-ratelimit-limit-tokens", + "200000", + "x-ratelimit-remaining-requests", + "9994", + "x-ratelimit-remaining-tokens", + "199921", + "x-ratelimit-reset-requests", + "48.715s", + "x-ratelimit-reset-tokens", + "23ms", + "x-request-id", + "req_7fe6087cc9b2b2c0f6fa81836d95cff2", + "CF-Cache-Status", + "DYNAMIC", + "X-Content-Type-Options", + "nosniff", + "Server", + "cloudflare", + "CF-RAY", + "8cc0932a6df77107-YVR", + "Content-Encoding", + "gzip" + ], + "responseIsBinary": false + } +] \ No newline at end of file diff --git a/packages/instrumentation-openai/test/fixtures/streaming-abort.js b/packages/instrumentation-openai/test/fixtures/streaming-abort.js new file mode 100644 index 00000000..a2a27f0c --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-abort.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + stream: true, + }); + for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + // Ensure that abort works when instrumented. + stream.controller.abort(); + } +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-bad-iterate.js b/packages/instrumentation-openai/test/fixtures/streaming-bad-iterate.js new file mode 100644 index 00000000..070e40bc --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-bad-iterate.js @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + stream: true, + }); + + for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); + + // Bad: iterate twice. + for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-chat-completion.js b/packages/instrumentation-openai/test/fixtures/streaming-chat-completion.js new file mode 100644 index 00000000..a276ae42 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-chat-completion.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + stream: true, + }); + for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-parallel-tool-calls.js b/packages/instrumentation-openai/test/fixtures/streaming-parallel-tool-calls.js new file mode 100644 index 00000000..94939545 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-parallel-tool-calls.js @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +const tools = [ + { + type: 'function', + function: { + name: 'get_weather', + strict: true, + parameters: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + required: ['location'], + additionalProperties: false, + }, + }, + }, +]; + +async function main() { + const client = createOpenAIClient(); + const messages = [ + { + role: 'system', + content: 'You are a helpful assistant providing weather updates.', + }, + { role: 'user', content: 'What is the weather in New York and London?' }, + ]; + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: messages, + stream: true, + stream_options: { + include_usage: true, + }, + tools: tools, + }); + for await (const chunk of stream) { + console.dir(chunk, { depth: 50 }); + } +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-tools.js b/packages/instrumentation-openai/test/fixtures/streaming-tools.js new file mode 100644 index 00000000..e65e118e --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-tools.js @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +// Example from https://platform.openai.com/docs/guides/function-calling?lang=node.js +const tools = [ + { + type: 'function', + function: { + name: 'get_delivery_date', + description: + "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'", + parameters: { + type: 'object', + properties: { + order_id: { + type: 'string', + description: "The customer's order ID.", + }, + }, + required: ['order_id'], + additionalProperties: false, + }, + }, + }, +]; + +async function main() { + const client = createOpenAIClient(); + const messages = [ + { + role: 'system', + content: + 'You are a helpful customer support assistant. Use the supplied tools to assist the user.', + }, + { + role: 'user', + content: 'Hi, can you tell me the delivery date for my order?', + }, + { + role: 'assistant', + content: + 'Hi there! I can help with that. Can you please provide your order ID?', + }, + { role: 'user', content: 'i think it is order_12345' }, + ]; + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: messages, + stream: true, + stream_options: { + include_usage: true, + }, + tools: tools, + }); + for await (const chunk of stream) { + console.dir(chunk, { depth: 50 }); + } +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-with-include_usage.js b/packages/instrumentation-openai/test/fixtures/streaming-with-include_usage.js new file mode 100644 index 00000000..1d9e0527 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-with-include_usage.js @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + stream: true, + // Note for testing: Ollama doesn't yet support stream_options. + // See https://github.com/ollama/ollama/issues/5200 + stream_options: { + include_usage: true, + }, + }); + for await (const chunk of stream) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/streaming-with-tee.js b/packages/instrumentation-openai/test/fixtures/streaming-with-tee.js new file mode 100644 index 00000000..963179b4 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/streaming-with-tee.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +async function main() { + const client = createOpenAIClient(); + const stream = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: [ + { + role: 'user', + content: + 'Answer in up to 3 words: Which ocean contains the falkland islands?', + }, + ], + stream: true, + // Ollama doesn't support stream_options. + stream_options: { + include_usage: true, + }, + }); + + // Use `stream.tee()` to ensure it works when instrumented. + const [left, right] = stream.tee(); + for await (const chunk of left) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); + for await (const chunk of right) { + process.stdout.write(chunk.choices[0]?.delta?.content || ''); + } + process.stdout.write('\n'); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/fixtures/telemetry.js b/packages/instrumentation-openai/test/fixtures/telemetry.js new file mode 100644 index 00000000..67246c1a --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/telemetry.js @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +// Setup OTel telemetry using the OpenAI instrumentation in this repo. + +const os = require('os'); +const { NodeSDK } = require('@opentelemetry/sdk-node'); +const { + OTLPMetricExporter, +} = require('@opentelemetry/exporter-metrics-otlp-proto'); +const { + OTLPTraceExporter, +} = require('@opentelemetry/exporter-trace-otlp-proto'); +const { OTLPLogExporter } = require('@opentelemetry/exporter-logs-otlp-proto'); +const { BatchLogRecordProcessor } = require('@opentelemetry/sdk-logs'); +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); +const { OpenAIInstrumentation } = require('../../'); // @elastic/opentelemetry-instrumentation-openai +const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); + +// const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api'); +// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + +// At time of writing, NodeSDK does not automatically provide a LoggerProvider +// so we must manually pass one in. Note that this simple implementation does +// not respond to the OTel specified OTEL_EXPORTER_OTLP_LOGS_PROTOCOL and +// OTEL_EXPORTER_OTLP_PROTOCOL envvars. +const logRecordProcessor = new BatchLogRecordProcessor(new OTLPLogExporter()); + +const metricReader = new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter(), +}); + +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter(), + logRecordProcessor, + metricReader, + instrumentations: [new HttpInstrumentation(), new OpenAIInstrumentation()], +}); + +process.on('SIGTERM', async () => { + // SDK shutdown does not seem to flush metrics. + await metricReader.forceFlush(); + try { + await sdk.shutdown(); + } catch (err) { + console.warn('warning: error shutting down OTel SDK', err); + } + process.exit(128 + os.constants.signals.SIGTERM); +}); + +process.once('beforeExit', async () => { + // SDK shutdown does not seem to flush metrics. + await metricReader.forceFlush(); + // Flush recent telemetry data if about to shutdown. + try { + await sdk.shutdown(); + } catch (err) { + console.warn('warning: error shutting down OTel SDK', err); + } +}); + +sdk.start(); diff --git a/packages/instrumentation-openai/test/fixtures/tools.js b/packages/instrumentation-openai/test/fixtures/tools.js new file mode 100644 index 00000000..9c882294 --- /dev/null +++ b/packages/instrumentation-openai/test/fixtures/tools.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +const { createOpenAIClient, runFnWithNockBack } = require('../testutils'); + +// Example from https://platform.openai.com/docs/guides/function-calling?lang=node.js +const tools = [ + { + type: 'function', + function: { + name: 'get_delivery_date', + description: + "Get the delivery date for a customer's order. Call this whenever you need to know the delivery date, for example when a customer asks 'Where is my package'", + parameters: { + type: 'object', + properties: { + order_id: { + type: 'string', + description: "The customer's order ID.", + }, + }, + required: ['order_id'], + additionalProperties: false, + }, + }, + }, +]; + +async function main() { + const client = createOpenAIClient(); + const messages = [ + { + role: 'system', + content: + 'You are a helpful customer support assistant. Use the supplied tools to assist the user.', + }, + { + role: 'user', + content: 'Hi, can you tell me the delivery date for my order?', + }, + { + role: 'assistant', + content: + 'Hi there! I can help with that. Can you please provide your order ID?', + }, + { role: 'user', content: 'i think it is order_12345' }, + ]; + const response = await client.chat.completions.create({ + model: process.env.TEST_MODEL_TOOLS, + messages: messages, + tools: tools, + }); + console.dir(response, { depth: 50 }); +} + +if (process.env.TEST_NOCK_BACK_MODE) { + runFnWithNockBack( + main, + process.env.TEST_FIXTURE_RECORDING_NAME, + process.env.TEST_NOCK_BACK_MODE + ); +} else { + main(); +} diff --git a/packages/instrumentation-openai/test/testutils.js b/packages/instrumentation-openai/test/testutils.js new file mode 100644 index 00000000..e1b1f6b3 --- /dev/null +++ b/packages/instrumentation-openai/test/testutils.js @@ -0,0 +1,693 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +'use strict'; + +// A dumping ground for testing utility functions. + +const assert = require('assert/strict'); +const fs = require('fs'); +const { execFile } = require('child_process'); +const path = require('path'); + +const moduleDetailsFromPath = require('module-details-from-path'); +const nock = require('nock'); +const { OpenAI, AzureOpenAI } = require('openai'); +const semver = require('semver'); +const { + MockOtlpServer, + normalizeLogs, + normalizeTrace, + normalizeMetrics, +} = require('@elastic/mockotlpserver'); + +/** + * Lookup the property "str" (given in dot-notation) in the object "obj". + * If the property isn't found, then `undefined` is returned. + */ +function dottedLookup(obj, str) { + var o = obj; + var fields = str.split('.'); + for (var i = 0; i < fields.length; i++) { + var field = fields[i]; + if (!Object.prototype.hasOwnProperty.call(o, field)) { + return undefined; + } + o = o[field]; + } + return o; +} + +/** + * Return the first element in the array that has a `key` with the given `val`; + * or if `val` is undefined, then the first element with any value for the given + * `key`. + * + * The `key` maybe a nested field given in dot-notation, for example: + * 'context.db.statement'. + */ +function findObjInArray(arr, key, val) { + let result = null; + arr.some(function (elm) { + const actualVal = dottedLookup(elm, key); + if (val === undefined) { + if (actualVal !== undefined) { + result = elm; + return true; + } + } else { + if (actualVal === val) { + result = elm; + return true; + } + } + return false; + }); + return result; +} + +/** + * Assert the given object matches given expectations. + * + * The fields in `expected` are compared against the equivalent field in `actual`: + * - if `expected` is a RegExp, comparison is via Tape's `t.match()` + * - if `actual` and `expected` are both arrays, a deep comparison is done, + * - if `actual` and `expected` are both objects, a deep comparison is done, + * - if `expected` is a function, it is passed the actual value and the + * return value is asserted with `t.ok()` + * - otherwise comparison is via `t.equal()` + * + * assertDeepMatch(t, {foo: 'bar'}, {foo: /^b/}); + * assertDeepMatch(t, process.versions, {node: /^18/, napi: '9'}); + */ +function assertDeepMatch(t, actual, expected, msgPrefix = 'obj') { + if (expected instanceof RegExp) { + t.match(actual, expected, msgPrefix); + } else if (typeof expected === 'function') { + t.ok(expected(actual), msgPrefix); + } else if (Array.isArray(actual) && Array.isArray(expected)) { + t.equal(actual.length, expected.length, msgPrefix + '.length'); + for (let i = 0; i < Math.min(actual.length, expected.length); i++) { + assertDeepMatch(t, actual[i], expected[i], msgPrefix + `[${i}]`); + } + } else if ( + actual != null && + typeof actual === 'object' && + expected != null && + typeof expected === 'object' + ) { + for (let k in expected) { + const a = actual[k]; + const e = expected[k]; + const kPrefix = msgPrefix + (isIdentifier.test(k) ? `.${k}` : `['${k}']`); + assertDeepMatch(t, a, e, kPrefix); + } + } else { + t.equal(actual, expected, msgPrefix); + } +} + +// isIdentifier based on https://github.com/sindresorhus/identifier-regex +const isIdentifier = /^[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}]*$/u; + +/** + * "Safely" get the version of the given package, if possible. Otherwise return + * null. + * Here "safely" means avoiding `require("$packageName/package.json")` because + * that can fail if the package uses an old form of "exports" + * (e.g. https://github.com/elastic/apm-agent-nodejs/issues/2350). + * + * @param {string} packageName + * @returns {string | null} + */ +function safeGetPackageVersion(packageName) { + let file; + try { + file = require.resolve(packageName); + } catch (_err) { + return null; + } + + // Use the same logic as require-in-the-middle for finding the 'basedir' of + // the package from `file`. + const details = moduleDetailsFromPath(file); + if (!details) { + return null; + } + + try { + const pkgContents = fs.readFileSync(details.basedir + '/package.json', { + encoding: 'utf-8', + }); + return JSON.parse(pkgContents).version; + } catch (_err) { + return null; + } +} + +// Match ANSI escapes (from https://stackoverflow.com/a/29497680/14444044). +const ANSI_RE = + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; /* eslint-disable-line no-control-regex */ + +/** + * Format the given data for passing to `t.comment()`. + * + * - t.comment() wipes leading whitespace. Prefix lines with '|' to avoid + * that, and to visually group a multi-line write. + * - Drop ANSI escape characters, because those include control chars that + * are illegal in XML. When we convert TAP output to JUnit XML for + * Jenkins, then Jenkins complains about invalid XML. `FORCE_COLOR=0` + * can be used to disable ANSI escapes in `next dev`'s usage of chalk, + * but not in its coloured exception output. + */ +function formatForTComment(data) { + return ( + data + .toString('utf8') + .replace(ANSI_RE, '') + .trimRight() + .replace(/\r?\n/g, '\n|') + '\n' + ); +} + +/** + * @param {string} a + * @returns {string} + */ +function quoteArg(a) { + if (a.includes("'")) { + return "'" + a.replace("'", "'\\''") + "'"; + } else if (a.includes('"') || a.includes('$')) { + return "'" + a + "'"; + } else if (a.includes(' ')) { + return '"' + a + '"'; + } else { + return a; + } +} + +/** + * @param {string[]} argv + * @returns {string} + */ +function quoteArgv(argv) { + return argv.map(quoteArg).join(' '); +} + +/** + * Returns a representation of the environment variables in string type + * quoting them if the values have certain characters + * + * process.env = { + * OTEL_SERVICE_NAME: 'my service', + * OTEL_LOG_LEVEL: 'trace', + * }; + * + * console.log(quoteEnv); + * // prints: OTEL_SERVICE_NAME="my service" OTEL_LOG_LEVEL=trace + * + * @param {NodeJS.ProcessEnv | undefined} env + * @returns {string} + */ +function quoteEnv(env) { + if (!env) { + return ''; + } + return Object.keys(env) + .map(k => { + // Environment values should be strings, but be defensive. + const v = + typeof env[k] === 'string' + ? env[k] + : env[k] == null + ? '' + : env[k].toString(); + return `${k}=${quoteArg(v)}`; + }) + .join(' '); +} + +// TODO: move this types to packages/mockotlpserver/lib/normalize.js +/** + * @typedef {Object} DataPointDouble + * @property {string} startTimeUnixNano + * @property {string} timeUnixNano + * @property {number} asDouble + * @property {Record} + */ + get sortedSpans() { + const spans = []; + this.rawTraces.forEach(rawTrace => { + const normTrace = normalizeTrace(rawTrace); + normTrace.resourceSpans.forEach(resourceSpan => { + resourceSpan.scopeSpans.forEach(scopeSpan => { + scopeSpan.spans.forEach(span => { + span.resource = resourceSpan.resource; + span.scope = scopeSpan.scope; + spans.push(span); + }); + }); + }); + }); + spans.sort((a, b) => { + assert(typeof a.startTimeUnixNano === 'string'); + assert(typeof b.startTimeUnixNano === 'string'); + let aStartInt = BigInt(a.startTimeUnixNano); + let bStartInt = BigInt(b.startTimeUnixNano); + + if (aStartInt === bStartInt) { + // Fast-created spans that start in the same millisecond cannot + // reliably be sorted, because OTel JS currently doesn't have + // sub-ms resolution. Attempt to improve the sorting by using + // `spanId` and `parentSpanId`: a span cannot start before its + // parent. + if (a.parentSpanId && a.parentSpanId === b.spanId) { + aStartInt += 1n; + } else if (b.parentSpanId && b.parentSpanId === a.spanId) { + bStartInt += 1n; + } + } + + return aStartInt < bStartInt ? -1 : aStartInt > bStartInt ? 1 : 0; + }); + + // NOTE: Ignore spans from the GCP detector for testing. It shouldn't be + // creating spans, but that should be fixed upstream. + return spans.filter(s => { + const attrs = s.attributes; + const url = attrs && attrs['http.url']; + // GCP detector does request to a specific path + return !url || !url.endsWith('/computeMetadata/v1/instance'); + }); + } + + get metrics() { + const metrics = []; + + this.rawMetrics.forEach(rawMetric => { + const normMetric = normalizeMetrics(rawMetric); + normMetric.resourceMetrics.forEach(resourceMetrics => { + // The `?.` usages are guards against empty array values (e.g. a + // MetricsServiceRequest with no ScopeMetrics) being normalized + // to the `.scopeMetrics` attribute not being set. I'm not sure + // that normalization isn't a bug itself. + resourceMetrics.scopeMetrics?.forEach(scopeMetrics => { + scopeMetrics.metrics?.forEach(metric => { + metric.resource = resourceMetrics.resource; + metric.scope = scopeMetrics.scope; + metrics.push(metric); + }); + }); + }); + }); + + return metrics; + } + + get logs() { + const logs = []; + this.rawLogs.forEach(logsServiceRequest => { + const normLogs = normalizeLogs(logsServiceRequest); + normLogs.resourceLogs.forEach(resourceLogs => { + resourceLogs.scopeLogs.forEach(scopeLogs => { + scopeLogs.logRecords.forEach(logRecord => { + logRecord.resource = resourceLogs.resource; + logRecord.scope = scopeLogs.scope; + logs.push(logRecord); + }); + }); + }); + }); + + return logs; + } +} + +/** + * @callback CheckResultCallback + * @param {import('tape').Test} t + * @param {import('child_process').ExecFileException | undefined} err + * @param {string} stdout + * @param {string} stderr + */ +/** + * @callback CheckTelemetryCallback + * @param {import('tape').Test} t + * @param {CollectorStore} collector + */ + +/** + * Run a series of "test fixture" tests. Each test fixture is an object that + * defines a Node.js script to run, how to run it (arguments, env, cwd), + * and function(s) to check the results after it is run. This runner starts + * a MockOTLPServer for the script to use. + * + * Assuming a "fixtures/hello.js" script like this: + * + * const http = require('http'); + * http.get('http://www.google.com/', (res) => { res.resume(); }); + * + * a simple example is: + * + * const testFixtures = [ + * { + * args: ['-r', '@elastic/opentelemetry-node', 'fixtures/hello.js'], + * cwd: __dirname, + * verbose: true, // use to get debug output for the script's run + * checkTelemetry: (t, tel) => { + * const spans = tel.sortedSpans; + * t.equal(spans.length, 1) + * t.ok(spans[0].name, 'GET') + * } + * } + * ] + * test('module fixtures', suite => { + * runTestFixtures(suite, testFixtures) + * suite.end() + * }) + * + * Each `testFixtures` script will be executed with a configured + * OTEL_EXPORTER_OTLP_ENDPOINT. By default it asserts that the script exits + * successfully. + * + * See the options below for controlling how the script is run, how to + * check the script output, whether to run or skip with the current node + * version, etc. + * + * @typedef {Object} TestFixture + * @property {string} [name] The name of the test. + * @property {Array} args The args to `node`. + * @property {string} [cwd] Typically this is `__dirname`, then the `args` can + * be relative to the test file. + * @property {number} [timeout] A timeout number of milliseconds for the process + * to execute. Default is no timeout. + * @property {number} [maxBuffer] A maxBuffer to use for the exec. + * @property {NodeJS.ProcessEnv} [env] Any custom envvars, e.g. `{NODE_OPTIONS:...}`. + * @property {boolean} [verbose] Set to `true` to include `t.comment()`s showing + * the command run and its output. This can be helpful to run the script + * manually for dev/debugging. + * @property {boolean} [only] For development, the set of test fixtures run can + * be limited by setting `only: true`. + * @property {import('tape').TestOptions} [testOpts] Additional tape test opts, if any. https://github.com/ljharb/tape#testname-opts-cb + * @property {Record>} [versionRanges] A mapping of + * required version ranges for either "node" or a given module name. If + * current versions don't satisfy, then the test will be skipped. E.g. this + * is common for ESM tests: + * versionRanges: { + * node: NODE_VER_RANGE_IITM + * } + * @property {CheckResultCallback} [checkResult] Check the exit and output of the + * script: `checkResult(t, err, stdout, stderr)`. If not provided, by + * default it will be asserted that the script exited successfully. + * @property {CheckTelemetryCallback} [checkTelemetry] Check the results + * received by the mock OTLP server. `checkTelemetry(t, collector, stdout, + * stderr)`. The second arg is a `TestCollector` object that has some + * convenience methods to use the collected data. + * + * @param {import('tape').Test} suite + * @param {Array} testFixtures + * @returns {Promise} + */ +function runTestFixtures(suite, testFixtures) { + // Handle fixtures with `only: true`, if any. + const onlyTestFixtures = testFixtures.filter(tf => Boolean(tf.only)); + if (onlyTestFixtures.length > 0) { + suite.comment( + `ONLY: limiting to "only" ${onlyTestFixtures.length} of ${testFixtures.length} testFixtures` + ); + testFixtures = onlyTestFixtures; + } + + // Wrap each test suite in a promise so we can await for it + const suitePromises = testFixtures.map(tf => { + // eslint-disable-next-line -- named `outerResolve` to differentiate from the one in inner Promise + return new Promise((outerResolve) => { + const testName = tf.name ?? quoteArgv(tf.args); + const testOpts = Object.assign({}, tf.testOpts); + suite.test(testName, testOpts, async t => { + // Handle "tf.versionRanges"-based skips here, because `tape` doesn't + // print any message for `testOpts.skip`. + if (tf.versionRanges) { + for (const name in tf.versionRanges) { + const ver = + name === 'node' ? process.version : safeGetPackageVersion(name); + const verRanges = Array.isArray(tf.versionRanges[name]) + ? tf.versionRanges[name] + : [tf.versionRanges[name]]; + for (let verRange of verRanges) { + if (!semver.satisfies(ver, verRange)) { + t.comment( + `SKIP ${name} ${ver} is not supported by this fixture (requires: ${verRanges.join( + ', ' + )})` + ); + t.end(); + return; + } + } + } + } + + const collector = new TestCollector(); + const otlpServer = new MockOtlpServer({ + logLevel: 'warn', + services: ['http'], + httpHostname: '127.0.0.1', // avoid default 'localhost' because possible IPv6 + httpPort: 0, + onTrace: collector.onTrace.bind(collector), + onMetrics: collector.onMetrics.bind(collector), + onLogs: collector.onLogs.bind(collector), + }); + await otlpServer.start(); + + const cwd = tf.cwd || process.cwd(); + if (tf.verbose) { + t.comment( + `running: (cd "${cwd}" && ${quoteEnv(tf.env)} node ${quoteArgv( + tf.args + )})` + ); + } + const start = Date.now(); + return new Promise(resolve => { + execFile( + process.execPath, + tf.args, + { + cwd, + timeout: tf.timeout || undefined, + killSignal: 'SIGINT', + env: Object.assign( + {}, + process.env, + { + OTEL_EXPORTER_OTLP_ENDPOINT: otlpServer.httpUrl.href, + OTEL_EXPORTER_OTLP_PROTOCOL: 'http/json', + }, + tf.env + ), + maxBuffer: tf.maxBuffer, + }, + async function done(err, stdout, stderr) { + if (tf.verbose) { + t.comment(`elapsed: ${(Date.now() - start) / 1000}s`); + if (err) { + t.comment(`err:\n|${formatForTComment(err)}`); + } + if (stdout) { + t.comment(`stdout:\n|${formatForTComment(stdout)}`); + } else { + t.comment('stdout: '); + } + if (stderr) { + t.comment(`stderr:\n|${formatForTComment(stderr)}`); + } else { + t.comment('stderr: '); + } + } + if (tf.checkResult) { + await tf.checkResult(t, err, stdout, stderr); + } else { + t.error(err, `exited successfully: err=${err}`); + if (err) { + if (!tf.verbose) { + t.comment(`stdout:\n|${formatForTComment(stdout)}`); + t.comment(`stderr:\n|${formatForTComment(stderr)}`); + } + } + } + if (tf.checkTelemetry) { + if (!tf.checkResult && err) { + t.comment('skip checkTelemetry because process errored out'); + } else { + await tf.checkTelemetry(t, collector, stdout, stderr); + } + } + await otlpServer.close(); + t.end(); + resolve(); + outerResolve(); + } + ); + }); + }); + }); + }); + + return Promise.all(suitePromises); +} + +/** + * Run the given function `fn` with Nock Back. + * https://github.com/nock/nock#nock-back + * + * `nock` is a Node.js package for mocking of the Node.js `http` and `https` + * modules. Nock "Back" is nock's feature for recording HTTP request/responses + * to file and then replaying them in subsequent runs. We are using Nock Back to + * (a) record OpenAI responses for executions of "fixture/*.js" scripts, and + * (b) to mock HTTP requests with those recorded responses for regular test + * runs. This allows for (1) testing against real OpenAI responses and + * (2) stable test runs. + * + * @param {Function} fn - The function to run with nock back. + * @param {string} recordingName - The base name of the file in which to store + * the recordings ("test/fixtures/nock-recordings/${recordingName}.json"). + * @param {nockBackMode} - https://github.com/nock/nock#modes + * Typically "lockdown" is used for a test run, "update" is used for + * (re)generating recordings. + */ +async function runFnWithNockBack(fn, recordingName, nockBackMode) { + // Remove any data from recorded responses that could have sensitive data + // and that we don't need for testing. + const sanitizeRecordedNockScopes = scopes => { + for (const scope of scopes) { + for (let i = 0; i < scope.rawHeaders.length; i += 2) { + if (scope.rawHeaders[i].toLowerCase() === 'set-cookie') { + scope.rawHeaders.splice(i, 2); + } + } + } + return scopes; + }; + + assert(recordingName); + nock.back.setMode(nockBackMode); + nock.back.fixtures = path.join(__dirname, 'fixtures', 'nock-recordings'); + const { nockDone } = await nock.back(recordingName + '.json', { + afterRecord: sanitizeRecordedNockScopes, + }); + try { + await fn(); + } finally { + nockDone(); + // This removes nock's monkey-patching of `http` and `https`. This is + // necessary so that the OTel SDK being run with the script is able + // to use HTTP to send its telemetry data. This assumes telemetry is + // only exported after `fn` is done. So far that has been fine, + // because the fixture scripts are fast. + // TODO: Is it possible to configure nock to skip intercepting OTEL_EXPORTER_OTLP_ENDPOINT? + nock.restore(); + } +} + +/** + * Returns an OpenAI client instance, using the AzureOpenAI client + * class if `AZURE_OPENAI_API_KEY` is defined. + */ +function createOpenAIClient() { + const clientCtor = process.env.AZURE_OPENAI_API_KEY ? AzureOpenAI : OpenAI; + return new clientCtor(); +} + +module.exports = { + findObjInArray, + assertDeepMatch, + formatForTComment, + safeGetPackageVersion, + runTestFixtures, + runFnWithNockBack, + createOpenAIClient, +}; diff --git a/packages/instrumentation-openai/tsconfig.base.json b/packages/instrumentation-openai/tsconfig.base.json new file mode 100644 index 00000000..8f5d77c3 --- /dev/null +++ b/packages/instrumentation-openai/tsconfig.base.json @@ -0,0 +1,27 @@ +// Based on tsconfig.base.json from opentelemetry-js-contrib.git. +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "declarationMap": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "module": "commonjs", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "pretty": true, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "es2017", + "incremental": true, + "newLine": "LF" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/packages/instrumentation-openai/tsconfig.json b/packages/instrumentation-openai/tsconfig.json new file mode 100644 index 00000000..8b9dcf10 --- /dev/null +++ b/packages/instrumentation-openai/tsconfig.json @@ -0,0 +1,12 @@ +// Based on tsconfig for instrumentations in opentelemetry-js-contrib.git. +{ + "extends": "./tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/mockotlpserver/package.json b/packages/mockotlpserver/package.json index 0ef233ff..366fca16 100644 --- a/packages/mockotlpserver/package.json +++ b/packages/mockotlpserver/package.json @@ -25,6 +25,7 @@ "node": ">=14.17.0" }, "scripts": { + "clean": "rm -rf node_modules", "lint": "npm run lint:eslint && npm run lint:types && npm run lint:deps && npm run lint:license-files", "lint:eslint": "eslint --ext=js,mjs,cjs . # requires node >=16.0.0", "lint:types": "tsc", diff --git a/packages/opentelemetry-node/TESTING.md b/packages/opentelemetry-node/TESTING.md index eb2cd017..2aca5b52 100644 --- a/packages/opentelemetry-node/TESTING.md +++ b/packages/opentelemetry-node/TESTING.md @@ -3,6 +3,9 @@ tl;dr: To run all tests locally: ``` +(cd ../mockotlpserver && npm ci) # used a devDep +npm ci + npm run test-services:start npm test npm run test-services:stop diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 64bd8208..3268f557 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -39,6 +39,7 @@ "require.js" ], "scripts": { + "clean": "rm -rf node_modules test/fixtures/a-ts-proj/node_modules", "example": "cd ../../examples && node -r @elastic/opentelemetry-node simple-http-request.js", "lint": "npm run lint:eslint && npm run lint:types && npm run lint:deps && npm run lint:license-files && npm run lint:changelog", "lint:eslint": "eslint --ext=js,mjs,cjs . # requires node >=16.0.0",