From c60b89d0dc531e18bcaeddb19c603b041f8c8979 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 14:07:34 +0100 Subject: [PATCH 01/10] chore: switch to oxfmt, oxlint - add ci checks --- .eslintignore | 4 - .github/workflows/code-quality.yml | 73 ++ .github/workflows/pr_checks.yml | 6 + .oxfmtrc.json | 25 + .oxlintrc.json | 24 + .prettierignore | 10 - apps/webapp/.eslintrc | 31 - apps/webapp/.prettierignore | 11 - apps/webapp/package.json | 18 +- apps/webapp/prettier.config.js | 4 - package.json | 7 +- pnpm-lock.yaml | 1778 +++++++--------------------- prettier.config.js | 11 - turbo.json | 69 +- 14 files changed, 594 insertions(+), 1477 deletions(-) delete mode 100644 .eslintignore create mode 100644 .github/workflows/code-quality.yml create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 .prettierignore delete mode 100644 apps/webapp/.eslintrc delete mode 100644 apps/webapp/.prettierignore delete mode 100644 apps/webapp/prettier.config.js delete mode 100644 prettier.config.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 827344f1f96..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -*/**.js -*/**.d.ts -packages/*/dist -packages/*/lib \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 00000000000..23ad4f79505 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,73 @@ +name: "šŸŽØ Format & Lint" + +on: + workflow_call: + +permissions: + contents: read + +jobs: + code-quality: + runs-on: ubuntu-latest + + steps: + - name: ā¬‡ļø Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: āŽ” Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 10.33.2 + + - name: āŽ” Setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 20.20.2 + cache: "pnpm" + + - name: šŸ“„ Download deps + run: pnpm install --frozen-lockfile + + # Gate only on files this PR adds/modifies (ratchet) — the repo is not + # fully formatted/linted yet, so a whole-repo check would fail every PR. + - name: šŸ” Determine changed files + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + FILES=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- \ + '*.ts' '*.tsx' '*.js' '*.jsx' '*.mjs' '*.cjs') + echo "Changed code files:" + echo "${FILES:-}" + { + echo "files<> "$GITHUB_OUTPUT" + if [ -z "$FILES" ]; then + echo "has_files=false" >> "$GITHUB_OUTPUT" + else + echo "has_files=true" >> "$GITHUB_OUTPUT" + fi + + # TODO enable format check for ALL code + - name: šŸ’… Check formatting + if: steps.changes.outputs.has_files == 'true' + env: + FILES: ${{ steps.changes.outputs.files }} + # Unquoted $FILES intentionally word-splits the newline-separated list + # into separate path args. Safe given the has_files guard above. + run: pnpm exec oxfmt --check $FILES + + # TODO update this so lint failures fail CI + # Informational only — lint findings are surfaced in the logs but never + # fail the job. Only the formatting check above blocks the PR. + - name: šŸ”Ž Lint (informational, non-blocking) + if: steps.changes.outputs.has_files == 'true' + continue-on-error: true + env: + FILES: ${{ steps.changes.outputs.files }} + run: pnpm exec oxlint $FILES diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 95805539807..1fb3a47a45c 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -102,6 +102,11 @@ jobs: - 'pnpm-workspace.yaml' - 'turbo.json' + code-quality: + needs: changes + if: needs.changes.outputs.code == 'true' + uses: ./.github/workflows/code-quality.yml + typecheck: needs: changes if: needs.changes.outputs.code == 'true' || needs.changes.outputs.typecheck_self == 'true' @@ -155,6 +160,7 @@ jobs: name: All PR Checks needs: - changes + - code-quality - typecheck - webapp - e2e-webapp diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000000..90f19a65790 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,25 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "sortPackageJson": false, + "ignorePatterns": [ + "node_modules", + ".env", + ".env.local", + "pnpm-lock.yaml", + "tailwind.css", + ".babelrc.json", + "**/.react-email/", + "**/storybook-static/", + "**/.changeset/", + "**/dist/", + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000000..463fa955515 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,24 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "react"], + "ignorePatterns": [ + "**/dist/**", + "**/build/**", + "**/*.d.ts", + "**/seed.js", + "**/seedCloud.ts", + "**/populate.js" + ], + "rules": { + "consistent-type-imports": [ + "warn", + { + "prefer": "type-imports", + "disallowTypeAnnotations": true, + "fixStyle": "inline-type-imports" + } + ], + "import/no-duplicates": ["warn", { "prefer-inline": true }], + "react-hooks/rules-of-hooks": "error" + } +} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index a34447dd45d..00000000000 --- a/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -.env -.env.local -pnpm-lock.yaml -tailwind.css -.babelrc.json -**/.react-email/ -**/storybook-static/ -**/.changeset/ -**/dist/ \ No newline at end of file diff --git a/apps/webapp/.eslintrc b/apps/webapp/.eslintrc deleted file mode 100644 index f292eef3cce..00000000000 --- a/apps/webapp/.eslintrc +++ /dev/null @@ -1,31 +0,0 @@ -{ - "plugins": ["react-hooks", "@typescript-eslint/eslint-plugin", "import"], - "parser": "@typescript-eslint/parser", - "overrides": [ - { - "files": ["*.ts", "*.tsx"], - "rules": { - // Autofixes imports from "@trigger.dev/core" to fine grained modules - // "@trigger.dev/no-trigger-core-import": "error", - // Normalize `import type {}` and `import { type }` - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - // the "type" annotation can get tangled and cause syntax errors - // during some autofixes, so easier to just turn it off - "prefer": "type-imports", - "disallowTypeAnnotations": true, - "fixStyle": "inline-type-imports" - } - ], - // no-trigger-core-import splits imports into multiple lines - // this one merges them back into a single line - // if they still import from the same module - "import/no-duplicates": ["warn", { "prefer-inline": true }], - // lots of undeclared vars, enable this rule if you want to clean them up - "turbo/no-undeclared-env-vars": "off" - } - } - ], - "ignorePatterns": ["seed.js", "seedCloud.ts", "populate.js"] -} diff --git a/apps/webapp/.prettierignore b/apps/webapp/.prettierignore deleted file mode 100644 index 835d1a6cdd7..00000000000 --- a/apps/webapp/.prettierignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules - -/build -/public/build -.env - -/cypress/screenshots -/cypress/videos -/postgres-data - -/app/styles/tailwind.css \ No newline at end of file diff --git a/apps/webapp/package.json b/apps/webapp/package.json index c5a4ae57878..571f35620bf 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -10,8 +10,8 @@ "build:sentry": "esbuild --platform=node --format=cjs --outbase=. ./sentry.server.ts ./app/utils/sentryTraceContext.server.ts --outdir=build --sourcemap", "dev": "cross-env PORT=3030 remix dev -c \"node ./build/server.js\"", "dev:worker": "cross-env NODE_PATH=../../node_modules/.pnpm/node_modules node ./build/server.js", - "format": "prettier --write .", - "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", + "format": "oxfmt .", + "lint": "oxlint -c ../../.oxlintrc.json", "start": "cross-env NODE_ENV=production node --max-old-space-size=8192 ./build/server.js", "start:local": "cross-env node --max-old-space-size=8192 ./build/server.js", "typecheck": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit -p ./tsconfig.check.json", @@ -21,11 +21,6 @@ "test": "vitest --no-file-parallelism", "eval:dev": "evalite watch" }, - "eslintIgnore": [ - "/node_modules", - "/build", - "/public/build" - ], "dependencies": { "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/openai": "^3.0.0", @@ -248,7 +243,6 @@ "@internal/replication": "workspace:*", "@internal/testcontainers": "workspace:*", "@remix-run/dev": "2.17.4", - "@remix-run/eslint-config": "2.17.4", "@remix-run/testing": "^2.17.4", "@sentry/cli": "2.50.2", "@swc/core": "^1.3.4", @@ -259,7 +253,6 @@ "@types/bcryptjs": "^2.4.2", "@types/compression": "^1.7.2", "@types/cookie": "^0.6.0", - "@types/eslint": "^8.4.6", "@types/express": "^4.17.13", "@types/humanize-duration": "^3.27.1", "@types/json-query": "^2.2.3", @@ -280,19 +273,12 @@ "@types/supertest": "^6.0.2", "@types/tar": "^6.1.4", "@types/ws": "^8.5.3", - "@typescript-eslint/eslint-plugin": "^5.59.6", - "@typescript-eslint/parser": "^5.59.6", "autoevals": "^0.0.130", "autoprefixer": "^10.4.13", "css-loader": "^6.10.0", "datepicker": "link:@types/@react-aria/datepicker", "engine.io": "^6.5.4", "esbuild": "^0.15.10", - "eslint": "^8.24.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-turbo": "^2.0.4", "evalite": "1.0.0-beta.16", "npm-run-all": "^4.1.5", "postcss-import": "^16.0.1", diff --git a/apps/webapp/prettier.config.js b/apps/webapp/prettier.config.js deleted file mode 100644 index f652d8bf750..00000000000 --- a/apps/webapp/prettier.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require("../../prettier.config.js"), - plugins: [require("prettier-plugin-tailwindcss")], -}; diff --git a/package.json b/package.json index 9c4b38c0aac..754d217dd8c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "dev": "turbo run dev", "i:dev": "infisical run -- turbo run dev", "generate": "turbo run generate", - "lint": "turbo run lint", + "format": "oxfmt .", + "lint": "oxlint", + "lint:fix": "oxlint --fix", "docker": "node scripts/docker.mjs -f docker/docker-compose.yml up -d --build --remove-orphans", "docker:stop": "node scripts/docker.mjs -f docker/docker-compose.yml stop", "docker:full": "node scripts/docker.mjs -f docker/docker-compose.yml -f docker/docker-compose.extras.yml up -d --build --remove-orphans", @@ -57,8 +59,9 @@ "@types/node": "20.14.14", "@vitest/coverage-v8": "4.1.7", "autoprefixer": "^10.4.12", - "eslint-plugin-turbo": "^2.0.4", "lefthook": "^1.11.3", + "oxfmt": "^0.54.0", + "oxlint": "^1.69.0", "pkg-pr-new": "0.0.75", "pkg-types": "1.1.3", "prettier": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 156a5ee7ac9..e006a17ba57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,12 +118,15 @@ importers: autoprefixer: specifier: ^10.4.12 version: 10.4.13(postcss@8.5.10) - eslint-plugin-turbo: - specifier: ^2.0.4 - version: 2.0.5(eslint@8.31.0) lefthook: specifier: ^1.11.3 version: 1.11.3 + oxfmt: + specifier: ^0.54.0 + version: 0.54.0 + oxlint: + specifier: ^1.69.0 + version: 1.70.0 pkg-pr-new: specifier: 0.0.75 version: 0.0.75 @@ -921,9 +924,6 @@ importers: '@remix-run/dev': specifier: 2.17.4 version: 2.17.4(@remix-run/react@2.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4))(@remix-run/serve@2.17.4(typescript@5.5.4))(@types/node@20.14.14)(bufferutil@4.0.9)(jiti@2.6.1)(lightningcss@1.29.2)(terser@5.46.1)(tsx@4.20.6)(typescript@5.5.4)(vite@6.4.2(@types/node@20.14.14)(jiti@2.6.1)(lightningcss@1.29.2)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0))(yaml@2.9.0) - '@remix-run/eslint-config': - specifier: 2.17.4 - version: 2.17.4(eslint@8.31.0)(react@18.3.1)(typescript@5.5.4) '@remix-run/testing': specifier: ^2.17.4 version: 2.17.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.4) @@ -954,9 +954,6 @@ importers: '@types/cookie': specifier: ^0.6.0 version: 0.6.0 - '@types/eslint': - specifier: ^8.4.6 - version: 8.4.10 '@types/express': specifier: ^4.17.13 version: 4.17.15 @@ -1017,12 +1014,6 @@ importers: '@types/ws': specifier: ^8.5.3 version: 8.5.4 - '@typescript-eslint/eslint-plugin': - specifier: ^5.59.6 - version: 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/parser': - specifier: ^5.59.6 - version: 5.59.6(eslint@8.31.0)(typescript@5.5.4) autoevals: specifier: ^0.0.130 version: 0.0.130(encoding@0.1.13)(ws@8.12.0(bufferutil@4.0.9)) @@ -1041,21 +1032,6 @@ importers: esbuild: specifier: ^0.15.10 version: 0.15.18 - eslint: - specifier: ^8.24.0 - version: 8.31.0 - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.6.0(eslint@8.31.0) - eslint-plugin-import: - specifier: ^2.29.1 - version: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-react-hooks: - specifier: ^4.6.2 - version: 4.6.2(eslint@8.31.0) - eslint-plugin-turbo: - specifier: ^2.0.4 - version: 2.0.5(eslint@8.31.0) evalite: specifier: 1.0.0-beta.16 version: 1.0.0-beta.16(ai@6.0.116(zod@3.25.76))(better-sqlite3@11.10.0)(bufferutil@4.0.9) @@ -3095,13 +3071,6 @@ packages: resolution: {integrity: sha512-2EENLmhpwplDux5PSsZnSbnSkB3tZ6QTksgO25xwEL7pIDcNOMhF5v/s6RzwjMZzZzw9Ofc30gHv5ChCC8pifQ==} engines: {node: '>=6.9.0'} - '@babel/eslint-parser@7.21.8': - resolution: {integrity: sha512-HLhI+2q+BP3sf78mFUZNCGc10KEmoUqtUT1OCdMZsN+qr4qFeLUod62/zAnF3jNQstwyasDkZnVXwfK2Bml7MQ==} - engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} - peerDependencies: - '@babel/core': '>=7.11.0' - eslint: ^7.5.0 || ^8.0.0 - '@babel/generator@7.24.7': resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} @@ -3256,42 +3225,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-display-name@7.18.6': - resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-development@7.18.6': - resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.22.15': - resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-pure-annotations@7.18.6': - resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.21.3': resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-react@7.18.6': - resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.21.5': resolution: {integrity: sha512-iqe3sETat5EOrORXiQ6rWfoOg2y68Cs75B9wNxdPW4kixJxh7aXQE1KPdWLDniC24T/6dSnguF33W9j/ZZQcmA==} engines: {node: '>=6.9.0'} @@ -4944,20 +4883,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.5.1': - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@1.4.1': - resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -5131,19 +5056,6 @@ packages: peerDependencies: '@hono/node-server': ^1.11.1 - '@humanwhocodes/config-array@0.11.8': - resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - deprecated: Use @eslint/object-schema instead - '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -5323,9 +5235,6 @@ packages: '@cfworker/json-schema': optional: true - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} - '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} @@ -5890,6 +5799,250 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 + '@oxfmt/binding-android-arm-eabi@0.54.0': + resolution: {integrity: sha512-NAtpl/SiaeU103e7/OmZw0MvUnsUUopW7hEm/ecegJg7YM0skQaA0IXEZoyTV6NUdiNPupdIUreRqUZTShbn/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.54.0': + resolution: {integrity: sha512-B4VZfBUlKK1rmMChsssNZbkZjE8+FzG3avMjGgMDwbGxXRoXkoeXiAZ+78Oa+eyDPHvDCiUb4zH/vmCOUSafLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.54.0': + resolution: {integrity: sha512-i02vF75b+ePsQP3tHqSxVYI5S6b8X/xqdPu7/mDHXtpgXLTYXi3jJmfHU0j+dnZZDKaYTx/ioCK7QYJmtiJR2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.54.0': + resolution: {integrity: sha512-8VMFvGvooXj7mswkbrhdVZ2/sgiDaBzWpkkbtO+qGDLV4EfJd67nQadHkQC0ZNbaWA9ajXfqI6i7PZLIeDzxEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.54.0': + resolution: {integrity: sha512-0cRHnp43WN1Jrc5s0BdbdKgR1XirdvHy7TAFi3JEsoEVQVJxTXMbpVd76sxXlgRswNMDhVFSJw+y7Eb8mEavFQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + resolution: {integrity: sha512-JyQAk3hK/OEtup7Rw6kZwfdzbKqTVD5jXXb8Xpfay29suwZyfBDMVW/bj4RqEPySYWc6zCp198pOluf8n5uYzg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + resolution: {integrity: sha512-qnvLatTpM8vtvjOfcckBOzJjk+n6ce/wwpP8OFeUrD5aNLYcKyWAitwj+Rk3PK9jGanbZvKsJnv14JGQ6XqFdw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + resolution: {integrity: sha512-SMkhnCzIYZYDk9vw3W/80eeYKmrMpGF0Giuxt4HruFlCH7jEtnPeb3SdQKMfgYi/dgtaf+hZAb5XWPYnxqCQ3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.54.0': + resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.54.0': + resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + resolution: {integrity: sha512-J0SSB8Z1Fre2sxRolYcW6Rl1RQmKdQ2hnHyq4YJrfBRiXTObLw4DXnIVraM/UyqGqwOi7yTrQA4VT7DPxlHVKA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + resolution: {integrity: sha512-O61UDVj8zz6yXJjkHPf05VaMLOXmEF8P5kf/N0W7AQMmd6bcQogl+KJc7rMutKTL524oE9iH32JXZClBFmEQIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + resolution: {integrity: sha512-1MDpqJPiFqxWtIHas8vkb1VZ7f7eKyTffAwmO8isxQYMaG1OFKsH666BWLeXQLO+IWNfiMssLD55hbR1lIPTqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.70.0': + resolution: {integrity: sha512-zFh0P4cswmRvw6nkyb89dr18rRanuaCPAsEXsFDoQY8WdaquI8Pt4NWFjaMJg6L23cy5NeN8J9cBnREbWzZhaw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.70.0': + resolution: {integrity: sha512-qI8o4HZjeGiBrWv+pJv4lH0Yi2Gl/JSp/EumBUApezJprIKa5PS4nU0lQsQngtky8k+SplQIOjv6hwu0SSxeyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.70.0': + resolution: {integrity: sha512-8KjgVVHI5F9nVwHCRwwA78Ty7zNKP4Wd9OeN5PSv3iu/F/u1RVXoOCgLhWqust6HmwQG6xc8c+RCyaWENy24+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.70.0': + resolution: {integrity: sha512-WVydssv5PSUBXFJTdNBWlmGkbNmvPGaFt/2SUT/EZRB6bq6bEOHmMlbnupZD5jmlEvi9+mZJHi8TCw15lyfSfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.70.0': + resolution: {integrity: sha512-hJucmUf8OlinHNb1R7fI4Fw6WsAstOz7i8nmkWQfiHoZXtbufNm+MxiDTIMk1ggh2Ro4vLzgQ+bKvRY54MZoRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + resolution: {integrity: sha512-1BnS7wbCYDSXwWzJJ+mc3NURoha6m6m6RT5c6vgAY3oz7C3OVXP+S0awo2mRq97arrJkVvO3qRQfyAHL+76xtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.70.0': + resolution: {integrity: sha512-yKy/UdbR55+M2yEcuiV5DCNC/gdQAjr/GioUy50QwBzSrKm8ueWADqyRLS9Xk+qjNeCYGg6A8FvUBds56ttfqg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.70.0': + resolution: {integrity: sha512-0A5XJ4alvmqFUFP/4oYSyaO+qLto/HrKEWTSaegiVl+HOufFngK2BjYw9x4RbwBt/du5QG6l5q1zeWiJYYG5yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.70.0': + resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.70.0': + resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.70.0': + resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.70.0': + resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.70.0': + resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.70.0': + resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.70.0': + resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.70.0': + resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.70.0': + resolution: {integrity: sha512-icAEsUI7JbW1TMRdEXV83mVAInhRVQYuuAlPpxdGwJ95chNdnCzjloRW8GglT0WvzOEZSio6fnYSk2DJ2Hv7LQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.70.0': + resolution: {integrity: sha512-FHMSWbVsPVs/f+Jcl04ws4JJ2wUnauyTzlpxWRG/lSO/8GpX08Fo2gQZqdA6CrRFI+zvkxl+N/KwJGWfUwYVZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.70.0': + resolution: {integrity: sha512-ptOlKwCz7n4AKs5VweMqG6DAg677FmKOK+vBkkL9DMNgFATIQ+upqUYBTOEwRQyRAx1ncGlPlXleV2hIcm3z4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -5897,10 +6050,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/utils@2.3.1': - resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.37.0': resolution: {integrity: sha512-181WBLk4SRUyH1Q96VZl7BP6HcK0b7lbdeKisn3N/vnjitk+9HbdlFz/L5fey05vxaAhldIDnzo8KUoy8S3mmQ==} engines: {node: '>=16'} @@ -7281,17 +7430,6 @@ packages: wrangler: optional: true - '@remix-run/eslint-config@2.17.4': - resolution: {integrity: sha512-Hhslms8Kl0fXHDS5UJWwyJ/1YQzJhRLjNZF+IfRLmCHI/zCvJP4dfy9yiT5BHnHb/m6MUz3l1L8EpPItg0dD5Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - eslint: ^8.0.0 - react: 18.3.1 - typescript: 5.5.4 - peerDependenciesMeta: - typescript: - optional: true - '@remix-run/express@2.17.4': resolution: {integrity: sha512-4zZs0L7v2pvAq896zHRLNMhoOKIPXM9qnYdHLbz4mpZUMbNAgQacGazArIrUV3M4g0gRMY0dLrt5CqMNrlBeYg==} engines: {node: '>=18.0.0'} @@ -7515,9 +7653,6 @@ packages: cpu: [x64] os: [win32] - '@rushstack/eslint-patch@1.2.0': - resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} - '@s2-dev/streamstore@0.22.10': resolution: {integrity: sha512-dtm+oFHVE8szINwOUoNQdx9xpGSJOrcAEvsxspPFvomjYKGnmhIRmU4OX8o6kxcPoiK76S1tPeU0smjZdmOngA==} @@ -8504,10 +8639,6 @@ packages: '@testcontainers/redis@11.14.0': resolution: {integrity: sha512-WX005slz2JMQPw2avbSjf5awVjpmFhOs5xCxeGSYLcV5ia4W1edv/P6MdOw4dZnvDQDuN5LfqNoV/ut3XGb2pA==} - '@testing-library/dom@8.19.1': - resolution: {integrity: sha512-P6iIPyYQ+qH8CvGauAqanhVnjrnRe0IZFSYCeGkSRW9q3u8bdVn2NPI+lasFyVsEQn1J/IFmp5Aax41+dAP9wg==} - engines: {node: '>=12'} - '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -8526,9 +8657,6 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} - '@types/aria-query@5.0.1': - resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} - '@types/aws-lambda@8.10.152': resolution: {integrity: sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==} @@ -8686,18 +8814,12 @@ packages: '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - '@types/eslint@8.4.10': - resolution: {integrity: sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw==} - '@types/eslint@8.56.12': resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} '@types/estree-jsx@1.0.0': resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==} - '@types/estree@1.0.0': - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -8746,12 +8868,6 @@ packages: '@types/json-query@2.2.3': resolution: {integrity: sha512-ygE4p8lyKzTBo9LF2K/u6MHnxPxbHY6wGvwM7TdAKhbP3SvEf+Y9aeVWedDiP8SMIPowTl9R/6awQYjiUTHz2g==} - '@types/json-schema@7.0.11': - resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} - - '@types/json-schema@7.0.13': - resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -8975,64 +9091,6 @@ packages: '@types/ws@8.5.4': resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} - '@typescript-eslint/eslint-plugin@5.59.6': - resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@5.59.6': - resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@5.59.6': - resolution: {integrity: sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/type-utils@5.59.6': - resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@5.59.6': - resolution: {integrity: sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@typescript-eslint/typescript-estree@5.59.6': - resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@5.59.6': - resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - - '@typescript-eslint/visitor-keys@5.59.6': - resolution: {integrity: sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@uiw/codemirror-extensions-basic-setup@4.19.5': resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==} peerDependencies: @@ -9391,10 +9449,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -9438,9 +9492,6 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - arktype@2.1.20: resolution: {integrity: sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q==} @@ -9451,33 +9502,14 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.8: - resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} - engines: {node: '>= 0.4'} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} - engines: {node: '>= 0.4'} - array.prototype.flat@1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} engines: {node: '>= 0.4'} - array.prototype.flat@1.3.2: - resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.2: - resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.1: - resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} - arraybuffer.prototype.slice@1.0.3: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} @@ -9507,9 +9539,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types-flow@0.0.7: - resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} - ast-v8-to-istanbul@1.0.2: resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} @@ -9567,16 +9596,9 @@ packages: aws4fetch@1.0.18: resolution: {integrity: sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ==} - axe-core@4.6.2: - resolution: {integrity: sha512-b1WlTV8+XKLj9gZy2DZXgQiyDp9xkkoe2a6U6UbYccScq2wgH/YwCeI2/Jq2mgo0HzQxqJOjWZBLeA/mqsk5Mg==} - engines: {node: '>=4'} - axios@1.16.1: resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} - axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} - b4a@1.6.6: resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} @@ -10520,9 +10542,6 @@ packages: dagre-d3-es@7.0.14: resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dashdash@1.14.1: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} @@ -10571,14 +10590,6 @@ packages: supports-color: optional: true - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -10606,15 +10617,6 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -10653,9 +10655,6 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deep-object-diff@1.1.9: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} @@ -10685,10 +10684,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -10800,17 +10795,6 @@ packages: resolution: {integrity: sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w==} engines: {node: '>= 8.0'} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-accessibility-api@0.5.15: - resolution: {integrity: sha512-8o+oVqLQZoruQPYy3uAAQtc6YbtSiRq5aPJBhJ82YTJRHvI6ofhYAkC81WmjFTnfUbqg6T3aCglIpU9p/5e7Cw==} - dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -10834,10 +10818,6 @@ packages: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} - dotenv@16.0.3: - resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} - engines: {node: '>=12'} - dotenv@16.4.4: resolution: {integrity: sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==} engines: {node: '>=12'} @@ -11318,181 +11298,19 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-config-prettier@8.6.0: - resolution: {integrity: sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} - - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.5.5: - resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - - eslint-module-utils@2.8.1: - resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-es@3.0.1: - resolution: {integrity: sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=4.19.1' - - eslint-plugin-import@2.29.1: - resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-jest-dom@4.0.3: - resolution: {integrity: sha512-9j+n8uj0+V0tmsoS7bYC7fLhQmIvjRqRYEcbDSi+TKPsTThLLXCyj5swMSSf/hTleeMktACnn+HFqXBr5gbcbA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6', yarn: '>=1'} - peerDependencies: - eslint: ^6.8.0 || ^7.0.0 || ^8.0.0 - - eslint-plugin-jest@26.9.0: - resolution: {integrity: sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - - eslint-plugin-jsx-a11y@6.7.1: - resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - eslint-plugin-node@11.1.0: - resolution: {integrity: sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==} - engines: {node: '>=8.10.0'} - peerDependencies: - eslint: '>=5.16.0' - - eslint-plugin-react-hooks@4.6.2: - resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - - eslint-plugin-react@7.32.2: - resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - - eslint-plugin-testing-library@5.11.0: - resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 - - eslint-plugin-turbo@2.0.5: - resolution: {integrity: sha512-nCTXZdaKmdRybBdjnMrDFG+ppLc9toUqB01Hf0pfhkQw8OoC29oJIVPsCSvuL/W58RKD02CNEUrwnVt57t36IQ==} - peerDependencies: - eslint: '>6.6.0' - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.2: - resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.31.0: - resolution: {integrity: sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.4.1: - resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - espree@9.6.0: - resolution: {integrity: sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -11533,10 +11351,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -11701,9 +11515,6 @@ packages: fast-json-stringify@6.0.1: resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-npm-meta@0.2.2: resolution: {integrity: sha512-E+fdxeaOQGo/CMWc9f4uHFfgUPJRAu7N3uB8GBvB3SDPAIWJK4GKyYhkAGFq+GYrcbKNfQIz5VVQyJnDuPPCrg==} @@ -11797,10 +11608,6 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-type@19.6.0: resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} engines: {node: '>=18'} @@ -11839,13 +11646,6 @@ packages: find-yarn-workspace-root2@1.2.16: resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -12090,10 +11890,6 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} - globals@13.19.0: - resolution: {integrity: sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==} - engines: {node: '>=8'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -12102,9 +11898,6 @@ packages: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -12535,11 +12328,6 @@ packages: is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12600,10 +12388,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} @@ -12683,10 +12467,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -12785,9 +12565,6 @@ packages: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} - js-sdsl@4.2.0: - resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} - js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -12850,9 +12627,6 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stable-stringify@1.3.0: resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} engines: {node: '>= 0.4'} @@ -12902,10 +12676,6 @@ packages: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} - jsx-ast-utils@3.3.3: - resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} - engines: {node: '>=4.0'} - junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -12948,12 +12718,6 @@ packages: resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} engines: {node: '>=20.10.0', npm: '>=10.2.3'} - language-subtag-registry@0.3.22: - resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} - - language-tags@1.0.5: - resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} - layerr@2.0.1: resolution: {integrity: sha512-z0730CwG/JO24evdORnyDkwG1Q7b7mF2Tp1qRQ0YvrMMARbt1DFG694SOv439Gm7hYKolyZyaB49YIrYIfZBdg==} @@ -13024,10 +12788,6 @@ packages: resolution: {integrity: sha512-HJp37y62j3j8qzAOODWuUJl4ysLwsDvCTBV6odr3jIRHR/a5e+tI14VQGIBcpK9ysqC3pGWyW5Rp9Jv1YDubyw==} hasBin: true - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} @@ -13270,10 +13030,6 @@ packages: resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} engines: {node: '>=12'} - lz-string@1.4.4: - resolution: {integrity: sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -13911,12 +13667,6 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - nearley@2.20.1: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true @@ -14122,25 +13872,6 @@ packages: resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} engines: {node: '>= 0.4'} - object.entries@1.1.6: - resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.hasown@1.1.2: - resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} - - object.values@1.2.0: - resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} - engines: {node: '>= 0.4'} - obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} @@ -14202,10 +13933,6 @@ packages: resolution: {integrity: sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==} engines: {node: '>=18'} - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - openai@4.33.1: resolution: {integrity: sha512-0DH572aSxGTT1JPOXgJQ9mjiuSPg/7scPot8hLc5I1mfQxPxLXTZWJpWerKaIWOuPkR2nrB0SamGDEehH8RuWA==} hasBin: true @@ -14228,10 +13955,6 @@ packages: openid-client@6.3.3: resolution: {integrity: sha512-lTK8AV8SjqCM4qznLX0asVESAwzV39XTVdfMAM185ekuaZCnkWdPzcxMTXNlsm9tsUAMa1Q30MBmKAykdT1LWw==} - optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -14246,6 +13969,32 @@ packages: outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + oxfmt@0.54.0: + resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.70.0: + resolution: {integrity: sha512-D6JgHtzkhRwvEC+A0Nw5AEc5bk8x5i1pHzvZIEf/a0C4hOzmAACNGtkDGPyFaxxX3ZVGxCPeig3P3rMM8XU3/g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.22.1' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-cancelable@1.1.0: resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} engines: {node: '>=6'} @@ -14858,10 +14607,6 @@ packages: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} engines: {node: '>=10'} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prepend-http@2.0.0: resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} engines: {node: '>=4'} @@ -14933,10 +14678,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -15198,9 +14939,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -15422,10 +15160,6 @@ packages: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - registry-auth-token@4.2.2: resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} engines: {node: '>=6.0.0'} @@ -15580,10 +15314,6 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - requireindex@1.2.0: - resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==} - engines: {node: '>=0.10.5'} - resend@3.2.0: resolution: {integrity: sha512-lDHhexiFYPoLXy7zRlJ8D5eKxoXy6Tr9/elN3+Vv7PkUoYuSSD1fpiIfa/JYXEWyiyN2UczkCTLpkT8dDPJ4Pg==} engines: {node: '>=18'} @@ -15614,10 +15344,6 @@ packages: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true - resolve@2.0.0-next.4: - resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} - hasBin: true - responselike@1.0.2: resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} @@ -15651,11 +15377,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -16121,9 +15842,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string.prototype.matchall@4.0.8: - resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} - string.prototype.padend@3.1.4: resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} engines: {node: '>= 0.4'} @@ -16186,10 +15904,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -16291,10 +16005,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} - systeminformation@5.31.7: resolution: {integrity: sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==} engines: {node: '>=8.0.0'} @@ -16345,10 +16055,6 @@ packages: tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -16417,9 +16123,6 @@ packages: text-decoder@1.2.0: resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -16445,9 +16148,6 @@ packages: tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - tiny-invariant@1.3.1: resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} @@ -16489,6 +16189,10 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -16628,9 +16332,6 @@ packages: tsconfig-paths@3.14.1: resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -16674,12 +16375,6 @@ packages: typescript: optional: true - tsutils@3.21.0: - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: 5.5.4 - tsx@3.12.2: resolution: {integrity: sha512-ykAEkoBg30RXxeOMVeZwar+JH632dZn9EUJVyJwhfag62k6UO/dIyJEV58YuLF6e5BTdV/qmbQrpkWqjq9cUnQ==} hasBin: true @@ -16752,18 +16447,10 @@ packages: tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -17356,10 +17043,6 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -19671,14 +19354,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/eslint-parser@7.21.8(@babel/core@7.22.17)(eslint@8.31.0)': - dependencies: - '@babel/core': 7.22.17 - '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.31.0 - eslint-visitor-keys: 2.1.0 - semver: 6.3.1 - '@babel/generator@7.24.7': dependencies: '@babel/types': 7.27.3 @@ -19841,31 +19516,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/helper-simple-access': 7.22.5 - '@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.24.0 - - '@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.17) - - '@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.22.17) - '@babel/types': 7.27.3 - - '@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/plugin-transform-typescript@7.21.3(@babel/core@7.22.17)': dependencies: '@babel/core': 7.22.17 @@ -19874,16 +19524,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.0 '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.22.17) - '@babel/preset-react@7.18.6(@babel/core@7.22.17)': - dependencies: - '@babel/core': 7.22.17 - '@babel/helper-plugin-utils': 7.24.0 - '@babel/helper-validator-option': 7.22.15 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.22.17) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.17) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.22.17) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.22.17) - '@babel/preset-typescript@7.21.5(@babel/core@7.22.17)': dependencies: '@babel/core': 7.22.17 @@ -21041,27 +20681,6 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.31.0)': - dependencies: - eslint: 8.31.0 - eslint-visitor-keys: 3.4.2 - - '@eslint-community/regexpp@4.5.1': {} - - '@eslint/eslintrc@1.4.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3(supports-color@10.0.0) - espree: 9.6.0 - globals: 13.19.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': @@ -21277,18 +20896,6 @@ snapshots: - bufferutil - utf-8-validate - '@humanwhocodes/config-array@0.11.8': - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.3(supports-color@10.0.0) - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@1.2.1': {} - '@iconify/types@2.0.0': {} '@iconify/utils@3.0.2': @@ -21607,10 +21214,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': - dependencies: - eslint-scope: 5.1.1 - '@nodable/entities@2.1.0': {} '@nodelib/fs.scandir@2.1.5': @@ -22375,20 +21978,125 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@oxfmt/binding-android-arm-eabi@0.54.0': + optional: true + + '@oxfmt/binding-android-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.54.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.54.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.54.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.54.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.54.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.54.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.54.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.54.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.70.0': + optional: true + + '@oxlint/binding-android-arm64@1.70.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.70.0': + optional: true + + '@oxlint/binding-darwin-x64@1.70.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.70.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.70.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.70.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.70.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.70.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.70.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.70.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.70.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.70.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.70.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.70.0': + optional: true + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/utils@2.3.1': - dependencies: - cross-spawn: 7.0.6 - is-glob: 4.0.3 - open: 8.4.0 - picocolors: 1.1.1 - tiny-glob: 0.2.9 - tslib: 2.8.1 - '@playwright/test@1.37.0': dependencies: '@types/node': 20.14.14 @@ -24314,33 +24022,6 @@ snapshots: - utf-8-validate - yaml - '@remix-run/eslint-config@2.17.4(eslint@8.31.0)(react@18.3.1)(typescript@5.5.4)': - dependencies: - '@babel/core': 7.22.17 - '@babel/eslint-parser': 7.21.8(@babel/core@7.22.17)(eslint@8.31.0) - '@babel/preset-react': 7.18.6(@babel/core@7.22.17) - '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - eslint-plugin-jest-dom: 4.0.3(eslint@8.31.0) - eslint-plugin-jsx-a11y: 6.7.1(eslint@8.31.0) - eslint-plugin-node: 11.1.0(eslint@8.31.0) - eslint-plugin-react: 7.32.2(eslint@8.31.0) - eslint-plugin-react-hooks: 4.6.2(eslint@8.31.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.5.4) - react: 18.3.1 - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - jest - - supports-color - '@remix-run/express@2.17.4(express@4.20.0)(typescript@5.5.4)': dependencies: '@remix-run/node': 2.17.4(typescript@5.5.4) @@ -24523,8 +24204,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true - '@rushstack/eslint-patch@1.2.0': {} - '@s2-dev/streamstore@0.22.10(supports-color@10.0.0)': dependencies: '@protobuf-ts/runtime': 2.11.1 @@ -25894,17 +25573,6 @@ snapshots: - bare-buffer - supports-color - '@testing-library/dom@8.19.1': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/runtime': 7.28.4 - '@types/aria-query': 5.0.1 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.15 - lz-string: 1.4.4 - pretty-format: 27.5.1 - '@tokenizer/token@0.3.0': {} '@total-typescript/ts-reset@0.4.2': {} @@ -25922,8 +25590,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/aria-query@5.0.1': {} - '@types/aws-lambda@8.10.152': {} '@types/bcryptjs@2.4.2': {} @@ -26117,11 +25783,6 @@ snapshots: '@types/eslint': 8.56.12 '@types/estree': 1.0.9 - '@types/eslint@8.4.10': - dependencies: - '@types/estree': 1.0.0 - '@types/json-schema': 7.0.11 - '@types/eslint@8.56.12': dependencies: '@types/estree': 1.0.9 @@ -26131,8 +25792,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - '@types/estree@1.0.0': {} - '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -26184,10 +25843,6 @@ snapshots: '@types/json-query@2.2.3': {} - '@types/json-schema@7.0.11': {} - - '@types/json-schema@7.0.13': {} - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -26452,90 +26107,6 @@ snapshots: dependencies: '@types/node': 20.14.14 - '@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.3.4 - eslint: 8.31.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - semver: 7.6.3 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - debug: 4.4.0 - eslint: 8.31.0 - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@5.59.6': - dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 - - '@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - debug: 4.4.3(supports-color@10.0.0) - eslint: 8.31.0 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@5.59.6': {} - - '@typescript-eslint/typescript-estree@5.59.6(typescript@5.5.4)': - dependencies: - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/visitor-keys': 5.59.6 - debug: 4.4.3(supports-color@10.0.0) - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.7.3 - tsutils: 3.21.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.5.4)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.31.0) - '@types/json-schema': 7.0.13 - '@types/semver': 7.5.1 - '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - eslint: 8.31.0 - eslint-scope: 5.1.1 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@5.59.6': - dependencies: - '@typescript-eslint/types': 5.59.6 - eslint-visitor-keys: 3.4.2 - '@uiw/codemirror-extensions-basic-setup@4.19.5(@codemirror/autocomplete@6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.3.0))(@codemirror/commands@6.1.3)(@codemirror/language@6.3.2)(@codemirror/lint@6.4.2)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)': dependencies: '@codemirror/autocomplete': 6.4.0(@codemirror/language@6.3.2)(@codemirror/state@6.2.0)(@codemirror/view@6.7.2)(@lezer/common@1.3.0) @@ -26984,8 +26555,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} antlr4ts-cli@0.5.0-alpha.4: {} @@ -27037,10 +26606,6 @@ snapshots: dependencies: tslib: 2.8.1 - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - arktype@2.1.20: dependencies: '@ark/schema': 0.46.0 @@ -27053,26 +26618,8 @@ snapshots: array-flatten@1.1.1: {} - array-includes@3.1.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.0.7 - array-union@2.1.0: {} - array.prototype.findlastindex@1.2.5: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.0.2 - array.prototype.flat@1.3.1: dependencies: call-bind: 1.0.8 @@ -27080,28 +26627,6 @@ snapshots: es-abstract: 1.23.3 es-shim-unscopables: 1.0.2 - array.prototype.flat@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - - array.prototype.flatmap@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - - array.prototype.tosorted@1.1.1: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-shim-unscopables: 1.0.2 - get-intrinsic: 1.3.0 - arraybuffer.prototype.slice@1.0.3: dependencies: array-buffer-byte-length: 1.0.1 @@ -27129,8 +26654,6 @@ snapshots: assertion-error@2.0.1: {} - ast-types-flow@0.0.7: {} - ast-v8-to-istanbul@1.0.2: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -27204,8 +26727,6 @@ snapshots: aws4fetch@1.0.18: {} - axe-core@4.6.2: {} - axios@1.16.1: dependencies: follow-redirects: 1.16.0 @@ -27216,10 +26737,6 @@ snapshots: - debug - supports-color - axobject-query@3.2.1: - dependencies: - dequal: 2.0.3 - b4a@1.6.6: {} bail@2.0.2: {} @@ -28253,8 +27770,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} - dashdash@1.14.1: dependencies: assert-plus: 1.0.0 @@ -28297,10 +27812,6 @@ snapshots: dependencies: ms: 2.0.0 - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.3.4: dependencies: ms: 2.1.2 @@ -28317,10 +27828,6 @@ snapshots: optionalDependencies: supports-color: 10.0.0 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.3(supports-color@10.0.0): dependencies: ms: 2.1.3 @@ -28353,8 +27860,6 @@ snapshots: deep-extend@0.6.0: {} - deep-is@0.1.4: {} - deep-object-diff@1.1.9: {} deepmerge-ts@7.1.5: {} @@ -28380,8 +27885,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} define-properties@1.1.4: @@ -28499,16 +28002,6 @@ snapshots: transitivePeerDependencies: - supports-color - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-accessibility-api@0.5.15: {} - dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.28.4 @@ -28540,8 +28033,6 @@ snapshots: dependencies: type-fest: 5.6.0 - dotenv@16.0.3: {} - dotenv@16.4.4: {} dotenv@16.4.5: {} @@ -28717,7 +28208,7 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 + tapable: 2.3.3 enquirer@2.3.6: dependencies: @@ -29203,275 +28694,15 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-config-prettier@8.6.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - - eslint-import-resolver-node@0.3.7: - dependencies: - debug: 3.2.7 - is-core-module: 2.14.0 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.14.0 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0): - dependencies: - debug: 4.4.3(supports-color@10.0.0) - enhanced-resolve: 5.18.3 - eslint: 8.31.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - get-tsconfig: 4.7.6 - globby: 13.2.2 - is-core-module: 2.14.0 - is-glob: 4.0.3 - synckit: 0.8.5 - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) - transitivePeerDependencies: - - supports-color - - eslint-plugin-es@3.0.1(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-utils: 2.1.0 - regexpp: 3.2.0 - - eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0): - dependencies: - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.31.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - hasown: 2.0.2 - is-core-module: 2.14.0 - is-glob: 4.0.3 - minimatch: 3.1.5 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jest-dom@4.0.3(eslint@8.31.0): - dependencies: - '@babel/runtime': 7.28.4 - '@testing-library/dom': 8.19.1 - eslint: 8.31.0 - requireindex: 1.2.0 - - eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4): - dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - optionalDependencies: - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4))(eslint@8.31.0)(typescript@5.5.4) - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-jsx-a11y@6.7.1(eslint@8.31.0): - dependencies: - '@babel/runtime': 7.28.4 - aria-query: 5.3.0 - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.2 - ast-types-flow: 0.0.7 - axe-core: 4.6.2 - axobject-query: 3.2.1 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 8.31.0 - has: 1.0.3 - jsx-ast-utils: 3.3.3 - language-tags: 1.0.5 - minimatch: 3.1.5 - object.entries: 1.1.6 - object.fromentries: 2.0.8 - semver: 6.3.1 - - eslint-plugin-node@11.1.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-plugin-es: 3.0.1(eslint@8.31.0) - eslint-utils: 2.1.0 - ignore: 5.2.4 - minimatch: 3.1.5 - resolve: 1.22.8 - semver: 6.3.1 - - eslint-plugin-react-hooks@4.6.2(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - - eslint-plugin-react@7.32.2(eslint@8.31.0): - dependencies: - array-includes: 3.1.8 - array.prototype.flatmap: 1.3.2 - array.prototype.tosorted: 1.1.1 - doctrine: 2.1.0 - eslint: 8.31.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.3.3 - minimatch: 3.1.5 - object.entries: 1.1.6 - object.fromentries: 2.0.8 - object.hasown: 1.1.2 - object.values: 1.2.0 - prop-types: 15.8.1 - resolve: 2.0.0-next.4 - semver: 6.3.1 - string.prototype.matchall: 4.0.8 - - eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.5.4): - dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - eslint: 8.31.0 - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-turbo@2.0.5(eslint@8.31.0): - dependencies: - dotenv: 16.0.3 - eslint: 8.31.0 - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@7.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-utils@2.1.0: - dependencies: - eslint-visitor-keys: 1.3.0 - - eslint-utils@3.0.0(eslint@8.31.0): - dependencies: - eslint: 8.31.0 - eslint-visitor-keys: 2.1.0 - - eslint-visitor-keys@1.3.0: {} - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.3.0: {} - - eslint-visitor-keys@3.4.2: {} - - eslint@8.31.0: - dependencies: - '@eslint/eslintrc': 1.4.1 - '@humanwhocodes/config-array': 0.11.8 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.31.0) - eslint-visitor-keys: 3.3.0 - espree: 9.4.1 - esquery: 1.4.0 - esutils: 2.0.3 - 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 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-sdsl: 4.2.0 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.4.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.2 - - espree@9.6.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.2 - esprima@4.0.1: {} - esquery@1.4.0: - dependencies: - estraverse: 5.3.0 - esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -29515,8 +28746,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} - etag@1.8.1: {} eval@0.1.6: @@ -29788,8 +29017,6 @@ snapshots: json-schema-ref-resolver: 2.0.1 rfdc: 1.4.1 - fast-levenshtein@2.0.6: {} - fast-npm-meta@0.2.2: {} fast-querystring@1.1.2: @@ -29882,10 +29109,6 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.0.4 - file-type@19.6.0: dependencies: get-stream: 9.0.1 @@ -29950,13 +29173,6 @@ snapshots: micromatch: 4.0.8 pkg-dir: 4.2.0 - flat-cache@3.0.4: - dependencies: - flatted: 3.4.2 - rimraf: 3.0.2 - - flatted@3.4.2: {} - follow-redirects@1.16.0: {} for-each@0.3.3: @@ -30229,18 +29445,12 @@ snapshots: globals@11.12.0: {} - globals@13.19.0: - dependencies: - type-fest: 0.20.2 - globals@15.15.0: {} globalthis@1.0.3: dependencies: define-properties: 1.2.1 - globalyzer@0.1.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -30767,8 +29977,6 @@ snapshots: is-deflate@1.0.0: {} - is-docker@2.2.1: {} - is-docker@3.0.0: {} is-electron@2.2.2: {} @@ -30807,8 +30015,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@1.1.0: {} is-plain-obj@3.0.0: {} @@ -30870,10 +30076,6 @@ snapshots: is-windows@1.0.2: {} - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -30966,8 +30168,6 @@ snapshots: js-levenshtein@1.1.6: {} - js-sdsl@4.2.0: {} - js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -31011,8 +30211,6 @@ snapshots: json-schema@0.4.0: {} - json-stable-stringify-without-jsonify@1.0.1: {} - json-stable-stringify@1.3.0: dependencies: call-bind: 1.0.8 @@ -31073,11 +30271,6 @@ snapshots: json-schema: 0.4.0 verror: 1.10.0 - jsx-ast-utils@3.3.3: - dependencies: - array-includes: 3.1.8 - object.assign: 4.1.5 - junk@4.0.1: {} jwa@1.4.2: @@ -31120,12 +30313,6 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.1.0 - language-subtag-registry@0.3.22: {} - - language-tags@1.0.5: - dependencies: - language-subtag-registry: 0.3.22 - layerr@2.0.1: {} layout-base@1.0.2: {} @@ -31181,11 +30368,6 @@ snapshots: lefthook-windows-arm64: 1.11.3 lefthook-windows-x64: 1.11.3 - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - light-my-request@6.6.0: dependencies: cookie: 1.0.2 @@ -31379,8 +30561,6 @@ snapshots: luxon@3.2.1: {} - lz-string@1.4.4: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -32410,10 +31590,6 @@ snapshots: napi-build-utils@2.0.0: optional: true - natural-compare-lite@1.4.0: {} - - natural-compare@1.4.0: {} - nearley@2.20.1: dependencies: commander: 2.20.3 @@ -32612,36 +31788,6 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - object.entries@1.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.hasown@1.1.2: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.23.3 - - object.values@1.2.0: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - obuf@1.1.2: {} obug@2.1.1: {} @@ -32708,12 +31854,6 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - openai@4.33.1(encoding@0.1.13): dependencies: '@types/node': 20.14.14 @@ -32755,15 +31895,6 @@ snapshots: jose: 6.0.8 oauth4webapi: 3.3.0 - optionator@0.9.1: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 - ora@5.4.1: dependencies: bl: 4.1.0 @@ -32784,6 +31915,52 @@ snapshots: outdent@0.8.0: {} + oxfmt@0.54.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.54.0 + '@oxfmt/binding-android-arm64': 0.54.0 + '@oxfmt/binding-darwin-arm64': 0.54.0 + '@oxfmt/binding-darwin-x64': 0.54.0 + '@oxfmt/binding-freebsd-x64': 0.54.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.54.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.54.0 + '@oxfmt/binding-linux-arm64-gnu': 0.54.0 + '@oxfmt/binding-linux-arm64-musl': 0.54.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.54.0 + '@oxfmt/binding-linux-riscv64-musl': 0.54.0 + '@oxfmt/binding-linux-s390x-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-gnu': 0.54.0 + '@oxfmt/binding-linux-x64-musl': 0.54.0 + '@oxfmt/binding-openharmony-arm64': 0.54.0 + '@oxfmt/binding-win32-arm64-msvc': 0.54.0 + '@oxfmt/binding-win32-ia32-msvc': 0.54.0 + '@oxfmt/binding-win32-x64-msvc': 0.54.0 + + oxlint@1.70.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.70.0 + '@oxlint/binding-android-arm64': 1.70.0 + '@oxlint/binding-darwin-arm64': 1.70.0 + '@oxlint/binding-darwin-x64': 1.70.0 + '@oxlint/binding-freebsd-x64': 1.70.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.70.0 + '@oxlint/binding-linux-arm-musleabihf': 1.70.0 + '@oxlint/binding-linux-arm64-gnu': 1.70.0 + '@oxlint/binding-linux-arm64-musl': 1.70.0 + '@oxlint/binding-linux-ppc64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-gnu': 1.70.0 + '@oxlint/binding-linux-riscv64-musl': 1.70.0 + '@oxlint/binding-linux-s390x-gnu': 1.70.0 + '@oxlint/binding-linux-x64-gnu': 1.70.0 + '@oxlint/binding-linux-x64-musl': 1.70.0 + '@oxlint/binding-openharmony-arm64': 1.70.0 + '@oxlint/binding-win32-arm64-msvc': 1.70.0 + '@oxlint/binding-win32-ia32-msvc': 1.70.0 + '@oxlint/binding-win32-x64-msvc': 1.70.0 + p-cancelable@1.1.0: {} p-event@5.0.1: @@ -33367,8 +32544,6 @@ snapshots: path-exists: 4.0.0 which-pm: 2.0.0 - prelude-ls@1.2.1: {} - prepend-http@2.0.0: {} prettier-plugin-tailwindcss@0.3.0(prettier@2.8.8): @@ -33381,12 +32556,6 @@ snapshots: prettier@3.8.3: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - pretty-hrtime@1.0.3: {} pretty-ms@7.0.1: @@ -33731,8 +32900,6 @@ snapshots: react-is@16.13.1: {} - react-is@17.0.2: {} - react-is@18.3.1: {} react-markdown@10.1.0(@types/react@18.2.69)(react@18.3.1): @@ -34036,8 +33203,6 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 - regexpp@3.2.0: {} - registry-auth-token@4.2.2: dependencies: rc: 1.2.8 @@ -34245,8 +33410,6 @@ snapshots: require-main-filename@2.0.0: {} - requireindex@1.2.0: {} - resend@3.2.0: dependencies: '@react-email/render': 0.0.12 @@ -34272,12 +33435,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.4: - dependencies: - is-core-module: 2.14.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - responselike@1.0.2: dependencies: lowercase-keys: 1.0.1 @@ -34301,10 +33458,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rimraf@6.0.1: dependencies: glob: 11.1.0 @@ -34945,17 +34098,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string.prototype.matchall@4.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.23.3 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - internal-slot: 1.0.7 - regexp.prototype.flags: 1.5.2 - side-channel: 1.1.0 - string.prototype.padend@3.1.4: dependencies: call-bind: 1.0.7 @@ -35028,8 +34170,6 @@ snapshots: strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - strnum@1.0.5: {} strnum@2.2.3: {} @@ -35143,11 +34283,6 @@ snapshots: rimraf: 6.0.1 tshy: 3.0.2 - synckit@0.8.5: - dependencies: - '@pkgr/utils': 2.3.1 - tslib: 2.8.1 - systeminformation@5.31.7: {} table@6.9.0: @@ -35232,8 +34367,6 @@ snapshots: tailwindcss@4.3.0: {} - tapable@2.3.0: {} - tapable@2.3.3: {} tar-fs@2.1.3: @@ -35353,8 +34486,6 @@ snapshots: dependencies: b4a: 1.6.6 - text-table@0.2.0: {} - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -35378,11 +34509,6 @@ snapshots: tiny-case@1.0.3: {} - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - tiny-invariant@1.3.1: {} tinybench@2.9.0: {} @@ -35422,6 +34548,8 @@ snapshots: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 + tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} tldts-core@7.0.7: {} @@ -35529,13 +34657,6 @@ snapshots: minimist: 1.2.7 strip-bom: 3.0.0 - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.7 - strip-bom: 3.0.0 - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -35622,11 +34743,6 @@ snapshots: - tsx - yaml - tsutils@3.21.0(typescript@5.5.4): - dependencies: - tslib: 1.14.1 - typescript: 5.5.4 - tsx@3.12.2: dependencies: '@esbuild-kit/cjs-loader': 2.4.1 @@ -35707,14 +34823,8 @@ snapshots: tweetnacl@0.14.5: {} - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-fest@0.13.1: {} - type-fest@0.20.2: {} - type-fest@0.6.0: {} type-fest@0.8.1: {} @@ -36408,8 +35518,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.3: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 68de2fe5c9c..00000000000 --- a/prettier.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - semi: true, - singleQuote: false, - jsxSingleQuote: false, - trailingComma: "es5", - bracketSpacing: true, - bracketSameLine: false, - printWidth: 100, - tabWidth: 2, - useTabs: false, -}; diff --git a/turbo.json b/turbo.json index 8f2c862d030..c820d115376 100644 --- a/turbo.json +++ b/turbo.json @@ -2,32 +2,16 @@ "$schema": "https://turborepo.org/schema.json", "pipeline": { "build": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "dist/**", - "public/build/**", - "build/**", - "app/styles/tailwind.css", - ".cache" - ] + "dependsOn": ["^build"], + "outputs": ["dist/**", "public/build/**", "build/**", "app/styles/tailwind.css", ".cache"] }, "webapp#start": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "public/build/**" - ] + "dependsOn": ["^build"], + "outputs": ["public/build/**"] }, "start": { - "dependsOn": [ - "^build" - ], - "outputs": [ - "public/build/**" - ] + "dependsOn": ["^build"], + "outputs": ["public/build/**"] }, "db:generate": { "cache": false @@ -37,9 +21,7 @@ }, "webapp#db:seed": { "cache": false, - "dependsOn": [ - "webapp#build" - ] + "dependsOn": ["webapp#build"] }, "db:studio": { "cache": false @@ -49,29 +31,20 @@ }, "dev": { "cache": false, - "dependsOn": [ - "^build" - ] + "dependsOn": ["^build"] }, "i:dev": { "cache": false }, "generate": { - "dependsOn": [ - "^generate" - ] - }, - "lint": { - "outputs": [] + "dependsOn": ["^generate"] }, "docker:build": { "outputs": [], "cache": false }, "test": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "test:dev": { @@ -79,28 +52,20 @@ "cache": false }, "test:e2e:dev": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [], "cache": false }, "test:e2e:ci": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "typecheck": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "check-exports": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "outputs": [] }, "clean": { @@ -113,9 +78,7 @@ "cache": false } }, - "globalDependencies": [ - ".env" - ], + "globalDependencies": [".env"], "globalEnv": [ "NODE_ENV", "REMIX_APP_PORT", @@ -139,4 +102,4 @@ "APP_ENV", "APP_LOG_LEVEL" ] -} \ No newline at end of file +} From 08c3bdce030f991efd02f82e28252142509f3b00 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 14:33:43 +0100 Subject: [PATCH 02/10] ignore rules-of-hooks --- .oxlintrc.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 463fa955515..f028e901d34 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -19,6 +19,9 @@ } ], "import/no-duplicates": ["warn", { "prefer-inline": true }], - "react-hooks/rules-of-hooks": "error" + // Disabled for now: oxlint treats any use*()/use() call as a React hook, + // producing false positives in async/test code (testcontainers, cli-v3 e2e). + // Re-enable (ideally scoped to React packages) once those are addressed. + "react-hooks/rules-of-hooks": "off" } } From 13a99e92178a2e923eb594ed3d2040d8a96a4613 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 14:33:53 +0100 Subject: [PATCH 03/10] make actionlint happy --- .github/workflows/code-quality.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 23ad4f79505..4b44dea9541 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -60,7 +60,7 @@ jobs: FILES: ${{ steps.changes.outputs.files }} # Unquoted $FILES intentionally word-splits the newline-separated list # into separate path args. Safe given the has_files guard above. - run: pnpm exec oxfmt --check $FILES + run: "pnpm exec oxfmt --check $FILES" # TODO update this so lint failures fail CI # Informational only — lint findings are surfaced in the logs but never @@ -70,4 +70,4 @@ jobs: continue-on-error: true env: FILES: ${{ steps.changes.outputs.files }} - run: pnpm exec oxlint $FILES + run: "pnpm exec oxlint $FILES" From 526889bc3ae141923ea1823acc1f9749dc86159e Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 14:58:01 +0100 Subject: [PATCH 04/10] enforce checks in ci --- .github/workflows/code-quality.yml | 41 +++--------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 4b44dea9541..6fd2f73b4ab 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -31,43 +31,8 @@ jobs: - name: šŸ“„ Download deps run: pnpm install --frozen-lockfile - # Gate only on files this PR adds/modifies (ratchet) — the repo is not - # fully formatted/linted yet, so a whole-repo check would fail every PR. - - name: šŸ” Determine changed files - id: changes - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - FILES=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- \ - '*.ts' '*.tsx' '*.js' '*.jsx' '*.mjs' '*.cjs') - echo "Changed code files:" - echo "${FILES:-}" - { - echo "files<> "$GITHUB_OUTPUT" - if [ -z "$FILES" ]; then - echo "has_files=false" >> "$GITHUB_OUTPUT" - else - echo "has_files=true" >> "$GITHUB_OUTPUT" - fi - - # TODO enable format check for ALL code - name: šŸ’… Check formatting - if: steps.changes.outputs.has_files == 'true' - env: - FILES: ${{ steps.changes.outputs.files }} - # Unquoted $FILES intentionally word-splits the newline-separated list - # into separate path args. Safe given the has_files guard above. - run: "pnpm exec oxfmt --check $FILES" + run: pnpm exec oxfmt --check . - # TODO update this so lint failures fail CI - # Informational only — lint findings are surfaced in the logs but never - # fail the job. Only the formatting check above blocks the PR. - - name: šŸ”Ž Lint (informational, non-blocking) - if: steps.changes.outputs.has_files == 'true' - continue-on-error: true - env: - FILES: ${{ steps.changes.outputs.files }} - run: "pnpm exec oxlint $FILES" + - name: šŸ”Ž Lint + run: pnpm exec oxlint . From 3d983438954ef495ad4bf1fcff0b9e5ba3856a0f Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 15:11:31 +0100 Subject: [PATCH 05/10] remove prettier --- apps/webapp/package.json | 2 - package.json | 1 - pnpm-lock.yaml | 72 --------------------- scripts/upgrade-package.mjs | 123 ------------------------------------ 4 files changed, 198 deletions(-) delete mode 100644 scripts/upgrade-package.mjs diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 571f35620bf..922837b756b 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -283,8 +283,6 @@ "npm-run-all": "^4.1.5", "postcss-import": "^16.0.1", "postcss-loader": "^8.1.1", - "prettier": "^2.8.8", - "prettier-plugin-tailwindcss": "^0.3.0", "prop-types": "^15.8.1", "rimraf": "^6.0.1", "style-loader": "^3.3.4", diff --git a/package.json b/package.json index 754d217dd8c..f535f51bf69 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "oxlint": "^1.69.0", "pkg-pr-new": "0.0.75", "pkg-types": "1.1.3", - "prettier": "^3.0.0", "tsx": "^3.7.1", "turbo": "^1.10.3", "typescript": "5.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e006a17ba57..8a721a0a238 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,6 @@ importers: pkg-types: specifier: 1.1.3 version: 1.1.3 - prettier: - specifier: ^3.0.0 - version: 3.0.0 tsx: specifier: ^3.7.1 version: 3.12.2 @@ -1044,12 +1041,6 @@ importers: postcss-loader: specifier: ^8.1.1 version: 8.1.1(postcss@8.5.10)(typescript@5.5.4)(webpack@5.102.1(@swc/core@1.3.26)(esbuild@0.15.18)) - prettier: - specifier: ^2.8.8 - version: 2.8.8 - prettier-plugin-tailwindcss: - specifier: ^0.3.0 - version: 0.3.0(prettier@2.8.8) prop-types: specifier: ^15.8.1 version: 15.8.1 @@ -14611,68 +14602,11 @@ packages: resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} engines: {node: '>=4'} - prettier-plugin-tailwindcss@0.3.0: - resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==} - engines: {node: '>=12.17.0'} - peerDependencies: - '@ianvs/prettier-plugin-sort-imports': '*' - '@prettier/plugin-pug': '*' - '@shopify/prettier-plugin-liquid': '*' - '@shufo/prettier-plugin-blade': '*' - '@trivago/prettier-plugin-sort-imports': '*' - prettier: '>=2.2.0' - prettier-plugin-astro: '*' - prettier-plugin-css-order: '*' - prettier-plugin-import-sort: '*' - prettier-plugin-jsdoc: '*' - prettier-plugin-marko: '*' - prettier-plugin-organize-attributes: '*' - prettier-plugin-organize-imports: '*' - prettier-plugin-style-order: '*' - prettier-plugin-svelte: '*' - prettier-plugin-twig-melody: '*' - peerDependenciesMeta: - '@ianvs/prettier-plugin-sort-imports': - optional: true - '@prettier/plugin-pug': - optional: true - '@shopify/prettier-plugin-liquid': - optional: true - '@shufo/prettier-plugin-blade': - optional: true - '@trivago/prettier-plugin-sort-imports': - optional: true - prettier-plugin-astro: - optional: true - prettier-plugin-css-order: - optional: true - prettier-plugin-import-sort: - optional: true - prettier-plugin-jsdoc: - optional: true - prettier-plugin-marko: - optional: true - prettier-plugin-organize-attributes: - optional: true - prettier-plugin-organize-imports: - optional: true - prettier-plugin-style-order: - optional: true - prettier-plugin-svelte: - optional: true - prettier-plugin-twig-melody: - optional: true - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - prettier@3.0.0: - resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} - engines: {node: '>=14'} - hasBin: true - prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} @@ -32546,14 +32480,8 @@ snapshots: prepend-http@2.0.0: {} - prettier-plugin-tailwindcss@0.3.0(prettier@2.8.8): - dependencies: - prettier: 2.8.8 - prettier@2.8.8: {} - prettier@3.0.0: {} - prettier@3.8.3: {} pretty-hrtime@1.0.3: {} diff --git a/scripts/upgrade-package.mjs b/scripts/upgrade-package.mjs deleted file mode 100644 index c1245cf9f7c..00000000000 --- a/scripts/upgrade-package.mjs +++ /dev/null @@ -1,123 +0,0 @@ -import { promises as fs } from "fs"; -import { join } from "path"; -import { exec } from "child_process"; -import prettier from "prettier"; - -import prettierConfig from "../prettier.config.js"; - -async function runShellCommand(command, directory) { - return new Promise((resolve, reject) => { - exec(command, { cwd: directory }, (error, stdout, stderr) => { - if (error) { - console.error(`Error: ${error.message}`); - reject(error); - return; - } - if (stderr) { - console.error(`Stderr: ${stderr}`); - reject(stderr); - return; - } - console.log(`Stdout: ${stdout}`); - resolve(stdout); - }); - }); -} - -async function updatePackage(directory) { - // Updating package.json - const packageJsonPath = join(directory, "package.json"); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); - - // Updating dependencies - packageJson.devDependencies = packageJson.devDependencies || {}; - packageJson.devDependencies["@trigger.dev/tsup"] = "workspace:*"; - packageJson.devDependencies["tsup"] = "8.0.1"; - packageJson.devDependencies["typescript"] = "^5.3.0"; - - // Updating exports, main, types, and module - packageJson.exports = { - ".": { - import: { - types: "./dist/index.d.mts", - default: "./dist/index.mjs", - }, - require: "./dist/index.js", - types: "./dist/index.d.ts", - }, - "./package.json": "./package.json", - }; - packageJson.main = "./dist/index.js"; - packageJson.types = "./dist/index.d.ts"; - packageJson.module = "./dist/index.mjs"; - packageJson.files = ["dist"]; - - await fs.writeFile( - packageJsonPath, - await prettier.format(JSON.stringify(packageJson, null, 2), { - parser: "json", - ...prettierConfig, - }) - ); - - console.log(`āœ… Updated package.json for ${packageJson.name}`); - - // Updating tsconfig.json - const tsconfigPath = join(directory, "tsconfig.json"); - const tsconfig = JSON.parse(await fs.readFile(tsconfigPath, "utf8")); - - if (tsconfig.extends === "@trigger.dev/tsconfig/integration.json") { - console.log( - `āœ… tsconfig.json for ${packageJson.name} already extends @trigger.dev/tsconfig/integration.json` - ); - } else { - tsconfig.compilerOptions = tsconfig.compilerOptions || {}; - tsconfig.compilerOptions.paths = { - ...tsconfig.compilerOptions.paths, - "@trigger.dev/tsup/*": ["../../config-packages/tsup/src/*"], - "@trigger.dev/tsup": ["../../config-packages/tsup/src/index"], - }; - - await fs.writeFile( - tsconfigPath, - await prettier.format(JSON.stringify(tsconfig, null, 2), { - parser: "json", - ...prettierConfig, - }) - ); - - console.log(`āœ… Updated tsconfig.json for ${packageJson.name}`); - } - - // Updating tsup.config.ts - const tsupConfigPath = join(directory, "tsup.config.ts"); - const tsupConfigContent = `import { defineConfigPackage } from "@trigger.dev/tsup";\n\nexport default defineConfigPackage;`; - await fs.writeFile( - tsupConfigPath, - await prettier.format(tsupConfigContent, { parser: "typescript", ...prettierConfig }) - ); - - console.log(`āœ… Updated tsup.config.ts for ${packageJson.name}`); - - console.log(`āœ… Updated package ${packageJson.name}, now running pnpm install and build`); - - await runShellCommand("pnpm install", process.cwd()); - await runShellCommand(`pnpm run build --filter ${packageJson.name}`, process.cwd()); -} - -async function main() { - const packagePath = process.argv[2]; - - if (!packagePath) { - throw new Error("Missing package path"); - } - - console.log(`Updating package ${packagePath}`); - - await updatePackage(packagePath); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); From ff169bb79305585aed8ec5a604c89e150d914a3b Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 16:48:42 +0100 Subject: [PATCH 06/10] remove lefthook --- package.json | 2 - pnpm-lock.yaml | 100 ------------------------------------------------- 2 files changed, 102 deletions(-) diff --git a/package.json b/package.json index f535f51bf69..6bd4e3a5569 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "@types/node": "20.14.14", "@vitest/coverage-v8": "4.1.7", "autoprefixer": "^10.4.12", - "lefthook": "^1.11.3", "oxfmt": "^0.54.0", "oxlint": "^1.69.0", "pkg-pr-new": "0.0.75", @@ -145,7 +144,6 @@ "better-sqlite3", "cpu-features", "esbuild", - "lefthook", "prisma", "protobufjs", "sharp", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a721a0a238..7c3d6c10478 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,9 +118,6 @@ importers: autoprefixer: specifier: ^10.4.12 version: 10.4.13(postcss@8.5.10) - lefthook: - specifier: ^1.11.3 - version: 1.11.3 oxfmt: specifier: ^0.54.0 version: 0.54.0 @@ -12725,60 +12722,6 @@ packages: leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - lefthook-darwin-arm64@1.11.3: - resolution: {integrity: sha512-IYzAOf8Qwqk7q+LoRyy7kSk9vzpUZ5wb/vLzEAH/F86Vay9AUaWe1f2pzeLwFg18qEc1QNklT69h9p/uLQMojA==} - cpu: [arm64] - os: [darwin] - - lefthook-darwin-x64@1.11.3: - resolution: {integrity: sha512-z/Wp7UMjE1Vyl+x9sjN3NvN6qKdwgHl+cDf98MKKDg/WyPE5XnzqLm9rLLJgImjyClfH7ptTfZxEyhTG3M3XvQ==} - cpu: [x64] - os: [darwin] - - lefthook-freebsd-arm64@1.11.3: - resolution: {integrity: sha512-QevwQ7lrv5wBCkk7LLTzT5KR3Bk/5nttSxT1UH2o0EsgirS/c2K5xSgQmV6m3CiZYuCe2Pja4BSIwN3zt17SMw==} - cpu: [arm64] - os: [freebsd] - - lefthook-freebsd-x64@1.11.3: - resolution: {integrity: sha512-PYbcyNgdJJ4J2pEO9Ss4oYo5yq4vmQGTKm3RTYbRx4viSWR65hvKCP0C4LnIqspMvmR05SJi2bqe7UBP2t60EA==} - cpu: [x64] - os: [freebsd] - - lefthook-linux-arm64@1.11.3: - resolution: {integrity: sha512-0pBMBAoafOAEg345eOPozsmRjWR0zCr6k+m5ZxwRBZbZx1bQFDqBakQ3TpFCphhcykmgFyaa1KeZJZUOrEsezA==} - cpu: [arm64] - os: [linux] - - lefthook-linux-x64@1.11.3: - resolution: {integrity: sha512-eiezheZ/bisBCMB2Ur0mctug/RDFyu39B5wzoE8y4z0W1yw6jHGrWMJ4Y8+5qKZ7fmdZg+7YPuMHZ2eFxOnhQA==} - cpu: [x64] - os: [linux] - - lefthook-openbsd-arm64@1.11.3: - resolution: {integrity: sha512-DRLTzXdtCj/TizpLcGSqXcnrqvgxeXgn/6nqzclIGqNdKCsNXDzpI0D3sP13Vwwmyoqv2etoTak2IHqZiXZDqg==} - cpu: [arm64] - os: [openbsd] - - lefthook-openbsd-x64@1.11.3: - resolution: {integrity: sha512-l7om+ZjWpYrVZyDuElwnucZhEqa7YfwlRaKBenkBxEh2zMje8O6Zodeuma1KmyDbSFvnvEjARo/Ejiot4gLXEw==} - cpu: [x64] - os: [openbsd] - - lefthook-windows-arm64@1.11.3: - resolution: {integrity: sha512-X0iTrql2gfPAkU2dzRwuHWgW5RcqCPbzJtKQ41X6Y/F7iQacRknmuYUGyC81funSvzGAsvlusMVLUvaFjIKnbA==} - cpu: [arm64] - os: [win32] - - lefthook-windows-x64@1.11.3: - resolution: {integrity: sha512-F+ORMn6YJXoS0EXU5LtN1FgV4QX9rC9LucZEkRmK6sKmS7hcb9IHpyb7siRGytArYzJvXVjPbxPBNSBdN4egZQ==} - cpu: [x64] - os: [win32] - - lefthook@1.11.3: - resolution: {integrity: sha512-HJp37y62j3j8qzAOODWuUJl4ysLwsDvCTBV6odr3jIRHR/a5e+tI14VQGIBcpK9ysqC3pGWyW5Rp9Jv1YDubyw==} - hasBin: true - light-my-request@6.6.0: resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} @@ -30259,49 +30202,6 @@ snapshots: leac@0.6.0: {} - lefthook-darwin-arm64@1.11.3: - optional: true - - lefthook-darwin-x64@1.11.3: - optional: true - - lefthook-freebsd-arm64@1.11.3: - optional: true - - lefthook-freebsd-x64@1.11.3: - optional: true - - lefthook-linux-arm64@1.11.3: - optional: true - - lefthook-linux-x64@1.11.3: - optional: true - - lefthook-openbsd-arm64@1.11.3: - optional: true - - lefthook-openbsd-x64@1.11.3: - optional: true - - lefthook-windows-arm64@1.11.3: - optional: true - - lefthook-windows-x64@1.11.3: - optional: true - - lefthook@1.11.3: - optionalDependencies: - lefthook-darwin-arm64: 1.11.3 - lefthook-darwin-x64: 1.11.3 - lefthook-freebsd-arm64: 1.11.3 - lefthook-freebsd-x64: 1.11.3 - lefthook-linux-arm64: 1.11.3 - lefthook-linux-x64: 1.11.3 - lefthook-openbsd-arm64: 1.11.3 - lefthook-openbsd-x64: 1.11.3 - lefthook-windows-arm64: 1.11.3 - lefthook-windows-x64: 1.11.3 - light-my-request@6.6.0: dependencies: cookie: 1.0.2 From 899323323da1e243774ed6168feeb552b9677e60 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Wed, 17 Jun 2026 18:06:00 +0100 Subject: [PATCH 07/10] ignore generated antlr files --- .oxfmtrc.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 90f19a65790..bb43c7c3ac4 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -21,5 +21,7 @@ "**/storybook-static/", "**/.changeset/", "**/dist/", + "**/*.yaml", + "internal-packages/tsql/src/grammar/" ] } From eb615947e5eee9de44e1ed4dc0d5e17391591ad6 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Thu, 25 Jun 2026 14:43:01 +0100 Subject: [PATCH 08/10] update configs --- .oxfmtrc.json | 5 ++++- .oxlintrc.json | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index bb43c7c3ac4..71809f50ef4 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -21,7 +21,10 @@ "**/storybook-static/", "**/.changeset/", "**/dist/", + "internal-packages/tsql/src/grammar/", + // Maybe turn these on in the future "**/*.yaml", - "internal-packages/tsql/src/grammar/" + "**/*.mdx", + "**/*.md" ] } diff --git a/.oxlintrc.json b/.oxlintrc.json index f028e901d34..99babf86a17 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -9,19 +9,38 @@ "**/seedCloud.ts", "**/populate.js" ], + // All rules below are disabled because they currently fire on the existing + // codebase (1748 warnings across these 20 rules as of this commit). Disabled + // to keep the lint baseline green; re-enable and fix incrementally. "rules": { - "consistent-type-imports": [ - "warn", - { - "prefer": "type-imports", - "disallowTypeAnnotations": true, - "fixStyle": "inline-type-imports" - } - ], - "import/no-duplicates": ["warn", { "prefer-inline": true }], - // Disabled for now: oxlint treats any use*()/use() call as a React hook, - // producing false positives in async/test code (testcontainers, cli-v3 e2e). - // Re-enable (ideally scoped to React packages) once those are addressed. + // eslint + "no-unused-vars": "off", + "no-unused-expressions": "off", + "no-control-regex": "off", + "no-empty-pattern": "off", + "no-unused-private-class-members": "off", + "no-useless-catch": "off", + "no-unsafe-optional-chaining": "off", + "no-unreachable": "off", + "require-yield": "off", + "no-async-promise-executor": "off", + "no-unsafe-finally": "off", + "no-useless-escape": "off", + // typescript + "typescript/consistent-type-imports": "off", + "typescript/no-this-alias": "off", + "typescript/no-non-null-asserted-optional-chain": "off", + "typescript/no-unnecessary-parameter-property-assignment": "off", + // import + "import/no-duplicates": "off", + "import/namespace": "off", + // react + "react/jsx-key": "off", + "react/no-children-prop": "off", + // react-hooks + "react-hooks/exhaustive-deps": "off", + // oxlint treats any use*()/use() call as a React hook, producing false + // positives in async/test code (testcontainers, cli-v3 e2e). "react-hooks/rules-of-hooks": "off" } } From 28592adeb8892775d11fa5d5249c27ab8cf1bc7e Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Fri, 26 Jun 2026 12:10:07 +0100 Subject: [PATCH 09/10] temp: don't format/lint webapp --- .oxfmtrc.json | 1 + .oxlintrc.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 71809f50ef4..b41db4dda9e 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -12,6 +12,7 @@ "sortPackageJson": false, "ignorePatterns": [ "node_modules", + "apps/webapp", ".env", ".env.local", "pnpm-lock.yaml", diff --git a/.oxlintrc.json b/.oxlintrc.json index 99babf86a17..76175b9eb54 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -7,7 +7,8 @@ "**/*.d.ts", "**/seed.js", "**/seedCloud.ts", - "**/populate.js" + "**/populate.js", + "apps/webapp/**" ], // All rules below are disabled because they currently fire on the existing // codebase (1748 warnings across these 20 rules as of this commit). Disabled From 9e076274a43250f18ac9f668a21654d22151c538 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Fri, 26 Jun 2026 12:10:27 +0100 Subject: [PATCH 10/10] lint + format --- .github/labeler.yml | 2 +- .../workflows/dependabot-critical-alerts.yml | 2 +- .../workflows/dependabot-weekly-summary.yml | 8 +- .github/workflows/release-helm.yml | 12 +- .github/workflows/workflow-checks.yml | 12 +- .github/zizmor.yml | 2 +- apps/coordinator/package.json | 2 +- apps/docker-provider/package.json | 2 +- apps/kubernetes-provider/package.json | 2 +- .../k8sPodCountSignalSource.test.ts | 4 +- .../redisBackpressureSignalSource.test.ts | 21 +- apps/supervisor/src/env.ts | 23 +- apps/supervisor/src/index.ts | 5 +- .../services/computeSnapshotService.test.ts | 8 +- .../src/services/otlpTraceService.test.ts | 4 +- .../warmStartVerificationService.test.ts | 5 +- apps/supervisor/src/wideEvents/index.ts | 7 +- .../src/wideEvents/middleware.test.ts | 23 +- .../src/workloadManager/compute.test.ts | 4 +- .../supervisor/src/workloadManager/compute.ts | 4 +- .../src/workloadManager/kubernetes.ts | 12 +- apps/supervisor/src/workloadManager/types.ts | 7 +- apps/supervisor/src/workloadServer/index.ts | 32 +- depot.json | 2 +- .../provisioning/dashboards/batch-queue.json | 63 +- .../provisioning/dashboards/dashboards.yml | 1 - .../dashboards/nodejs-runtime.json | 40 +- .../dashboards/realtime-native.json | 11 +- .../dashboards/runs-replication.json | 42 +- .../provisioning/datasources/datasources.yml | 1 - docker/config/prometheus.yml | 1 - docker/config/toxiproxy.json | 2 +- docs/docs.json | 73 +- docs/style.css | 52 +- hosting/docker/webapp/docker-compose.yml | 2 +- hosting/docker/worker/docker-compose.yml | 8 +- internal-packages/cache/package.json | 2 +- .../cache/src/stores/lruMemory.ts | 12 +- internal-packages/cache/src/stores/redis.ts | 7 +- .../clickhouse/src/client/client.ts | 5 +- .../clickhouse/src/client/noop.ts | 5 +- .../clickhouse/src/client/tsql.ts | 9 +- internal-packages/clickhouse/src/sessions.ts | 10 +- .../clickhouse/src/taskEvents.ts | 15 +- .../clickhouse/src/taskRuns.test.ts | 16 +- internal-packages/clickhouse/src/tsql.test.ts | 83 +- internal-packages/compute/src/client.ts | 6 +- .../drizzle/meta/0000_snapshot.json | 2 +- .../drizzle/meta/0001_snapshot.json | 7 +- .../drizzle/meta/_journal.json | 2 +- .../dashboard-agent-db/migrate-status.mjs | 9 +- .../dashboard-agent-db/src/queries.ts | 20 +- .../src/dashboard-agent.eval.ts | 78 +- .../src/dashboard-agent.test.ts | 4 +- .../dashboard-agent/src/dashboard-agent.ts | 11 +- .../dashboard-agent/src/eval-turn.ts | 34 +- .../dashboard-agent/src/prompts.ts | 3 +- .../dashboard-agent/src/repo-tools.test.ts | 27 +- .../dashboard-agent/src/repo-tools.ts | 32 +- .../dashboard-agent/src/tool-schemas.ts | 83 +- .../dashboard-agent/src/tools.ts | 46 +- internal-packages/database/package.json | 2 +- .../emails/emails/mfa-enabled.tsx | 3 +- internal-packages/emails/src/index.tsx | 4 +- .../emails/src/transports/aws-ses.ts | 24 +- .../emails/src/transports/index.ts | 12 +- .../emails/src/transports/null.ts | 11 +- .../emails/src/transports/resend.ts | 14 +- internal-packages/emails/tsconfig.json | 2 +- .../llm-model-catalog/scripts/generate.mjs | 15 +- .../llm-model-catalog/src/defaultPrices.ts | 6169 +++++++++-------- .../llm-model-catalog/src/model-catalog.json | 713 +- .../llm-model-catalog/src/modelCatalog.ts | 4059 +++++------ .../llm-model-catalog/src/registry.test.ts | 12 +- .../llm-model-catalog/src/registry.ts | 5 +- .../llm-model-catalog/src/sync.test.ts | 9 +- .../llm-model-catalog/src/sync.ts | 4 +- internal-packages/otlp-importer/package.json | 2 +- .../proto/collector/logs/v1/logs_service.ts | 56 +- .../collector/metrics/v1/metrics_service.ts | 74 +- .../proto/collector/trace/v1/trace_service.ts | 57 +- .../opentelemetry/proto/common/v1/common.ts | 62 +- .../opentelemetry/proto/logs/v1/logs.ts | 68 +- .../opentelemetry/proto/metrics/v1/metrics.ts | 289 +- .../proto/resource/v1/resource.ts | 17 +- .../opentelemetry/proto/trace/v1/trace.ts | 94 +- internal-packages/rbac/src/ability.test.ts | 8 +- internal-packages/rbac/src/fallback.ts | 22 +- internal-packages/rbac/src/index.ts | 19 +- internal-packages/redis/package.json | 2 +- internal-packages/redis/src/index.ts | 6 +- internal-packages/replication/package.json | 2 +- internal-packages/run-engine/package.json | 2 +- .../src/batch-queue/completionTracker.ts | 6 +- .../run-engine/src/batch-queue/index.ts | 28 +- .../src/batch-queue/tests/index.test.ts | 97 +- .../run-engine/src/engine/index.ts | 157 +- .../run-engine/src/engine/retrying.ts | 17 +- .../src/engine/systems/delayedRunSystem.ts | 1 - .../engine/systems/executionSnapshotSystem.ts | 4 +- .../engine/systems/pendingVersionSystem.ts | 4 +- .../src/engine/systems/runAttemptSystem.ts | 15 +- .../src/engine/systems/ttlSystem.ts | 226 +- .../src/engine/systems/waitpointSystem.ts | 2 +- .../engine/tests/batchTriggerAndWait.test.ts | 4 +- .../src/engine/tests/cancelling.test.ts | 15 +- .../engine/tests/createCancelledRun.test.ts | 91 +- .../engine/tests/createFailedTaskRun.test.ts | 189 +- .../src/engine/tests/debounce.test.ts | 831 ++- .../src/engine/tests/dequeuing.test.ts | 10 +- .../engine/tests/getSnapshotsSince.test.ts | 5 +- .../tests/helpers/snapshotTestHelpers.ts | 4 +- .../src/engine/tests/lazyWaitpoint.test.ts | 501 +- .../src/engine/tests/trigger.test.ts | 21 +- .../run-engine/src/engine/tests/ttl.test.ts | 25 +- .../run-engine/src/run-queue/index.ts | 27 +- .../src/run-queue/tests/ckCounters.test.ts | 632 +- .../src/run-queue/tests/ckIndex.test.ts | 562 +- .../dequeueMessageFromWorkerQueue.test.ts | 5 +- .../run-queue/tests/enqueueMessage.test.ts | 284 +- .../src/run-queue/tests/keyProducer.test.ts | 8 +- .../src/run-queue/tests/nack.test.ts | 20 +- .../run-store/src/NoopRunStore.ts | 108 +- .../run-store/src/PostgresRunStore.test.ts | 568 +- .../run-store/src/PostgresRunStore.ts | 17 +- internal-packages/run-store/src/types.ts | 109 +- .../schedule-engine/package.json | 2 +- .../schedule-engine/src/engine/index.ts | 2 +- .../test/scheduleRecovery.test.ts | 3 +- .../src/fixtures/deno/test.ts | 13 +- .../src/fixtures/esm-import/test.mjs | 13 +- internal-packages/sso/src/fallback.ts | 5 +- internal-packages/sso/src/index.ts | 18 +- internal-packages/sso/src/loader.test.ts | 16 +- .../testcontainers/src/webapp.ts | 22 +- internal-packages/tracing/package.json | 2 +- internal-packages/tsql/src/index.test.ts | 100 +- internal-packages/tsql/src/query/database.ts | 4 +- internal-packages/tsql/src/query/functions.ts | 176 +- .../tsql/src/query/parse_string.ts | 104 +- internal-packages/tsql/src/query/parser.ts | 8 +- .../tsql/src/query/printer.test.ts | 286 +- internal-packages/tsql/src/query/printer.ts | 9 +- .../tsql/src/query/results.test.ts | 5 +- internal-packages/tsql/src/query/results.ts | 1 - internal-packages/tsql/src/query/schema.ts | 6 +- .../tsql/src/query/security.test.ts | 14 +- internal-packages/tsql/src/query/timings.ts | 148 +- internal-packages/zod-worker/package.json | 2 +- .../src/extensions/core/neonSyncEnvVars.ts | 3 +- .../extensions/core/syncSupabaseEnvVars.ts | 12 +- .../src/extensions/core/vercelSyncEnvVars.ts | 2 +- .../build/src/internal/additionalFiles.ts | 6 +- .../fixtures/monorepo-react-email/.yarnrc.yml | 2 +- packages/cli-v3/e2e/utils.ts | 16 +- packages/cli-v3/src/apiClient.ts | 6 +- packages/cli-v3/src/build/bundleSkills.ts | 12 +- packages/cli-v3/src/build/manifests.ts | 5 +- packages/cli-v3/src/commands/deploy.ts | 8 +- packages/cli-v3/src/commands/dev.ts | 26 +- packages/cli-v3/src/commands/init.ts | 5 +- packages/cli-v3/src/commands/skills.ts | 36 +- packages/cli-v3/src/config.ts | 8 +- packages/cli-v3/src/dev/devOutput.ts | 12 +- packages/cli-v3/src/dev/devSupervisor.ts | 21 +- .../src/entryPoints/dev-run-controller.ts | 1 - .../src/entryPoints/managed-index-worker.ts | 4 +- .../cli-v3/src/executions/taskRunProcess.ts | 10 +- packages/cli-v3/src/mcp/config.ts | 6 +- packages/cli-v3/src/mcp/formatters.ts | 8 +- packages/cli-v3/src/mcp/schemas.ts | 28 +- packages/cli-v3/src/mcp/smoke.test.ts | 61 +- packages/cli-v3/src/mcp/tools.test.ts | 5 +- packages/cli-v3/src/mcp/tools.ts | 6 +- packages/cli-v3/src/mcp/tools/agentChat.ts | 35 +- packages/cli-v3/src/mcp/tools/agents.ts | 4 +- packages/cli-v3/src/mcp/tools/deploys.ts | 12 +- packages/cli-v3/src/mcp/tools/devServer.ts | 5 +- packages/cli-v3/src/mcp/tools/profiles.ts | 4 +- packages/cli-v3/src/mcp/tools/prompts.ts | 12 +- packages/cli-v3/src/mcp/tools/query.ts | 4 +- packages/cli-v3/src/mcp/tools/runs.ts | 24 +- packages/cli-v3/src/mcp/tools/tasks.ts | 14 +- packages/cli-v3/src/rules/install.ts | 1 - .../cli-v3/src/utilities/colorMarkup.test.ts | 18 +- packages/cli-v3/src/utilities/colorMarkup.ts | 5 +- .../cli-v3/src/utilities/discoveryCheck.ts | 13 +- .../src/utilities/platformNotifications.ts | 9 +- packages/core/src/schemas/eventFilter.ts | 4 +- packages/core/src/v3/apiClient/core.ts | 4 +- packages/core/src/v3/apiClient/errors.ts | 30 +- packages/core/src/v3/apiClient/getBranch.ts | 6 +- packages/core/src/v3/apiClient/index.ts | 43 +- .../core/src/v3/apiClient/runStream.test.ts | 6 +- packages/core/src/v3/apiClient/runStream.ts | 4 +- .../core/src/v3/apiClientManager/index.ts | 8 +- packages/core/src/v3/auth/environment.ts | 6 +- packages/core/src/v3/errors.test.ts | 4 +- packages/core/src/v3/errors.ts | 8 +- packages/core/src/v3/idempotencyKeys.ts | 9 +- packages/core/src/v3/inputStreams/manager.ts | 62 +- .../core/src/v3/inputStreams/noopManager.ts | 4 +- .../core/src/v3/otel/nodejsRuntimeMetrics.ts | 37 +- packages/core/src/v3/otel/tracingSDK.ts | 43 +- .../src/v3/realtimeStreams/manager.test.ts | 17 +- .../core/src/v3/realtimeStreams/manager.ts | 8 +- .../src/v3/realtimeStreams/streamsWriterV1.ts | 2 +- packages/core/src/v3/realtimeStreams/types.ts | 9 +- .../core/src/v3/resource-catalog/catalog.ts | 6 +- .../core/src/v3/resource-catalog/index.ts | 6 +- .../resource-catalog/noopResourceCatalog.ts | 6 +- .../standardResourceCatalog.ts | 11 +- .../supervisor/queueConsumer.test.ts | 8 +- .../supervisor/queueConsumer.ts | 15 +- .../v3/runEngineWorker/supervisor/session.ts | 5 +- .../core/src/v3/runMetadata/noopManager.ts | 8 +- packages/core/src/v3/schemas/build.ts | 8 +- packages/core/src/v3/serverOnly/httpServer.ts | 10 +- .../core/src/v3/serverOnly/idempotencyKeys.ts | 7 +- packages/core/src/v3/sessionStreams/index.ts | 12 +- .../src/v3/sessionStreams/manager.test.ts | 24 +- .../core/src/v3/sessionStreams/manager.ts | 30 +- .../core/src/v3/sessionStreams/noopManager.ts | 6 +- packages/core/src/v3/sessionStreams/types.ts | 12 +- .../core/src/v3/taskContext/otelProcessors.ts | 8 +- .../core/src/v3/test/mock-task-context.ts | 6 +- .../src/v3/test/test-input-stream-manager.ts | 4 +- .../v3/test/test-session-stream-manager.ts | 23 +- packages/core/src/v3/timeout/api.ts | 4 +- packages/core/src/v3/timeout/types.ts | 4 +- packages/core/src/v3/types/schemas.ts | 26 +- packages/core/src/v3/types/tasks.ts | 88 +- packages/core/src/v3/types/tools.ts | 4 +- packages/core/src/v3/utils/durations.ts | 4 +- .../src/v3/utils/reconnectBackoff.test.ts | 4 +- .../core/src/v3/utils/structuredLogger.ts | 4 +- packages/core/src/v3/waitpoints/index.ts | 8 +- packages/core/src/v3/workers/taskExecutor.ts | 4 +- packages/core/test/errors.test.ts | 10 +- .../test/externalSpanExporterWrapper.test.ts | 5 +- packages/core/test/flattenAttributes.test.ts | 20 +- .../core/test/recordSpanException.test.ts | 5 +- packages/core/test/resourceCatalog.test.ts | 44 +- packages/core/test/skillCatalog.test.ts | 7 +- packages/plugins/src/rbac.ts | 9 +- packages/plugins/src/sso.ts | 9 +- packages/redis-worker/package.json | 2 +- packages/redis-worker/src/fair-queue/index.ts | 14 +- .../redis-worker/src/fair-queue/scheduler.ts | 1 - .../src/fair-queue/schedulers/drr.ts | 11 +- .../src/fair-queue/schedulers/index.ts | 1 - .../src/fair-queue/schedulers/roundRobin.ts | 1 - .../src/fair-queue/schedulers/weighted.ts | 7 +- .../redis-worker/src/fair-queue/telemetry.ts | 1 - .../src/fair-queue/tests/drr.test.ts | 548 +- .../src/fair-queue/tests/fairQueue.test.ts | 2 - .../fair-queue/tests/raceConditions.test.ts | 7 +- .../fair-queue/tests/tenantDispatch.test.ts | 5 +- .../src/fair-queue/tests/visibility.test.ts | 61 +- .../redis-worker/src/fair-queue/visibility.ts | 4 +- .../redis-worker/src/mollifier/buffer.test.ts | 736 +- packages/redis-worker/src/mollifier/buffer.ts | 78 +- .../src/mollifier/drainer.test.ts | 558 +- .../redis-worker/src/mollifier/drainer.ts | 19 +- .../redis-worker/src/mollifier/schemas.ts | 5 +- packages/redis-worker/src/utils.ts | 4 +- packages/redis-worker/src/worker.ts | 47 +- packages/rsc/src/build.ts | 2 +- packages/schema-to-json/tsconfig.json | 2 +- packages/schema-to-json/vitest.config.ts | 6 +- packages/trigger-sdk/src/v3/ai-shared.ts | 29 +- packages/trigger-sdk/src/v3/ai.ts | 1823 +++-- packages/trigger-sdk/src/v3/chat-client.ts | 67 +- packages/trigger-sdk/src/v3/chat-react.ts | 5 +- .../trigger-sdk/src/v3/chat-server.test.ts | 69 +- packages/trigger-sdk/src/v3/chat-server.ts | 17 +- packages/trigger-sdk/src/v3/chat.test.ts | 53 +- packages/trigger-sdk/src/v3/chat.ts | 24 +- .../trigger-sdk/src/v3/idempotencyKeys.ts | 6 +- packages/trigger-sdk/src/v3/prompt.ts | 57 +- .../trigger-sdk/src/v3/promptManagement.ts | 51 +- packages/trigger-sdk/src/v3/retry.ts | 9 +- packages/trigger-sdk/src/v3/runs.ts | 8 +- .../trigger-sdk/src/v3/schedules/index.ts | 6 +- packages/trigger-sdk/src/v3/schemas.ts | 2 +- packages/trigger-sdk/src/v3/sessions.ts | 11 +- packages/trigger-sdk/src/v3/shared.test.ts | 1 - packages/trigger-sdk/src/v3/shared.ts | 36 +- packages/trigger-sdk/src/v3/skill.ts | 5 +- packages/trigger-sdk/src/v3/streams.test.ts | 94 +- packages/trigger-sdk/src/v3/streams.ts | 26 +- .../src/v3/test/mock-chat-agent.ts | 179 +- .../src/v3/test/test-session-handle.ts | 49 +- .../trigger-sdk/src/v3/triggerClient.test.ts | 2 +- .../trigger-sdk/test/chat-snapshot.test.ts | 50 +- .../trigger-sdk/test/chatHandover.test.ts | 22 +- .../test/chatHandoverBackends.test.ts | 18 +- packages/trigger-sdk/test/merge-by-id.test.ts | 22 +- .../trigger-sdk/test/mockChatAgent.test.ts | 48 +- .../trigger-sdk/test/promptCaching.test.ts | 8 +- .../trigger-sdk/test/recovery-boot.test.ts | 52 +- .../test/replay-session-in.test.ts | 14 +- .../test/replay-session-out.test.ts | 42 +- packages/trigger-sdk/test/wire-shape.test.ts | 32 +- packages/trigger-sdk/vitest.config.ts | 1 - rules/manifest.json | 2 +- scripts/bundleSdkDocs.ts | 4 +- scripts/generate-github-release.mjs | 16 +- scripts/recover-stuck-runs.ts | 12 +- scripts/stamp-preview-version.mjs | 4 +- server.json | 2 +- 311 files changed, 12193 insertions(+), 12583 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 279bab91a79..6760c37bcd0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,4 +9,4 @@ - any: ["**/*.md"] "šŸ“Œ area: ci": - - any: [".github/**/*"] \ No newline at end of file + - any: [".github/**/*"] diff --git a/.github/workflows/dependabot-critical-alerts.yml b/.github/workflows/dependabot-critical-alerts.yml index e69e9c0af7b..9a61387324d 100644 --- a/.github/workflows/dependabot-critical-alerts.yml +++ b/.github/workflows/dependabot-critical-alerts.yml @@ -2,7 +2,7 @@ name: Dependabot Critical Alerts on: schedule: - - cron: "0 8 * * *" # Daily 08:00 UTC + - cron: "0 8 * * *" # Daily 08:00 UTC workflow_dispatch: inputs: severity: diff --git a/.github/workflows/dependabot-weekly-summary.yml b/.github/workflows/dependabot-weekly-summary.yml index 43463b488e4..d7b489d55da 100644 --- a/.github/workflows/dependabot-weekly-summary.yml +++ b/.github/workflows/dependabot-weekly-summary.yml @@ -2,7 +2,7 @@ name: Dependabot Weekly Summary on: schedule: - - cron: "0 8 * * 1" # Mon 08:00 UTC + - cron: "0 8 * * 1" # Mon 08:00 UTC workflow_dispatch: # Single-purpose monitoring workflow; serialise on workflow name only - we never @@ -12,9 +12,9 @@ concurrency: cancel-in-progress: false permissions: - contents: read # gh CLI baseline - pull-requests: read # gh pr list (open dependabot PRs) - actions: read # gh run list / view (parse latest dependabot run logs) + contents: read # gh CLI baseline + pull-requests: read # gh pr list (open dependabot PRs) + actions: read # gh run list / view (parse latest dependabot run logs) jobs: summary: diff --git a/.github/workflows/release-helm.yml b/.github/workflows/release-helm.yml index 6dbfee7d0c8..d21c62de8be 100644 --- a/.github/workflows/release-helm.yml +++ b/.github/workflows/release-helm.yml @@ -3,17 +3,17 @@ name: 🧭 Helm Chart Release on: push: tags: - - 'helm-v*' + - "helm-v*" workflow_call: inputs: chart_version: - description: 'Chart version to release' + description: "Chart version to release" required: true type: string workflow_dispatch: inputs: chart_version: - description: 'Chart version to release' + description: "Chart version to release" required: true type: string @@ -58,7 +58,7 @@ jobs: - name: Validate manifests uses: docker://ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c with: - entrypoint: '/kubeconform' + entrypoint: "/kubeconform" args: "-summary -output json ./helm-output" release: @@ -134,7 +134,7 @@ jobs: run: | VERSION="${STEPS_VERSION_OUTPUTS_VERSION}" CHART_PACKAGE="/tmp/${{ env.CHART_NAME }}-${VERSION}.tgz" - + # Push to GHCR OCI registry helm push "$CHART_PACKAGE" "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts" env: @@ -153,7 +153,7 @@ jobs: oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/${{ env.CHART_NAME }} \ --version "${{ steps.version.outputs.version }}" ``` - + ### Changes See commit history for detailed changes in this release. files: | diff --git a/.github/workflows/workflow-checks.yml b/.github/workflows/workflow-checks.yml index e99a4d33427..62406671fc5 100644 --- a/.github/workflows/workflow-checks.yml +++ b/.github/workflows/workflow-checks.yml @@ -4,14 +4,14 @@ on: push: branches: [main] paths: - - '.github/workflows/**' - - '.github/actions/**' - - '.github/zizmor.yml' + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" pull_request: paths: - - '.github/workflows/**' - - '.github/actions/**' - - '.github/zizmor.yml' + - ".github/workflows/**" + - ".github/actions/**" + - ".github/zizmor.yml" permissions: {} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 2fcbb540127..e7920e8cf17 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -2,4 +2,4 @@ rules: unpinned-uses: config: policies: - '*': hash-pin + "*": hash-pin diff --git a/apps/coordinator/package.json b/apps/coordinator/package.json index 3b4240bd37d..446606a44c8 100644 --- a/apps/coordinator/package.json +++ b/apps/coordinator/package.json @@ -27,4 +27,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/docker-provider/package.json b/apps/docker-provider/package.json index f3e4015ef08..aa812c68d4a 100644 --- a/apps/docker-provider/package.json +++ b/apps/docker-provider/package.json @@ -24,4 +24,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/kubernetes-provider/package.json b/apps/kubernetes-provider/package.json index 6cb26e2c70f..b4a4307abbb 100644 --- a/apps/kubernetes-provider/package.json +++ b/apps/kubernetes-provider/package.json @@ -25,4 +25,4 @@ "esbuild": "^0.19.11", "tsx": "^4.7.0" } -} \ No newline at end of file +} diff --git a/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts b/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts index 8857372a042..8c7104cfb14 100644 --- a/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts +++ b/apps/supervisor/src/backpressure/k8sPodCountSignalSource.test.ts @@ -73,9 +73,9 @@ describe("K8sPodCountSignalSource", () => { engageThreshold: 10000, releaseThreshold: 5000, }); - expect((await source.read()).engaged).toBe(true); // engage + expect((await source.read()).engaged).toBe(true); // engage count = 7000; - expect((await source.read()).engaged).toBe(true); // band -> still engaged + expect((await source.read()).engaged).toBe(true); // band -> still engaged count = 4999; expect((await source.read()).engaged).toBe(false); // below release -> off count = 7000; diff --git a/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts b/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts index 77a7457b13c..dc8040a4c5e 100644 --- a/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts +++ b/apps/supervisor/src/backpressure/redisBackpressureSignalSource.test.ts @@ -49,14 +49,17 @@ describe("RedisBackpressureSignalSource", () => { } }); - redisTest("returns null for valid JSON of the wrong shape (fail-open)", async ({ redisOptions }) => { - const redis = new Redis(redisOptions); - try { - await redis.set(KEY, JSON.stringify({ foo: "bar" })); - const source = new RedisBackpressureSignalSource(redis, KEY); - expect(await source.read()).toBeNull(); - } finally { - await redis.quit(); + redisTest( + "returns null for valid JSON of the wrong shape (fail-open)", + async ({ redisOptions }) => { + const redis = new Redis(redisOptions); + try { + await redis.set(KEY, JSON.stringify({ foo: "bar" })); + const source = new RedisBackpressureSignalSource(redis, KEY); + expect(await source.read()).toBeNull(); + } finally { + await redis.quit(); + } } - }); + ); }); diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 10e8f9b62b3..2cb1bfb9a74 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -75,9 +75,21 @@ export const Env = z TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_TLS_DISABLED: BoolEnv.default(false), TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENABLED: BoolEnv.default(false), TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_DRY_RUN: BoolEnv.default(true), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENGAGE: z.coerce.number().int().positive().default(10_000), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_RELEASE: z.coerce.number().int().positive().default(5_000), - TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_REFRESH_MS: z.coerce.number().int().positive().default(5_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_ENGAGE: z.coerce + .number() + .int() + .positive() + .default(10_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_RELEASE: z.coerce + .number() + .int() + .positive() + .default(5_000), + TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_REFRESH_MS: z.coerce + .number() + .int() + .positive() + .default(5_000), // Hard timeout on the apiserver /metrics scrape. A hung request would otherwise // never settle and freeze the monitor's refresh loop (fail-open silently). TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_SCRAPE_TIMEOUT_MS: z.coerce @@ -350,7 +362,10 @@ export const Env = z path: ["TRIGGER_WORKLOAD_API_DOMAIN"], }); } - if (data.TRIGGER_DEQUEUE_BACKPRESSURE_ENABLED && !data.TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_HOST) { + if ( + data.TRIGGER_DEQUEUE_BACKPRESSURE_ENABLED && + !data.TRIGGER_DEQUEUE_BACKPRESSURE_REDIS_HOST + ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index d2bc82c13b9..833448ad873 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -274,7 +274,10 @@ class ManagedSupervisor { rampMs: env.TRIGGER_DEQUEUE_BACKPRESSURE_RAMP_MS, dryRun: env.TRIGGER_DEQUEUE_BACKPRESSURE_POD_COUNT_DRY_RUN, logger: this.logger, - metrics: new BackpressureMetrics({ register, prefix: "supervisor_backpressure_pod_count" }), + metrics: new BackpressureMetrics({ + register, + prefix: "supervisor_backpressure_pod_count", + }), }) ); this.logger.log("šŸ›‘ Dequeue backpressure enabled (pod-count source)", { diff --git a/apps/supervisor/src/services/computeSnapshotService.test.ts b/apps/supervisor/src/services/computeSnapshotService.test.ts index b039b63bd4d..44a13a4bdd3 100644 --- a/apps/supervisor/src/services/computeSnapshotService.test.ts +++ b/apps/supervisor/src/services/computeSnapshotService.test.ts @@ -10,7 +10,9 @@ const DELAY_MS = 200; const SETTLE_MS = 600; function createService() { - const snapshot = vi.fn(async (_opts: { runnerId: string; metadata: Record }) => true); + const snapshot = vi.fn( + async (_opts: { runnerId: string; metadata: Record }) => true + ); const computeManager = { snapshotDelayMs: DELAY_MS, @@ -97,9 +99,7 @@ describe("ComputeSnapshotService", () => { expect(service.cancel("run_1", "runner-b")).toBe(false); await vi.waitFor(() => expect(snapshot).toHaveBeenCalledTimes(1), { timeout: 2_000 }); - expect(snapshot).toHaveBeenCalledWith( - expect.objectContaining({ runnerId: "runner-a" }) - ); + expect(snapshot).toHaveBeenCalledWith(expect.objectContaining({ runnerId: "runner-a" })); } finally { service.stop(); } diff --git a/apps/supervisor/src/services/otlpTraceService.test.ts b/apps/supervisor/src/services/otlpTraceService.test.ts index baf3bd90306..95053021ca1 100644 --- a/apps/supervisor/src/services/otlpTraceService.test.ts +++ b/apps/supervisor/src/services/otlpTraceService.test.ts @@ -31,9 +31,7 @@ describe("buildPayload", () => { expect(triggerAttr).toEqual({ key: "$trigger", value: { boolValue: true } }); // Resource attributes - const envAttr = resourceSpan.resource.attributes.find( - (a) => a.key === "ctx.environment.id" - ); + const envAttr = resourceSpan.resource.attributes.find((a) => a.key === "ctx.environment.id"); expect(envAttr).toEqual({ key: "ctx.environment.id", value: { stringValue: "env_123" }, diff --git a/apps/supervisor/src/services/warmStartVerificationService.test.ts b/apps/supervisor/src/services/warmStartVerificationService.test.ts index 994c678b4b8..4a5f58875c2 100644 --- a/apps/supervisor/src/services/warmStartVerificationService.test.ts +++ b/apps/supervisor/src/services/warmStartVerificationService.test.ts @@ -19,10 +19,7 @@ function makeMessage(runFriendlyId = "run_1"): DequeuedMessage { } as unknown as DequeuedMessage; } -function createService(opts: { - latestSnapshotId?: string; - probeError?: boolean; -}) { +function createService(opts: { latestSnapshotId?: string; probeError?: boolean }) { const getLatestSnapshot = vi.fn(async (_runId: string) => opts.probeError ? { success: false as const, error: "connection refused" } diff --git a/apps/supervisor/src/wideEvents/index.ts b/apps/supervisor/src/wideEvents/index.ts index 4eda429a50a..6e61d85896f 100644 --- a/apps/supervisor/src/wideEvents/index.ts +++ b/apps/supervisor/src/wideEvents/index.ts @@ -11,12 +11,7 @@ export { type Env, isValidRequestId, newState, type NewStateOptions } from "./ne export { emit, EmitMessage } from "./emit.js"; export { parseTraceId } from "./traceparent.js"; export { fromContext, wideEventStorage } from "./context.js"; -export { - type PhaseOpt, - recordPhase, - recordPhaseSince, - timePhase, -} from "./record.js"; +export { type PhaseOpt, recordPhase, recordPhaseSince, timePhase } from "./record.js"; export { emitOneShot, runWideEvent, diff --git a/apps/supervisor/src/wideEvents/middleware.test.ts b/apps/supervisor/src/wideEvents/middleware.test.ts index afb59f43d6e..a431df4ada5 100644 --- a/apps/supervisor/src/wideEvents/middleware.test.ts +++ b/apps/supervisor/src/wideEvents/middleware.test.ts @@ -124,7 +124,8 @@ describe("runWideEvent", () => { { service: "supervisor", env: {}, - enabled: true, op: "test", + enabled: true, + op: "test", setup: (state) => { state.meta.run_id = "run_abc"; state.extras.iteration = "dequeue"; @@ -158,16 +159,13 @@ describe("runWideEvent", () => { const lines = await captureStdout(async () => { await Promise.all( ["a", "b", "c"].map((tag) => - runWideEvent( - { service: "supervisor", env: {}, enabled: true, op: "test" }, - async () => { - const s = fromContext(); - if (!s) throw new Error("no state"); - s.meta.tag = tag; - await new Promise((r) => setTimeout(r, 5)); - expect(s.meta.tag).toBe(tag); - } - ) + runWideEvent({ service: "supervisor", env: {}, enabled: true, op: "test" }, async () => { + const s = fromContext(); + if (!s) throw new Error("no state"); + s.meta.tag = tag; + await new Promise((r) => setTimeout(r, 5)); + expect(s.meta.tag).toBe(tag); + }) ) ); }); @@ -182,7 +180,8 @@ describe("emitOneShot", () => { emitOneShot({ service: "supervisor", env: {}, - enabled: true, op: "test", + enabled: true, + op: "test", populate: (s) => { s.meta.run_id = "run_abc"; s.extras.event = "run:start"; diff --git a/apps/supervisor/src/workloadManager/compute.test.ts b/apps/supervisor/src/workloadManager/compute.test.ts index ea5ddabf285..95a9ca2f3b0 100644 --- a/apps/supervisor/src/workloadManager/compute.test.ts +++ b/apps/supervisor/src/workloadManager/compute.test.ts @@ -15,9 +15,7 @@ describe("runnerNameForAttempt", () => { describe("isRetryableCreateError", () => { it("retries statuses where the create definitely did not commit", () => { - expect(isRetryableCreateError(new ComputeClientError(500, "tap busy", "http://gw"))).toBe( - true - ); + expect(isRetryableCreateError(new ComputeClientError(500, "tap busy", "http://gw"))).toBe(true); expect(isRetryableCreateError(new ComputeClientError(503, "no placement", "http://gw"))).toBe( true ); diff --git a/apps/supervisor/src/workloadManager/compute.ts b/apps/supervisor/src/workloadManager/compute.ts index 7a4270513cf..857e129a83f 100644 --- a/apps/supervisor/src/workloadManager/compute.ts +++ b/apps/supervisor/src/workloadManager/compute.ts @@ -250,9 +250,7 @@ export class ComputeWorkloadManager implements WorkloadManager { // name registered, so subsequent attempts use a suffixed name. let suffixAttempts = false; for (; attempt <= this.createMaxAttempts; attempt++) { - const attemptRunnerId = suffixAttempts - ? runnerNameForAttempt(runnerId, attempt) - : runnerId; + const attemptRunnerId = suffixAttempts ? runnerNameForAttempt(runnerId, attempt) : runnerId; [error, data] = await tryCatch( this.compute.instances.create( attemptRunnerId === runnerId diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 9e79fff76c2..762860104bd 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -431,7 +431,8 @@ export class KubernetesWorkloadManager implements WorkloadManager { // Only large machine affinity produces hard requirements (non-large runs must stay off the large pool). // Schedule affinity is soft both ways. const required = [ - ...(largeNodeAffinity?.requiredDuringSchedulingIgnoredDuringExecution?.nodeSelectorTerms ?? []), + ...(largeNodeAffinity?.requiredDuringSchedulingIgnoredDuringExecution?.nodeSelectorTerms ?? + []), ]; const hasNodeAffinity = preferred.length > 0 || required.length > 0; @@ -443,7 +444,9 @@ export class KubernetesWorkloadManager implements WorkloadManager { return { ...(hasNodeAffinity && { nodeAffinity: { - ...(preferred.length > 0 && { preferredDuringSchedulingIgnoredDuringExecution: preferred }), + ...(preferred.length > 0 && { + preferredDuringSchedulingIgnoredDuringExecution: preferred, + }), ...(required.length > 0 && { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: required }, }), @@ -497,7 +500,10 @@ export class KubernetesWorkloadManager implements WorkloadManager { } #getScheduleNodeAffinityRules(isScheduledRun: boolean): k8s.V1NodeAffinity | undefined { - if (!env.KUBERNETES_SCHEDULED_RUN_AFFINITY_ENABLED || !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_POOL_LABEL_VALUE) { + if ( + !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_ENABLED || + !env.KUBERNETES_SCHEDULED_RUN_AFFINITY_POOL_LABEL_VALUE + ) { return undefined; } diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index 86199afe469..ee759cb8a79 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -1,4 +1,9 @@ -import type { EnvironmentType, MachinePreset, PlacementTag, RunAnnotations } from "@trigger.dev/core/v3"; +import type { + EnvironmentType, + MachinePreset, + PlacementTag, + RunAnnotations, +} from "@trigger.dev/core/v3"; export interface WorkloadManagerOptions { workloadApiProtocol: "http" | "https"; diff --git a/apps/supervisor/src/workloadServer/index.ts b/apps/supervisor/src/workloadServer/index.ts index 5dd1a79cc9a..7397313080a 100644 --- a/apps/supervisor/src/workloadServer/index.ts +++ b/apps/supervisor/src/workloadServer/index.ts @@ -317,9 +317,7 @@ export class WorkloadServer extends EventEmitter { return; } - reply.json( - completeResponse.data satisfies WorkloadRunAttemptCompleteResponseBody - ); + reply.json(completeResponse.data satisfies WorkloadRunAttemptCompleteResponseBody); return; } ), @@ -566,7 +564,9 @@ export class WorkloadServer extends EventEmitter { } reply.json( - dequeueResponse.data.map(legacifyCheckpointType) satisfies WorkloadDequeueFromVersionResponseBody + dequeueResponse.data.map( + legacifyCheckpointType + ) satisfies WorkloadDequeueFromVersionResponseBody ); } ), @@ -613,16 +613,22 @@ export class WorkloadServer extends EventEmitter { httpServer.route("/api/v1/compute/snapshot-complete", "POST", { bodySchema: SnapshotCallbackPayloadSchema, handler: async (ctx) => - this.wideRoute(ctx, "snapshot.callback", "/api/v1/compute/snapshot-complete", "POST", async () => { - const { reply, body } = ctx; - if (!this.snapshotService) { - reply.empty(404); - return; - } + this.wideRoute( + ctx, + "snapshot.callback", + "/api/v1/compute/snapshot-complete", + "POST", + async () => { + const { reply, body } = ctx; + if (!this.snapshotService) { + reply.empty(404); + return; + } - const result = await this.snapshotService.handleCallback(body); - reply.empty(result.status); - }), + const result = await this.snapshotService.handleCallback(body); + reply.empty(result.status); + } + ), }); return httpServer; diff --git a/depot.json b/depot.json index 60fd90c6ece..b1b6660dd5c 100644 --- a/depot.json +++ b/depot.json @@ -1,3 +1,3 @@ { "id": "g2k5ln95n6" -} \ No newline at end of file +} diff --git a/docker/config/grafana/provisioning/dashboards/batch-queue.json b/docker/config/grafana/provisioning/dashboards/batch-queue.json index 1c154b43fe9..7d27c67d698 100644 --- a/docker/config/grafana/provisioning/dashboards/batch-queue.json +++ b/docker/config/grafana/provisioning/dashboards/batch-queue.json @@ -186,7 +186,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -238,7 +243,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -298,7 +308,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -355,7 +370,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -420,7 +440,12 @@ "gridPos": { "h": 8, "w": 8, "x": 0, "y": 25 }, "id": 12, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -467,7 +492,12 @@ "gridPos": { "h": 8, "w": 8, "x": 8, "y": 25 }, "id": 13, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -561,7 +591,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 34 }, "id": 16, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -618,7 +653,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 34 }, "id": 17, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -683,7 +723,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 43 }, "id": 19, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ diff --git a/docker/config/grafana/provisioning/dashboards/dashboards.yml b/docker/config/grafana/provisioning/dashboards/dashboards.yml index bdb2d1b7135..9763da209d9 100644 --- a/docker/config/grafana/provisioning/dashboards/dashboards.yml +++ b/docker/config/grafana/provisioning/dashboards/dashboards.yml @@ -14,4 +14,3 @@ providers: allowUiUpdates: true options: path: /etc/grafana/provisioning/dashboards - diff --git a/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json b/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json index 9f190b4977e..1a432bcaaf3 100644 --- a/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json +++ b/docker/config/grafana/provisioning/dashboards/nodejs-runtime.json @@ -142,9 +142,7 @@ "mappings": [], "thresholds": { "mode": "absolute", - "steps": [ - { "color": "green", "value": null } - ] + "steps": [{ "color": "green", "value": null }] }, "unit": "short" }, @@ -207,7 +205,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -254,7 +257,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -324,7 +332,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -371,7 +384,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -418,7 +436,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 }, "id": 11, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -443,4 +466,3 @@ "uid": "nodejs-runtime", "version": 1 } - diff --git a/docker/config/grafana/provisioning/dashboards/realtime-native.json b/docker/config/grafana/provisioning/dashboards/realtime-native.json index 832f2c8e320..5bc21240c20 100644 --- a/docker/config/grafana/provisioning/dashboards/realtime-native.json +++ b/docker/config/grafana/provisioning/dashboards/realtime-native.json @@ -1,10 +1,7 @@ { "title": "Realtime Native Backend", "uid": "realtime-native", - "tags": [ - "trigger.dev", - "realtime" - ], + "tags": ["trigger.dev", "realtime"], "timezone": "browser", "schemaVersion": 39, "refresh": "10s", @@ -174,9 +171,7 @@ "colorMode": "background", "graphMode": "area", "reduceOptions": { - "calcs": [ - "lastNotNull" - ] + "calcs": ["lastNotNull"] } }, "description": "A backstop that finds missed changes means the notify/replay path is leaking. Alert on sustained non-zero." @@ -500,4 +495,4 @@ "description": "hit/coalesced vs miss = the single-flight cache collapsing same-filter herds. Gate in use near the limit = reconnect stampedes queueing." } ] -} \ No newline at end of file +} diff --git a/docker/config/grafana/provisioning/dashboards/runs-replication.json b/docker/config/grafana/provisioning/dashboards/runs-replication.json index 680f260daa0..2bdf91294c7 100644 --- a/docker/config/grafana/provisioning/dashboards/runs-replication.json +++ b/docker/config/grafana/provisioning/dashboards/runs-replication.json @@ -186,7 +186,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 }, "id": 6, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -233,7 +238,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 }, "id": 7, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -293,7 +303,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 9, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -350,7 +365,12 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 10, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -415,7 +435,12 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, "id": 13, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ @@ -519,7 +544,12 @@ "gridPos": { "h": 8, "w": 24, "x": 0, "y": 34 }, "id": 16, "options": { - "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ diff --git a/docker/config/grafana/provisioning/datasources/datasources.yml b/docker/config/grafana/provisioning/datasources/datasources.yml index 51194dbc064..52f2d640694 100644 --- a/docker/config/grafana/provisioning/datasources/datasources.yml +++ b/docker/config/grafana/provisioning/datasources/datasources.yml @@ -15,4 +15,3 @@ datasources: httpMethod: POST manageAlerts: true prometheusType: Prometheus - diff --git a/docker/config/prometheus.yml b/docker/config/prometheus.yml index b4ad879ffed..19e681de94a 100644 --- a/docker/config/prometheus.yml +++ b/docker/config/prometheus.yml @@ -30,4 +30,3 @@ scrape_configs: - job_name: "prometheus" static_configs: - targets: ["localhost:9090"] - diff --git a/docker/config/toxiproxy.json b/docker/config/toxiproxy.json index 3462471672d..435bf0c55b5 100644 --- a/docker/config/toxiproxy.json +++ b/docker/config/toxiproxy.json @@ -5,4 +5,4 @@ "upstream": "host.docker.internal:3030", "enabled": true } -] \ No newline at end of file +] diff --git a/docs/docs.json b/docs/docs.json index b74563300d8..f373503049c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -10,11 +10,7 @@ }, "favicon": "/images/favicon.png", "contextual": { - "options": [ - "copy", - "view", - "claude" - ] + "options": ["copy", "view", "claude"] }, "navigation": { "dropdowns": [ @@ -40,11 +36,7 @@ "pages": [ { "group": "Tasks", - "pages": [ - "tasks/overview", - "tasks/schemaTask", - "tasks/scheduled" - ] + "pages": ["tasks/overview", "tasks/schemaTask", "tasks/scheduled"] }, "triggering", "runs", @@ -57,10 +49,7 @@ "building-with-ai", { "group": "MCP Server", - "pages": [ - "mcp-introduction", - "mcp-tools" - ] + "pages": ["mcp-introduction", "mcp-tools"] }, "skills", "mcp-agent-rules" @@ -74,12 +63,7 @@ "errors-retrying", { "group": "Wait", - "pages": [ - "wait", - "wait-for", - "wait-until", - "wait-for-token" - ] + "pages": ["wait", "wait-for", "wait-until", "wait-for-token"] }, "queue-concurrency", "versioning", @@ -203,10 +187,7 @@ "deployment/atomic-deployment", { "group": "Deployment integrations", - "pages": [ - "github-integration", - "vercel-integration" - ] + "pages": ["github-integration", "vercel-integration"] } ] }, @@ -270,19 +251,11 @@ }, { "group": "Observability", - "pages": [ - "observability/query", - "observability/dashboards" - ] + "pages": ["observability/query", "observability/dashboards"] }, { "group": "Using the Dashboard", - "pages": [ - "run-tests", - "troubleshooting-alerts", - "replaying", - "bulk-actions" - ] + "pages": ["run-tests", "troubleshooting-alerts", "replaying", "bulk-actions"] }, { "group": "Troubleshooting", @@ -305,30 +278,18 @@ "self-hosting/kubernetes", { "group": "Environment variables", - "pages": [ - "self-hosting/env/webapp", - "self-hosting/env/supervisor" - ] + "pages": ["self-hosting/env/webapp", "self-hosting/env/supervisor"] }, "open-source-self-hosting" ] }, { "group": "Open source", - "pages": [ - "open-source-contributing", - "github-repo", - "changelog", - "roadmap" - ] + "pages": ["open-source-contributing", "github-repo", "changelog", "roadmap"] }, { "group": "Help", - "pages": [ - "community", - "help-slack", - "help-email" - ] + "pages": ["community", "help-slack", "help-email"] } ] }, @@ -471,9 +432,7 @@ "groups": [ { "group": "Introduction", - "pages": [ - "guides/introduction" - ] + "pages": ["guides/introduction"] }, { "group": "Frameworks", @@ -539,10 +498,7 @@ }, { "group": "Migration guides", - "pages": [ - "migration-mergent", - "migration-n8n" - ] + "pages": ["migration-mergent", "migration-n8n"] }, { "group": "Use cases", @@ -633,10 +589,7 @@ "href": "https://trigger.dev" }, "api": { - "openapi": [ - "openapi.yml", - "v3-openapi.yaml" - ], + "openapi": ["openapi.yml", "v3-openapi.yaml"], "playground": { "display": "simple" } diff --git a/docs/style.css b/docs/style.css index 94aea582db3..91cf9cd270c 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,31 +1,31 @@ -button~.absolute.peer-hover\:opacity-100 { - color: #000 +button ~ .absolute.peer-hover\:opacity-100 { + color: #000; } :root { - /* Code block colors - Trigger.dark theme */ - --mint-color-background: #121317; - --mint-color-text: #D4D4D4; - --mint-token-constant: #9B99FF; - --mint-token-string: #AFEC73; - --mint-token-comment: #5F6570; - --mint-token-keyword: #E888F8; - --mint-token-parameter: #CCCBFF; - --mint-token-function: #D9F07C; - --mint-token-string-expression: #AFEC73; - --mint-token-punctuation: #878C99; - --mint-token-link: #826DFF; + /* Code block colors - Trigger.dark theme */ + --mint-color-background: #121317; + --mint-color-text: #d4d4d4; + --mint-token-constant: #9b99ff; + --mint-token-string: #afec73; + --mint-token-comment: #5f6570; + --mint-token-keyword: #e888f8; + --mint-token-parameter: #cccbff; + --mint-token-function: #d9f07c; + --mint-token-string-expression: #afec73; + --mint-token-punctuation: #878c99; + --mint-token-link: #826dff; - /* Shiki css-variables fallbacks */ - --shiki-foreground: #D4D4D4; - --shiki-background: #121317; - --shiki-token-constant: #9B99FF; - --shiki-token-string: #AFEC73; - --shiki-token-comment: #5F6570; - --shiki-token-keyword: #E888F8; - --shiki-token-parameter: #CCCBFF; - --shiki-token-function: #D9F07C; - --shiki-token-string-expression: #AFEC73; - --shiki-token-punctuation: #878C99; - --shiki-token-link: #826DFF; + /* Shiki css-variables fallbacks */ + --shiki-foreground: #d4d4d4; + --shiki-background: #121317; + --shiki-token-constant: #9b99ff; + --shiki-token-string: #afec73; + --shiki-token-comment: #5f6570; + --shiki-token-keyword: #e888f8; + --shiki-token-parameter: #cccbff; + --shiki-token-function: #d9f07c; + --shiki-token-string-expression: #afec73; + --shiki-token-punctuation: #878c99; + --shiki-token-link: #826dff; } diff --git a/hosting/docker/webapp/docker-compose.yml b/hosting/docker/webapp/docker-compose.yml index d246babf953..7b2de751b7f 100644 --- a/hosting/docker/webapp/docker-compose.yml +++ b/hosting/docker/webapp/docker-compose.yml @@ -238,4 +238,4 @@ networks: supervisor: name: supervisor webapp: - name: webapp \ No newline at end of file + name: webapp diff --git a/hosting/docker/worker/docker-compose.yml b/hosting/docker/worker/docker-compose.yml index 6e9f4db2721..a69605949e4 100644 --- a/hosting/docker/worker/docker-compose.yml +++ b/hosting/docker/worker/docker-compose.yml @@ -45,7 +45,13 @@ services: DOCKER_REGISTRY_PASSWORD: ${DOCKER_REGISTRY_PASSWORD:-} DOCKER_AUTOREMOVE_EXITED_CONTAINERS: 0 healthcheck: - test: ["CMD", "node", "-e", "http.get('http://localhost:8020/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + test: + [ + "CMD", + "node", + "-e", + "http.get('http://localhost:8020/health', res => process.exit(res.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))", + ] interval: 30s timeout: 10s retries: 5 diff --git a/internal-packages/cache/package.json b/internal-packages/cache/package.json index 02ba86e1fd6..8d1bec36c77 100644 --- a/internal-packages/cache/package.json +++ b/internal-packages/cache/package.json @@ -18,4 +18,4 @@ "test": "vitest", "test:watch": "vitest" } -} \ No newline at end of file +} diff --git a/internal-packages/cache/src/stores/lruMemory.ts b/internal-packages/cache/src/stores/lruMemory.ts index 63b1d5cd4b5..5cd736dec5b 100644 --- a/internal-packages/cache/src/stores/lruMemory.ts +++ b/internal-packages/cache/src/stores/lruMemory.ts @@ -28,9 +28,10 @@ export type LRUMemoryStoreConfig = { * but will be evicted by LRU when the cache is full. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export class LRUMemoryStore - implements Store -{ +export class LRUMemoryStore implements Store< + TNamespace, + TValue +> { readonly name: string; private readonly cache: LRUCache>; @@ -110,10 +111,7 @@ export class LRUMemoryStore } } - async remove( - namespace: TNamespace, - keys: string | string[] - ): Promise> { + async remove(namespace: TNamespace, keys: string | string[]): Promise> { try { const keyArray = Array.isArray(keys) ? keys : [keys]; diff --git a/internal-packages/cache/src/stores/redis.ts b/internal-packages/cache/src/stores/redis.ts index 4f40133a4fc..456b5813cb7 100644 --- a/internal-packages/cache/src/stores/redis.ts +++ b/internal-packages/cache/src/stores/redis.ts @@ -9,9 +9,10 @@ export type RedisCacheStoreConfig = { useModernCacheKeyBuilder?: boolean; }; -export class RedisCacheStore - implements Store -{ +export class RedisCacheStore implements Store< + TNamespace, + TValue +> { public readonly name = "redis"; private readonly redis: Redis; diff --git a/internal-packages/clickhouse/src/client/client.ts b/internal-packages/clickhouse/src/client/client.ts index d281b60482c..29323d14182 100644 --- a/internal-packages/clickhouse/src/client/client.ts +++ b/internal-packages/clickhouse/src/client/client.ts @@ -518,7 +518,10 @@ export class ClickhouseClient implements ClickhouseReader, ClickhouseWriter { }; } - public queryFastStream, TParams extends Record>(req: { + public queryFastStream< + TOut extends Record, + TParams extends Record, + >(req: { name: string; query: string; columns: Array; diff --git a/internal-packages/clickhouse/src/client/noop.ts b/internal-packages/clickhouse/src/client/noop.ts index 9f212c3d16b..0e7f2416c1a 100644 --- a/internal-packages/clickhouse/src/client/noop.ts +++ b/internal-packages/clickhouse/src/client/noop.ts @@ -95,7 +95,10 @@ export class NoopClient implements ClickhouseReader, ClickhouseWriter { }; } - public queryFastStream, TParams extends Record>(req: { + public queryFastStream< + TOut extends Record, + TParams extends Record, + >(req: { name: string; query: string; columns: string[]; diff --git a/internal-packages/clickhouse/src/client/tsql.ts b/internal-packages/clickhouse/src/client/tsql.ts index 0a8367e7173..f3cf9067bef 100644 --- a/internal-packages/clickhouse/src/client/tsql.ts +++ b/internal-packages/clickhouse/src/client/tsql.ts @@ -15,7 +15,7 @@ import { type QuerySettings, type FieldMappings, type TimeRange, - type WhereClauseCondition + type WhereClauseCondition, } from "@internal/tsql"; import type { ClickhouseReader, QueryStats } from "./types.js"; import { QueryError } from "./errors.js"; @@ -180,9 +180,10 @@ export async function executeTSQL( try { // 1. Compile the TSQL query to ClickHouse SQL // Pass maxRows + 1 to fetch one extra row for overflow detection - const compiledSettings = maxRows !== undefined - ? { ...options.querySettings, maxRows: maxRows + 1 } - : options.querySettings; + const compiledSettings = + maxRows !== undefined + ? { ...options.querySettings, maxRows: maxRows + 1 } + : options.querySettings; const { sql, params, columns, hiddenColumns } = compileTSQL(options.query, { tableSchema: options.tableSchema, diff --git a/internal-packages/clickhouse/src/sessions.ts b/internal-packages/clickhouse/src/sessions.ts index 995e98447e0..bebb8b15492 100644 --- a/internal-packages/clickhouse/src/sessions.ts +++ b/internal-packages/clickhouse/src/sessions.ts @@ -157,10 +157,7 @@ export function getSessionsQueryBuilder(ch: ClickhouseReader, settings?: ClickHo }); } -export function getSessionsCountQueryBuilder( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSessionsCountQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSessionsCount", baseQuery: "SELECT count() as count FROM trigger_dev.sessions_v1 FINAL", @@ -175,10 +172,7 @@ export const SessionTagsQueryResult = z.object({ export type SessionTagsQueryResult = z.infer; -export function getSessionTagsQueryBuilder( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSessionTagsQueryBuilder(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSessionTags", baseQuery: "SELECT DISTINCT arrayJoin(tags) as tag FROM trigger_dev.sessions_v1", diff --git a/internal-packages/clickhouse/src/taskEvents.ts b/internal-packages/clickhouse/src/taskEvents.ts index e8d4c2bd95f..4c17d5068d0 100644 --- a/internal-packages/clickhouse/src/taskEvents.ts +++ b/internal-packages/clickhouse/src/taskEvents.ts @@ -213,10 +213,7 @@ export function insertTaskEventsV2(ch: ClickhouseWriter, settings?: ClickHouseSe }); } -export function getTraceSummaryQueryBuilderV2( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getTraceSummaryQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilderFast({ name: "getTraceEventsV2", table: "trigger_dev.task_events_v2", @@ -258,10 +255,7 @@ export function getTraceDetailedSummaryQueryBuilderV2( }); } -export function getSpanDetailsQueryBuilderV2( - ch: ClickhouseReader, - settings?: ClickHouseSettings -) { +export function getSpanDetailsQueryBuilderV2(ch: ClickhouseReader, settings?: ClickHouseSettings) { return ch.queryBuilder({ name: "getSpanDetailsV2", baseQuery: @@ -283,7 +277,6 @@ export function getTraceEventsForExportQueryBuilderV2( }); } - // ============================================================================ // Search Table Query Builders (for logs page, using task_events_search_v1) // ============================================================================ @@ -350,7 +343,7 @@ export const LogDetailV2Result = z.object({ kind: z.string(), status: z.string(), duration: z.number().or(z.string()), - attributes_text: z.string() + attributes_text: z.string(), }); export type LogDetailV2Result = z.output; @@ -376,4 +369,4 @@ export function getLogDetailQueryBuilderV2(ch: ClickhouseReader) { "attributes_text", ], }); -} \ No newline at end of file +} diff --git a/internal-packages/clickhouse/src/taskRuns.test.ts b/internal-packages/clickhouse/src/taskRuns.test.ts index fb64d458b1c..0d4ec995c24 100644 --- a/internal-packages/clickhouse/src/taskRuns.test.ts +++ b/internal-packages/clickhouse/src/taskRuns.test.ts @@ -556,9 +556,7 @@ describe("Task Runs V2", () => { null, ]; - const childA_v2: TaskRunInsertArray = [ - ...childA_v1, - ]; + const childA_v2: TaskRunInsertArray = [...childA_v1]; childA_v2[TASK_RUN_INDEX.status] = "COMPLETED_SUCCESSFULLY"; childA_v2[TASK_RUN_INDEX._version] = "2"; @@ -676,24 +674,18 @@ describe("Task Runs V2", () => { null, ]; - const childDeleted_v2: TaskRunInsertArray = [ - ...childDeleted_v1, - ]; + const childDeleted_v2: TaskRunInsertArray = [...childDeleted_v1]; childDeleted_v2[TASK_RUN_INDEX._version] = "2"; childDeleted_v2[TASK_RUN_INDEX._is_deleted] = 1; - const childWrongRoot: TaskRunInsertArray = [ - ...childB, - ]; + const childWrongRoot: TaskRunInsertArray = [...childB]; childWrongRoot[TASK_RUN_INDEX.run_id] = "child_wrong_root"; childWrongRoot[TASK_RUN_INDEX.friendly_id] = "run_child_wrong_root"; childWrongRoot[TASK_RUN_INDEX.root_run_id] = "other_root"; childWrongRoot[TASK_RUN_INDEX.parent_run_id] = "other_root"; childWrongRoot[TASK_RUN_INDEX.span_id] = "span_child_wrong_root"; - const childOld: TaskRunInsertArray = [ - ...childB, - ]; + const childOld: TaskRunInsertArray = [...childB]; childOld[TASK_RUN_INDEX.run_id] = "child_old"; childOld[TASK_RUN_INDEX.created_at] = oldCreatedAt; childOld[TASK_RUN_INDEX.updated_at] = oldCreatedAt; diff --git a/internal-packages/clickhouse/src/tsql.test.ts b/internal-packages/clickhouse/src/tsql.test.ts index 5c1d6ed8022..cb30d5e85c1 100644 --- a/internal-packages/clickhouse/src/tsql.test.ts +++ b/internal-packages/clickhouse/src/tsql.test.ts @@ -1563,52 +1563,49 @@ describe("Field Mapping Tests", () => { } ); - clickhouseTest( - "should handle field mapping with IN clause", - async ({ clickhouseContainer }) => { - const client = new ClickhouseClient({ - name: "test", - url: clickhouseContainer.getConnectionUrl(), - }); + clickhouseTest("should handle field mapping with IN clause", async ({ clickhouseContainer }) => { + const client = new ClickhouseClient({ + name: "test", + url: clickhouseContainer.getConnectionUrl(), + }); - const insert = insertTaskRuns(client, { async_insert: 0 }); + const insert = insertTaskRuns(client, { async_insert: 0 }); - // Insert test runs with different projects - await insert([ - createTaskRun({ - run_id: "run_fm_in1", - project_id: "proj_tenant1", - status: "COMPLETED_SUCCESSFULLY", - }), - createTaskRun({ - run_id: "run_fm_in2", - project_id: "proj_other", - organization_id: "org_tenant1", - status: "PENDING", - }), - ]); + // Insert test runs with different projects + await insert([ + createTaskRun({ + run_id: "run_fm_in1", + project_id: "proj_tenant1", + status: "COMPLETED_SUCCESSFULLY", + }), + createTaskRun({ + run_id: "run_fm_in2", + project_id: "proj_other", + organization_id: "org_tenant1", + status: "PENDING", + }), + ]); - // Query using IN clause with external project_ref values - const [error, result] = await executeTSQL(client, { - name: "test-field-mapping-in", - query: - "SELECT run_id FROM task_runs WHERE project_ref IN ('my-project-ref', 'other-project')", - schema: z.object({ run_id: z.string() }), - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_tenant1" }, - }, - tableSchema: [fieldMappingSchema], - fieldMappings: { - project: { - proj_tenant1: "my-project-ref", - proj_other: "other-project", - }, + // Query using IN clause with external project_ref values + const [error, result] = await executeTSQL(client, { + name: "test-field-mapping-in", + query: + "SELECT run_id FROM task_runs WHERE project_ref IN ('my-project-ref', 'other-project')", + schema: z.object({ run_id: z.string() }), + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_tenant1" }, + }, + tableSchema: [fieldMappingSchema], + fieldMappings: { + project: { + proj_tenant1: "my-project-ref", + proj_other: "other-project", }, - }); + }, + }); - expect(error).toBeNull(); - expect(result?.rows).toHaveLength(2); - expect(result?.rows?.map((r) => r.run_id).sort()).toEqual(["run_fm_in1", "run_fm_in2"]); - } - ); + expect(error).toBeNull(); + expect(result?.rows).toHaveLength(2); + expect(result?.rows?.map((r) => r.run_id).sort()).toEqual(["run_fm_in1", "run_fm_in2"]); + }); }); diff --git a/internal-packages/compute/src/client.ts b/internal-packages/compute/src/client.ts index d6215678b53..bfcf5f782ac 100644 --- a/internal-packages/compute/src/client.ts +++ b/internal-packages/compute/src/client.ts @@ -62,7 +62,11 @@ class HttpTransport { return options?.signal ?? AbortSignal.timeout(this.opts.timeoutMs); } - async post(path: string, body: unknown, options?: RequestOptions): Promise { + async post( + path: string, + body: unknown, + options?: RequestOptions + ): Promise { const url = `${this.opts.gatewayUrl}${path}`; const response = await fetch(url, { diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json b/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json index c60365cd592..9ff7afa4f6d 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/0000_snapshot.json @@ -175,4 +175,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json b/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json index 5846087eb2d..57f716b9512 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/0001_snapshot.json @@ -303,10 +303,7 @@ "compositePrimaryKeys": { "chat_turn_evals_chat_id_turn_pk": { "name": "chat_turn_evals_chat_id_turn_pk", - "columns": [ - "chat_id", - "turn" - ] + "columns": ["chat_id", "turn"] } }, "uniqueConstraints": {}, @@ -441,4 +438,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json b/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json index a3e444293e7..62516c1d0e4 100644 --- a/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json +++ b/internal-packages/dashboard-agent-db/drizzle/meta/_journal.json @@ -17,4 +17,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/internal-packages/dashboard-agent-db/migrate-status.mjs b/internal-packages/dashboard-agent-db/migrate-status.mjs index 431876aaf34..8b3791a06b3 100644 --- a/internal-packages/dashboard-agent-db/migrate-status.mjs +++ b/internal-packages/dashboard-agent-db/migrate-status.mjs @@ -33,10 +33,7 @@ function normalizeConnectionString(value) { } } -const journalPath = join( - dirname(fileURLToPath(import.meta.url)), - "drizzle/meta/_journal.json" -); +const journalPath = join(dirname(fileURLToPath(import.meta.url)), "drizzle/meta/_journal.json"); const sql = postgres(normalizeConnectionString(connectionString), { max: 1, prepare: false, @@ -59,9 +56,7 @@ async function main() { } const pending = entries.filter((e) => e.when > lastAppliedAt); - console.log( - `${entries.length} migration(s) found, ${entries.length - pending.length} applied` - ); + console.log(`${entries.length} migration(s) found, ${entries.length - pending.length} applied`); if (pending.length > 0) { console.log(`${pending.length} pending migration(s):`); diff --git a/internal-packages/dashboard-agent-db/src/queries.ts b/internal-packages/dashboard-agent-db/src/queries.ts index a5cbe4ffa1a..ca9ba1de54b 100644 --- a/internal-packages/dashboard-agent-db/src/queries.ts +++ b/internal-packages/dashboard-agent-db/src/queries.ts @@ -1,6 +1,12 @@ import { and, desc, eq, isNull, sql } from "drizzle-orm"; import type { DashboardAgentDb } from "./client.js"; -import { chats, chatSessions, chatTurnEvals, type ChatSession, type NewChatTurnEval } from "./schema.js"; +import { + chats, + chatSessions, + chatTurnEvals, + type ChatSession, + type NewChatTurnEval, +} from "./schema.js"; /** * The access-pattern layer. Every query that touches user data is scoped by @@ -64,11 +70,7 @@ export async function getChatMessages( .select({ messages: chats.messages }) .from(chats) .where( - and( - eq(chats.id, params.chatId), - eq(chats.userId, params.userId), - isNull(chats.deletedAt) - ) + and(eq(chats.id, params.chatId), eq(chats.userId, params.userId), isNull(chats.deletedAt)) ) .limit(1); return rows[0]?.messages ?? null; @@ -175,11 +177,7 @@ export async function setChatTitleIfDefault( .update(chats) .set({ title: params.title, updatedAt: sql`now()` }) .where( - and( - eq(chats.id, params.chatId), - eq(chats.title, DEFAULT_CHAT_TITLE), - isNull(chats.deletedAt) - ) + and(eq(chats.id, params.chatId), eq(chats.title, DEFAULT_CHAT_TITLE), isNull(chats.deletedAt)) ); } diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts b/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts index d02628dbe38..827066482fb 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.eval.ts @@ -195,7 +195,12 @@ const Verdict = z.object({ .min(1) .max(5) .describe("Does the answer directly address the user's question? 5 = fully, 1 = not at all."), - concise: z.number().int().min(1).max(5).describe("Direct and free of padding. Do not reward length."), + concise: z + .number() + .int() + .min(1) + .max(5) + .describe("Direct and free of padding. Do not reward length."), }); const JUDGE_SYSTEM = [ @@ -246,48 +251,43 @@ const TOOL_CASES: Array<{ question: string; expect: string }> = [ const TOOL_SELECTION_THRESHOLD = 0.83; // tolerate ~2/12 misses; a trend reds the suite describe.skipIf(!HAS_KEY)("dashboardAgent evals (real model)", () => { - it( - "tool selection: picks the right tool for the question", - async () => { - const results: Array<{ question: string; expected: string; got: string; ok: boolean }> = []; - for (const c of TOOL_CASES) { - const { calls } = await runCase(c.question); - const got = calls[0]?.tool ?? "(none)"; - results.push({ question: c.question, expected: c.expect, got, ok: got === c.expect }); - } + it("tool selection: picks the right tool for the question", async () => { + const results: Array<{ question: string; expected: string; got: string; ok: boolean }> = []; + for (const c of TOOL_CASES) { + const { calls } = await runCase(c.question); + const got = calls[0]?.tool ?? "(none)"; + results.push({ question: c.question, expected: c.expect, got, ok: got === c.expect }); + } - const passed = results.filter((r) => r.ok).length; - const rate = passed / results.length; - // Surface the full table so a failing case is diagnosable, not just a number. - // process.stdout.write (not console.log) so it survives vitest's console intercept. - process.stdout.write( - `\ntool selection: ${passed}/${results.length} (${(rate * 100).toFixed(0)}%)\n` + - results - .map((r) => ` ${r.ok ? "PASS" : "FAIL"} ${r.got.padEnd(18)} (want ${r.expected}) ${r.question}`) - .join("\n") + - "\n" - ); + const passed = results.filter((r) => r.ok).length; + const rate = passed / results.length; + // Surface the full table so a failing case is diagnosable, not just a number. + // process.stdout.write (not console.log) so it survives vitest's console intercept. + process.stdout.write( + `\ntool selection: ${passed}/${results.length} (${(rate * 100).toFixed(0)}%)\n` + + results + .map( + (r) => + ` ${r.ok ? "PASS" : "FAIL"} ${r.got.padEnd(18)} (want ${r.expected}) ${r.question}` + ) + .join("\n") + + "\n" + ); - expect(rate).toBeGreaterThanOrEqual(TOOL_SELECTION_THRESHOLD); - }, - 180_000 - ); + expect(rate).toBeGreaterThanOrEqual(TOOL_SELECTION_THRESHOLD); + }, 180_000); - it( - "answer quality: grounded and on-question (LLM judge)", - async () => { - const question = "What errors are happening in this environment? Summarize the top ones."; - const { calls, answer } = await runCase(question); + it("answer quality: grounded and on-question (LLM judge)", async () => { + const question = "What errors are happening in this environment? Summarize the top ones."; + const { calls, answer } = await runCase(question); - expect(calls[0]?.tool).toBe("list_errors"); - expect(answer.length).toBeGreaterThan(0); + expect(calls[0]?.tool).toBe("list_errors"); + expect(answer.length).toBeGreaterThan(0); - const verdict = await judge({ question, toolData: FIXTURES.list_errors, answer }); - process.stdout.write(`\nanswer:\n${answer}\n\njudge: ${JSON.stringify(verdict)}\n`); + const verdict = await judge({ question, toolData: FIXTURES.list_errors, answer }); + process.stdout.write(`\nanswer:\n${answer}\n\njudge: ${JSON.stringify(verdict)}\n`); - expect(verdict.grounded).toBeGreaterThanOrEqual(4); - expect(verdict.answersQuestion).toBeGreaterThanOrEqual(4); - }, - 120_000 - ); + expect(verdict.grounded).toBeGreaterThanOrEqual(4); + expect(verdict.answersQuestion).toBeGreaterThanOrEqual(4); + }, 120_000); }); diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.test.ts b/internal-packages/dashboard-agent/src/dashboard-agent.test.ts index ad2f09da3d0..db23d2130f9 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.test.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.test.ts @@ -302,7 +302,9 @@ describe("buildDashboardAgentTools", () => { category: "user_code_error", likelyCause: "processOrder throws when items is empty.", confidence: "high", - evidence: [{ type: "error", detail: "Error: order has no items", reference: "run_abc123" }], + evidence: [ + { type: "error", detail: "Error: order has no items", reference: "run_abc123" }, + ], nextSteps: ["Validate the payload before triggering."], }, ], diff --git a/internal-packages/dashboard-agent/src/dashboard-agent.ts b/internal-packages/dashboard-agent/src/dashboard-agent.ts index 4639e7095c7..ce1919e68e9 100644 --- a/internal-packages/dashboard-agent/src/dashboard-agent.ts +++ b/internal-packages/dashboard-agent/src/dashboard-agent.ts @@ -45,8 +45,7 @@ let dbClient: DashboardAgentDbClient | undefined; function getDb(): DashboardAgentDbClient { if (!dbClient) { - const connectionString = - process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; + const connectionString = process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; if (!connectionString) { throw new Error( "DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the dashboard agent" @@ -182,7 +181,9 @@ async function generateAndSaveTitle( const { text } = await generateText({ model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel((resolved.model ?? "anthropic:claude-haiku-4-5") as `anthropic:${string}`), + registry.languageModel( + (resolved.model ?? "anthropic:claude-haiku-4-5") as `anthropic:${string}` + ), system: resolved.text, prompt: userText, ...resolved.toAISDKTelemetry(), @@ -373,7 +374,9 @@ export const dashboardAgent = chat.agent({ // prompt's model through the provider registry. model: locals.get(dashboardAgentModelKey) ?? - registry.languageModel((resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}`), + registry.languageModel( + (resolved.model ?? "anthropic:claude-sonnet-4-6") as `anthropic:${string}` + ), messages, abortSignal: signal, // toStreamTextOptions() defaults to a single step; override so the model diff --git a/internal-packages/dashboard-agent/src/eval-turn.ts b/internal-packages/dashboard-agent/src/eval-turn.ts index ef86e7cdc4c..3943c7726c4 100644 --- a/internal-packages/dashboard-agent/src/eval-turn.ts +++ b/internal-packages/dashboard-agent/src/eval-turn.ts @@ -25,10 +25,11 @@ const JUDGE_MODEL = "claude-sonnet-4-6"; let dbClient: DashboardAgentDbClient | undefined; function getEvalDb(): DashboardAgentDbClient { if (!dbClient) { - const connectionString = - process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; + const connectionString = process.env.DASHBOARD_AGENT_DATABASE_URL ?? process.env.DATABASE_URL; if (!connectionString) { - throw new Error("DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the eval task"); + throw new Error( + "DASHBOARD_AGENT_DATABASE_URL (or DATABASE_URL) must be set for the eval task" + ); } dbClient = createDashboardAgentDb(connectionString, { max: 2 }); } @@ -77,8 +78,15 @@ const TurnEval = z.object({ .int() .min(1) .max(5) - .describe("Does the answer use only facts from the tool results? Penalize invented ids/counts/status. 5 = fully grounded."), - answered: z.number().int().min(1).max(5).describe("Does it directly answer the question? 5 = fully."), + .describe( + "Does the answer use only facts from the tool results? Penalize invented ids/counts/status. 5 = fully grounded." + ), + answered: z + .number() + .int() + .min(1) + .max(5) + .describe("Does it directly answer the question? 5 = fully."), concise: z.number().int().min(1).max(5).describe("Direct, no padding. Do not reward length."), // Insight classification. intentCategory: z @@ -86,17 +94,25 @@ const TurnEval = z.object({ .describe("What the user was trying to do."), outcome: z .enum(["resolved", "partial", "unresolved", "deflected"]) - .describe("Did the agent actually help? deflected = sent the user elsewhere without answering."), + .describe( + "Did the agent actually help? deflected = sent the user elsewhere without answering." + ), sentiment: z.enum(["positive", "neutral", "negative", "frustrated"]), capabilityGap: z .boolean() .describe("The agent lacked a tool, data, or permission needed to fully help."), - docsGap: z.boolean().describe("A how-to the agent answered weakly or that better docs would solve."), + docsGap: z + .boolean() + .describe("A how-to the agent answered weakly or that better docs would solve."), supportOpportunity: z .boolean() - .describe("The user seems stuck, blocked, or frustrated and would benefit from a human follow-up."), + .describe( + "The user seems stuck, blocked, or frustrated and would benefit from a human follow-up." + ), featureRequest: z.boolean().describe("The user wants something the product does not do."), - topics: z.array(z.string()).describe("1-3 short topic tags, e.g. 'concurrency', 'failed deploys'."), + topics: z + .array(z.string()) + .describe("1-3 short topic tags, e.g. 'concurrency', 'failed deploys'."), signals: z .array( z.object({ diff --git a/internal-packages/dashboard-agent/src/prompts.ts b/internal-packages/dashboard-agent/src/prompts.ts index 1f0a152599d..3e8a46dec9b 100644 --- a/internal-packages/dashboard-agent/src/prompts.ts +++ b/internal-packages/dashboard-agent/src/prompts.ts @@ -28,7 +28,8 @@ export const systemPrompt = prompts.define({ // repo, so the agent has the source-reading tools too. export const codeSystemPrompt = prompts.define({ id: "dashboard-agent-system-code", - description: "System prompt for the in-dashboard agent when the project's GitHub repo is connected.", + description: + "System prompt for the in-dashboard agent when the project's GitHub repo is connected.", model: `anthropic:${DASHBOARD_AGENT_MODEL}`, content: DASHBOARD_AGENT_CODE_SYSTEM_PROMPT, }); diff --git a/internal-packages/dashboard-agent/src/repo-tools.test.ts b/internal-packages/dashboard-agent/src/repo-tools.test.ts index d00018ed710..d56d15580a6 100644 --- a/internal-packages/dashboard-agent/src/repo-tools.test.ts +++ b/internal-packages/dashboard-agent/src/repo-tools.test.ts @@ -67,7 +67,12 @@ afterAll(async () => { describe("repo-tools", () => { it("get_repo_info returns the connected repo and pinned commit", async () => { const res = await call(tools.get_repo_info, {}); - expect(res).toEqual({ owner: "acme", repo: "demo", sha: "deadbeefdeadbeef", defaultBranch: "main" }); + expect(res).toEqual({ + owner: "acme", + repo: "demo", + sha: "deadbeefdeadbeef", + defaultBranch: "main", + }); }); it("read_file reads a file from the workspace", async () => { @@ -78,7 +83,11 @@ describe("repo-tools", () => { }); it("read_file honors a line range", async () => { - const res: any = await call(tools.read_file, { path: "src/trigger/order.ts", startLine: 2, endLine: 2 }); + const res: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + startLine: 2, + endLine: 2, + }); expect(res.content).toBe("const LIMIT = 10000;"); expect(res.startLine).toBe(2); expect(res.endLine).toBe(2); @@ -99,7 +108,10 @@ describe("repo-tools", () => { it("read_file with runId reads the run's pinned commit", async () => { const def: any = await call(tools.read_file, { path: "src/trigger/order.ts" }); expect(def.content).toContain("const LIMIT = 10000;"); - const pinned: any = await call(tools.read_file, { path: "src/trigger/order.ts", runId: "run_pinned" }); + const pinned: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + runId: "run_pinned", + }); expect(pinned.error).toBeUndefined(); expect(pinned.content).toContain("const LIMIT = 5000;"); }); @@ -110,14 +122,19 @@ describe("repo-tools", () => { }); it("read_file with an unresolvable runId errors instead of falling back", async () => { - const res: any = await call(tools.read_file, { path: "src/trigger/order.ts", runId: "run_unknown" }); + const res: any = await call(tools.read_file, { + path: "src/trigger/order.ts", + runId: "run_unknown", + }); expect(res.error).toMatch(/Couldn't resolve the source/); }); it.runIf(hasRg)("search_code finds a match (and does not hang on stdin)", async () => { const res: any = await call(tools.search_code, { query: "const LIMIT" }); expect(res.error).toBeUndefined(); - expect(res.matches.some((m: any) => String(m.file).includes("order.ts") && /LIMIT/.test(m.text))).toBe(true); + expect( + res.matches.some((m: any) => String(m.file).includes("order.ts") && /LIMIT/.test(m.text)) + ).toBe(true); }); it.runIf(hasRg)("list_files lists workspace files", async () => { diff --git a/internal-packages/dashboard-agent/src/repo-tools.ts b/internal-packages/dashboard-agent/src/repo-tools.ts index 552996e9d9f..910f60df512 100644 --- a/internal-packages/dashboard-agent/src/repo-tools.ts +++ b/internal-packages/dashboard-agent/src/repo-tools.ts @@ -86,7 +86,8 @@ async function ensureWorkspace(snapshot: RepoSnapshot): Promise { const length = Number(res.headers.get("content-length") ?? 0); if (length > MAX_ARCHIVE_BYTES) throw new Error(`archive too large (${length} bytes)`); const bytes = new Uint8Array(await res.arrayBuffer()); - if (bytes.length > MAX_ARCHIVE_BYTES) throw new Error(`archive too large (${bytes.length} bytes)`); + if (bytes.length > MAX_ARCHIVE_BYTES) + throw new Error(`archive too large (${bytes.length} bytes)`); const scratch = await mkdtemp(join(tmpdir(), "dashboard-agent-tar-")); tarPath = join(scratch, "repo.tar.gz"); @@ -149,7 +150,8 @@ export function buildRepoTools( // (resolved server-side), otherwise the default tracked-branch snapshot. async function snapshotFor(runId?: string): Promise { if (!runId) return defaultSnapshot; - if (!resolveRunSnapshot) return { error: "Reading a specific run's source isn't available here." }; + if (!resolveRunSnapshot) + return { error: "Reading a specific run's source isn't available here." }; const snap = await resolveRunSnapshot(runId); return ( snap ?? { @@ -177,7 +179,12 @@ export function buildRepoTools( execute: async ({ runId }) => { const snap = await snapshotFor(runId); if ("error" in snap) return snap; - return { owner: snap.owner, repo: snap.repo, sha: snap.sha, defaultBranch: snap.defaultBranch }; + return { + owner: snap.owner, + repo: snap.repo, + sha: snap.sha, + defaultBranch: snap.defaultBranch, + }; }, }), @@ -199,8 +206,14 @@ export function buildRepoTools( const cwd = realSub ?? sub; try { const { stdout } = await execFileAsync("rg", args, { cwd, maxBuffer: 16 * 1024 * 1024 }); - const files = stdout.split("\n").filter(Boolean).map((f) => relative(workdir, resolve(cwd, f))); - return { files: files.slice(0, MAX_LIST_FILES), truncated: files.length > MAX_LIST_FILES }; + const files = stdout + .split("\n") + .filter(Boolean) + .map((f) => relative(workdir, resolve(cwd, f))); + return { + files: files.slice(0, MAX_LIST_FILES), + truncated: files.length > MAX_LIST_FILES, + }; } catch (error) { // rg exits 1 when there are no matches; treat as empty, not an error. if ((error as { code?: number }).code === 1) return { files: [], truncated: false }; @@ -256,14 +269,19 @@ export function buildRepoTools( // in a spawned process) and blocks forever. The "." makes it search files. args.push("-e", query, "."); try { - const { stdout } = await execFileAsync("rg", args, { cwd: workdir, maxBuffer: 16 * 1024 * 1024 }); + const { stdout } = await execFileAsync("rg", args, { + cwd: workdir, + maxBuffer: 16 * 1024 * 1024, + }); const matches = stdout .split("\n") .filter(Boolean) .slice(0, cap) .map((line) => { const m = line.match(/^([^:]+):(\d+):(.*)$/); - return m ? { file: m[1], line: Number(m[2]), text: m[3].slice(0, 300) } : { text: line }; + return m + ? { file: m[1], line: Number(m[2]), text: m[3].slice(0, 300) } + : { text: line }; }); return { matches, truncated: matches.length >= cap }; } catch (error) { diff --git a/internal-packages/dashboard-agent/src/tool-schemas.ts b/internal-packages/dashboard-agent/src/tool-schemas.ts index 8117e3c563d..65a7c0c60a4 100644 --- a/internal-packages/dashboard-agent/src/tool-schemas.ts +++ b/internal-packages/dashboard-agent/src/tool-schemas.ts @@ -48,12 +48,20 @@ export const listRunsSchema = tool({ errorId: z .string() .optional() - .describe("Only runs that hit this error group (an error_... id from list_errors/get_error)."), + .describe( + "Only runs that hit this error group (an error_... id from list_errors/get_error)." + ), period: z .string() .optional() .describe("Relative window, e.g. 1h, 24h, 7d. Max 30d; larger values are capped at 30d."), - limit: z.number().int().positive().max(50).optional().describe("Max runs to return (default 10)."), + limit: z + .number() + .int() + .positive() + .max(50) + .optional() + .describe("Max runs to return (default 10)."), }), }); @@ -119,7 +127,9 @@ export const getQuerySchemaSchema = tool({ table: z .string() .optional() - .describe("A table name (e.g. 'runs') to get its columns. Omit to list the available tables."), + .describe( + "A table name (e.g. 'runs') to get its columns. Omit to list the available tables." + ), }), }); @@ -129,11 +139,15 @@ export const runQuerySchema = tool({ inputSchema: z.object({ query: z .string() - .describe("The TRQL query. A read-only SELECT over runs / metrics / llm_metrics / llm_models."), + .describe( + "The TRQL query. A read-only SELECT over runs / metrics / llm_metrics / llm_models." + ), period: z .string() .optional() - .describe("Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column."), + .describe( + "Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column." + ), }), }); @@ -141,7 +155,9 @@ export const askSupportSchema = tool({ description: "Ask the Trigger.dev support assistant a question about how Trigger.dev works: docs, concepts, features, configuration, best practices, and troubleshooting how-tos (e.g. 'how do retries work?', 'how do I set a concurrency limit?', 'does Trigger.dev support cron schedules?'). Use this for product/knowledge questions, NOT for the user's own runs, errors, or data (use the read and query tools for those). Returns a composed answer.", inputSchema: z.object({ - question: z.string().describe("The user's question about how Trigger.dev works, in natural language."), + question: z + .string() + .describe("The user's question about how Trigger.dev works, in natural language."), }), }); @@ -183,7 +199,9 @@ export const diagnosisBlockSchema = z.object({ .describe("Your classification of the root cause."), likelyCause: z .string() - .describe("The most probable root cause, in specific terms — name the code, config, or dependency."), + .describe( + "The most probable root cause, in specific terms — name the code, config, or dependency." + ), confidence: z .enum(["high", "medium", "low"]) .describe("How confident you are in this diagnosis given the evidence. Be honest."), @@ -208,7 +226,9 @@ export const diagnosisBlockSchema = z.object({ ), }) ) - .describe("The concrete signals behind the diagnosis. Cite real ids, spans, versions, or file:line."), + .describe( + "The concrete signals behind the diagnosis. Cite real ids, spans, versions, or file:line." + ), impact: z .string() .optional() @@ -220,7 +240,9 @@ export const diagnosisBlockSchema = z.object({ label: z.string().describe("Button text, e.g. 'View run' or 'Read the retries docs'."), kind: z .enum(["view_run", "docs"]) - .describe("view_run links to a run page in this environment; docs opens an external URL."), + .describe( + "view_run links to a run page in this environment; docs opens an external URL." + ), target: z.string().describe("For view_run: a run id (run_...). For docs: an https URL."), }) ) @@ -244,13 +266,19 @@ export const chartBlockSchema = z.object({ period: z .string() .optional() - .describe("Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column."), + .describe( + "Time window shorthand like '24h', '7d', '30d' (max 30d), applied to the table's time column." + ), chartType: z .enum(["line", "bar"]) - .describe("line for trends over time, bar for comparing categories. Stack with `stacked` for composition."), + .describe( + "line for trends over time, bar for comparing categories. Stack with `stacked` for composition." + ), xAxisColumn: z .string() - .describe("The result column for the x-axis: a time bucket (for line) or a category (for bar)."), + .describe( + "The result column for the x-axis: a time bucket (for line) or a category (for bar)." + ), yAxisColumns: z .array(z.string()) .min(1) @@ -258,15 +286,23 @@ export const chartBlockSchema = z.object({ groupByColumn: z .string() .nullish() - .describe("Optional result column to split a single yAxisColumn into one series per distinct value."), - stacked: z.boolean().optional().describe("Stack the series (cumulative/composition). Default false."), + .describe( + "Optional result column to split a single yAxisColumn into one series per distinct value." + ), + stacked: z + .boolean() + .optional() + .describe("Stack the series (cumulative/composition). Default false."), aggregation: z .enum(["sum", "avg", "count", "min", "max"]) .optional() .describe("How to combine values that share an x point. Default sum."), }); -export const viewBlockSchema = z.discriminatedUnion("type", [diagnosisBlockSchema, chartBlockSchema]); +export const viewBlockSchema = z.discriminatedUnion("type", [ + diagnosisBlockSchema, + chartBlockSchema, +]); export type DiagnosisBlock = z.infer; export type ChartBlock = z.infer; @@ -303,7 +339,10 @@ export const listFilesSchema = tool({ "List source files in the connected repository (respecting .gitignore). Optionally filter by a glob like '**/*.ts' or scope to a subdirectory. Use this to find where something lives before reading it.", inputSchema: z.object({ glob: z.string().optional().describe("Glob filter, e.g. 'src/**/*.ts' or '*.json'."), - path: z.string().optional().describe("Subdirectory (relative to repo root) to scope the listing to."), + path: z + .string() + .optional() + .describe("Subdirectory (relative to repo root) to scope the listing to."), runId: runIdField, }), }); @@ -312,7 +351,9 @@ export const readFileSchema = tool({ description: "Read a file from the connected repository by its path relative to the repo root. Optionally restrict to a line range. Use this to read the actual task source behind a run or error.", inputSchema: z.object({ - path: z.string().describe("File path relative to the repo root, e.g. src/trigger/processOrder.ts."), + path: z + .string() + .describe("File path relative to the repo root, e.g. src/trigger/processOrder.ts."), startLine: z.number().int().positive().optional().describe("First line to include (1-based)."), endLine: z.number().int().positive().optional().describe("Last line to include (1-based)."), runId: runIdField, @@ -325,7 +366,13 @@ export const searchCodeSchema = tool({ inputSchema: z.object({ query: z.string().describe("The ripgrep pattern to search for."), glob: z.string().optional().describe("Restrict the search to files matching this glob."), - maxResults: z.number().int().positive().max(80).optional().describe("Max matches to return (default 40)."), + maxResults: z + .number() + .int() + .positive() + .max(80) + .optional() + .describe("Max matches to return (default 40)."), runId: runIdField, }), }); diff --git a/internal-packages/dashboard-agent/src/tools.ts b/internal-packages/dashboard-agent/src/tools.ts index 16988cde666..4106ef03eef 100644 --- a/internal-packages/dashboard-agent/src/tools.ts +++ b/internal-packages/dashboard-agent/src/tools.ts @@ -174,7 +174,11 @@ function curateTrace(data: unknown) { for (const child of span.children ?? []) walk(child, depth + 1); }; walk(root, 0); - return { traceId: (data as any)?.trace?.traceId, spans, truncated: spans.length >= MAX_TRACE_SPANS }; + return { + traceId: (data as any)?.trace?.traceId, + spans, + truncated: spans.length >= MAX_TRACE_SPANS, + }; } function curateErrors(data: unknown) { @@ -260,7 +264,13 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!result.ok) return null; const d = result.data as Partial | undefined; if (!d?.tarballUrl || !d.owner || !d.repo || !d.sha) return null; - return { tarballUrl: d.tarballUrl, owner: d.owner, repo: d.repo, sha: d.sha, defaultBranch: d.defaultBranch }; + return { + tarballUrl: d.tarballUrl, + owner: d.owner, + repo: d.repo, + sha: d.sha, + defaultBranch: d.defaultBranch, + }; }; const apiTools: ToolSet = { @@ -280,7 +290,11 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!hasAuth) return NO_AUTH; const ref = inputRef ?? projectRef; if (!ref) return { error: "No project ref available. Ask the user which project." }; - const result = await apiGet(origin, `/api/v1/projects/${ref}/environments`, userActorToken!); + const result = await apiGet( + origin, + `/api/v1/projects/${ref}/environments`, + userActorToken! + ); if (!result.ok) return { error: `Couldn't list environments (status ${result.status}).` }; return curateEnvironments(result.data); }, @@ -340,7 +354,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to read runs from." }; const result = await apiGet(origin, `/api/v1/runs/${runId}/trace`, envJwt); - if (!result.ok) return { error: `Couldn't get the trace for ${runId} (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't get the trace for ${runId} (status ${result.status}).` }; return curateTrace(result.data); }, }), @@ -368,7 +383,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to read errors from." }; const result = await apiGet(origin, `/api/v1/errors/${errorId}`, envJwt); - if (!result.ok) return { error: `Couldn't get error ${errorId} (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't get error ${errorId} (status ${result.status}).` }; return curateError(result.data); }, }), @@ -379,7 +395,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe const envJwt = await getEnvJwt(); if (!envJwt) return { error: "No current environment is available to query." }; const result = await apiGet(origin, "/api/v1/query/schema", envJwt); - if (!result.ok) return { error: `Couldn't load the query schema (status ${result.status}).` }; + if (!result.ok) + return { error: `Couldn't load the query schema (status ${result.status}).` }; const tables = ((result.data as { tables?: any[] })?.tables ?? []) as any[]; // No table → list what's queryable; a table → its columns. if (!table) { @@ -393,7 +410,9 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe } const match = tables.find((t) => t.name === table); if (!match) { - return { error: `Unknown table "${table}". Available: ${tables.map((t) => t.name).join(", ")}.` }; + return { + error: `Unknown table "${table}". Available: ${tables.map((t) => t.name).join(", ")}.`, + }; } return { name: match.name, @@ -433,7 +452,9 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe // the model can fix the query rather than the turn dying. const data = (await res.json().catch(() => ({}))) as { results?: unknown; error?: string }; if (!res.ok) return { error: data.error ?? `Query failed (status ${res.status}).` }; - const rows = Array.isArray(data.results) ? (data.results as Array>) : []; + const rows = Array.isArray(data.results) + ? (data.results as Array>) + : []; const cap = 200; return { rows: rows.slice(0, cap), rowCount: rows.length, truncated: rows.length > cap }; }, @@ -448,7 +469,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe execute: async ({ question }) => { const url = process.env.SUPPORT_ASK_URL ?? "http://localhost:3939/api/ask"; const secret = process.env.SUPPORT_ASK_SECRET; - if (!secret) return { error: "The support assistant isn't configured in this environment." }; + if (!secret) + return { error: "The support assistant isn't configured in this environment." }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 60_000); try { @@ -461,7 +483,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe }), signal: controller.signal, }); - if (!res.ok) return { error: `The support assistant request failed (status ${res.status}).` }; + if (!res.ok) + return { error: `The support assistant request failed (status ${res.status}).` }; // The endpoint streams a UI-message SSE; accumulate the text-delta // chunks into the final answer (tool-output-error chunks are noise). const body = await res.text(); @@ -472,7 +495,8 @@ export function buildDashboardAgentTools(ctx: DashboardAgentToolContext): ToolSe if (!payload || payload === "[DONE]") continue; try { const chunk = JSON.parse(payload) as { type?: string; delta?: string }; - if (chunk.type === "text-delta" && typeof chunk.delta === "string") answer += chunk.delta; + if (chunk.type === "text-delta" && typeof chunk.delta === "string") + answer += chunk.delta; } catch { // Skip keepalives / non-JSON lines. } diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index b8e530908e2..ec0bc950b8c 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -25,4 +25,4 @@ "build": "pnpm run clean && tsc --noEmit false --outDir dist --declaration", "dev": "tsc --noEmit false --outDir dist --declaration --watch" } -} \ No newline at end of file +} diff --git a/internal-packages/emails/emails/mfa-enabled.tsx b/internal-packages/emails/emails/mfa-enabled.tsx index 93db74128c2..93af4b08aa5 100644 --- a/internal-packages/emails/emails/mfa-enabled.tsx +++ b/internal-packages/emails/emails/mfa-enabled.tsx @@ -39,8 +39,7 @@ export default function Email(props: MfaEnabledEmailProps) { • Keep your authenticator app safe and secured -
- • Never share your MFA codes with anyone +
• Never share your MFA codes with anyone
• Store your recovery codes in a secure location
{ + async send({ to, from, replyTo, subject, react }: MailMessage): Promise { try { await this.#client.sendMail({ from: from, @@ -27,8 +27,7 @@ export class AwsSesMailTransport implements MailTransport { subject, html: await render(react), }); - } - catch (error) { + } catch (error) { if (error instanceof Error) { console.error( `Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}` @@ -40,7 +39,7 @@ export class AwsSesMailTransport implements MailTransport { } } - async sendPlainText({to, from, replyTo, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise { try { await this.#client.sendMail({ from: from, @@ -49,8 +48,7 @@ export class AwsSesMailTransport implements MailTransport { subject, text: text, }); - } - catch (error) { + } catch (error) { if (error instanceof Error) { console.error( `Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}` diff --git a/internal-packages/emails/src/transports/index.ts b/internal-packages/emails/src/transports/index.ts index af85ab6ee17..5e7f9497e66 100644 --- a/internal-packages/emails/src/transports/index.ts +++ b/internal-packages/emails/src/transports/index.ts @@ -18,7 +18,7 @@ export type PlainTextMailMessage = { replyTo: string; subject: string; text: string; -} +}; export interface MailTransport { send(message: MailMessage): Promise; @@ -33,13 +33,13 @@ export class EmailError extends Error { } export type MailTransportOptions = - AwsSesMailTransportOptions | - ResendMailTransportOptions | - NullMailTransportOptions | - SmtpMailTransportOptions + | AwsSesMailTransportOptions + | ResendMailTransportOptions + | NullMailTransportOptions + | SmtpMailTransportOptions; export function constructMailTransport(options: MailTransportOptions): MailTransport { - switch(options.type) { + switch (options.type) { case "aws-ses": return new AwsSesMailTransport(options); case "resend": diff --git a/internal-packages/emails/src/transports/null.ts b/internal-packages/emails/src/transports/null.ts index 6a78fb90135..3674410bf64 100644 --- a/internal-packages/emails/src/transports/null.ts +++ b/internal-packages/emails/src/transports/null.ts @@ -2,14 +2,13 @@ import { render } from "@react-email/render"; import { MailMessage, MailTransport, PlainTextMailMessage } from "./index"; export type NullMailTransportOptions = { - type: undefined, -} + type: undefined; +}; export class NullMailTransport implements MailTransport { - constructor(options: NullMailTransportOptions) { - } + constructor(options: NullMailTransportOptions) {} - async send({to, subject, react}: MailMessage): Promise { + async send({ to, subject, react }: MailMessage): Promise { const plainText = await render(react, { plainText: true, }); @@ -21,7 +20,7 @@ ${plainText} `); } - async sendPlainText({to, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, subject, text }: PlainTextMailMessage): Promise { console.log(` ##### sendEmail to ${to}, subject: ${subject} diff --git a/internal-packages/emails/src/transports/resend.ts b/internal-packages/emails/src/transports/resend.ts index 9241dd4b933..534df99752e 100644 --- a/internal-packages/emails/src/transports/resend.ts +++ b/internal-packages/emails/src/transports/resend.ts @@ -2,20 +2,20 @@ import { EmailError, MailMessage, MailTransport, PlainTextMailMessage } from "./ import { Resend } from "resend"; export type ResendMailTransportOptions = { - type: 'resend', + type: "resend"; config: { - apiKey?: string - } -} + apiKey?: string; + }; +}; export class ResendMailTransport implements MailTransport { #client: Resend; constructor(options: ResendMailTransportOptions) { - this.#client = new Resend(options.config.apiKey) + this.#client = new Resend(options.config.apiKey); } - async send({to, from, replyTo, subject, react}: MailMessage): Promise { + async send({ to, from, replyTo, subject, react }: MailMessage): Promise { const result = await this.#client.emails.send({ from: from, to, @@ -32,7 +32,7 @@ export class ResendMailTransport implements MailTransport { } } - async sendPlainText({to, from, replyTo, subject, text}: PlainTextMailMessage): Promise { + async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise { const result = await this.#client.emails.send({ from: from, to, diff --git a/internal-packages/emails/tsconfig.json b/internal-packages/emails/tsconfig.json index fabe2cd4748..6d7e4a1b487 100644 --- a/internal-packages/emails/tsconfig.json +++ b/internal-packages/emails/tsconfig.json @@ -13,7 +13,7 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "target": "ES2015", + "target": "ES2015" }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] diff --git a/internal-packages/llm-model-catalog/scripts/generate.mjs b/internal-packages/llm-model-catalog/scripts/generate.mjs index 115643b1dff..430a4edb34d 100644 --- a/internal-packages/llm-model-catalog/scripts/generate.mjs +++ b/internal-packages/llm-model-catalog/scripts/generate.mjs @@ -43,7 +43,8 @@ if (existsSync(pricesJsonPath)) { let out = 'import type { DefaultModelDefinition } from "./types.js";\n\n'; out += "// Auto-generated from default-model-prices.json — do not edit manually.\n"; - out += "// Run `pnpm run sync-prices` to update the JSON, then `pnpm run generate` to regenerate.\n"; + out += + "// Run `pnpm run sync-prices` to update the JSON, then `pnpm run generate` to regenerate.\n"; out += "// Source: https://github.com/langfuse/langfuse\n\n"; out += "export const defaultModelPrices: DefaultModelDefinition[] = "; out += JSON.stringify(stripped, null, 2) + ";\n"; @@ -65,9 +66,12 @@ if (existsSync(catalogJsonPath)) { for (const key of Object.keys(data)) { if (data[key].releaseDate === undefined) data[key].releaseDate = null; if (data[key].isHidden === undefined) data[key].isHidden = false; - if (data[key].supportsStructuredOutput === undefined) data[key].supportsStructuredOutput = false; - if (data[key].supportsParallelToolCalls === undefined) data[key].supportsParallelToolCalls = false; - if (data[key].supportsStreamingToolCalls === undefined) data[key].supportsStreamingToolCalls = false; + if (data[key].supportsStructuredOutput === undefined) + data[key].supportsStructuredOutput = false; + if (data[key].supportsParallelToolCalls === undefined) + data[key].supportsParallelToolCalls = false; + if (data[key].supportsStreamingToolCalls === undefined) + data[key].supportsStreamingToolCalls = false; if (data[key].deprecationDate === undefined) data[key].deprecationDate = null; if (data[key].knowledgeCutoff === undefined) data[key].knowledgeCutoff = null; if (data[key].resolvedAt === undefined) data[key].resolvedAt = new Date().toISOString(); @@ -82,7 +86,8 @@ if (existsSync(catalogJsonPath)) { let out = 'import type { ModelCatalogEntry } from "./types.js";\n\n'; out += "// Auto-generated from model-catalog.json — do not edit manually.\n"; - out += "// Run `pnpm run generate-catalog` to update the JSON, then `pnpm run generate` to regenerate.\n\n"; + out += + "// Run `pnpm run generate-catalog` to update the JSON, then `pnpm run generate` to regenerate.\n\n"; out += "export const modelCatalog: Record = "; out += JSON.stringify(data, null, 2) + ";\n"; diff --git a/internal-packages/llm-model-catalog/src/defaultPrices.ts b/internal-packages/llm-model-catalog/src/defaultPrices.ts index fb347c2bef6..982b6b2ec15 100644 --- a/internal-packages/llm-model-catalog/src/defaultPrices.ts +++ b/internal-packages/llm-model-catalog/src/defaultPrices.ts @@ -6,3091 +6,3106 @@ import type { DefaultModelDefinition } from "./types.js"; export const defaultModelPrices: DefaultModelDefinition[] = [ { - "modelName": "gpt-4o", - "matchPattern": "(?i)^(openai/)?(gpt-4o)$", - "startDate": "2024-05-13T23:15:07.670Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-05-13", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-05-13)$", - "startDate": "2024-05-13T23:15:07.670Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "output": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-4-1106-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-1106-preview)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-vision", - "matchPattern": "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-32k", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-4-32k-0613", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-1106", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000001, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0613", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000015, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-4-0613", - "matchPattern": "(?i)^(openai/)?(gpt-4-0613)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-instruct", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000015, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "text-ada-001", - "matchPattern": "(?i)^(text-ada-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.000004 - } - } - ] - }, - { - "modelName": "text-babbage-001", - "matchPattern": "(?i)^(text-babbage-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 5e-7 - } - } - ] - }, - { - "modelName": "text-curie-001", - "matchPattern": "(?i)^(text-curie-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-001", - "matchPattern": "(?i)^(text-davinci-001)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-002", - "matchPattern": "(?i)^(text-davinci-002)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-davinci-003", - "matchPattern": "(?i)^(text-davinci-003)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 0.00002 - } - } - ] - }, - { - "modelName": "text-embedding-ada-002-v2", - "matchPattern": "(?i)^(text-embedding-ada-002-v2)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "text-embedding-ada-002", - "matchPattern": "(?i)^(text-embedding-ada-002)$", - "startDate": "2024-01-24T18:18:50.861Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-16k-0613", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", - "startDate": "2024-02-03T17:29:57.350Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "output": 0.000004 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0301", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "output": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-4-32k-0314", - "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0314)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00006, - "output": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-4-0314", - "matchPattern": "(?i)^(openai/)?(gpt-4-0314)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "gpt-4", - "matchPattern": "(?i)^(openai/)?(gpt-4)$", - "startDate": "2024-01-24T10:19:21.693Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00006 - } - } - ] - }, - { - "modelName": "claude-instant-1.2", - "matchPattern": "(?i)^(anthropic/)?(claude-instant-1.2)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000163, - "output": 0.00000551 - } - } - ] - }, - { - "modelName": "claude-2.0", - "matchPattern": "(?i)^(anthropic/)?(claude-2.0)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-2.1", - "matchPattern": "(?i)^(anthropic/)?(claude-2.1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.3", - "matchPattern": "(?i)^(anthropic/)?(claude-1.3)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.2", - "matchPattern": "(?i)^(anthropic/)?(claude-1.2)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-1.1", - "matchPattern": "(?i)^(anthropic/)?(claude-1.1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000008, - "output": 0.000024 - } - } - ] - }, - { - "modelName": "claude-instant-1", - "matchPattern": "(?i)^(anthropic/)?(claude-instant-1)$", - "startDate": "2024-01-30T15:44:13.447Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000163, - "output": 0.00000551 - } - } - ] - }, - { - "modelName": "babbage-002", - "matchPattern": "(?i)^(babbage-002)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "davinci-002", - "matchPattern": "(?i)^(davinci-002)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000006, - "output": 0.000012 - } - } - ] - }, - { - "modelName": "text-embedding-3-small", - "matchPattern": "(?i)^(text-embedding-3-small)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 2e-8 - } - } - ] - }, - { - "modelName": "text-embedding-3-large", - "matchPattern": "(?i)^(text-embedding-3-large)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1.3e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-0125", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", - "startDate": "2024-02-13T12:00:37.424Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-4-0125-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-0125-preview)$", - "startDate": "2024-01-26T17:35:21.129Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "ft:gpt-3.5-turbo-1106", - "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "output": 0.000006 - } - } - ] - }, - { - "modelName": "ft:gpt-3.5-turbo-0613", - "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000012, - "output": 0.000016 - } - } - ] - }, - { - "modelName": "ft:davinci-002", - "matchPattern": "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000012, - "output": 0.000012 - } - } - ] - }, - { - "modelName": "ft:babbage-002", - "matchPattern": "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000016, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "chat-bison", - "matchPattern": "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "codechat-bison-32k", - "matchPattern": "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "codechat-bison", - "matchPattern": "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "text-bison-32k", - "matchPattern": "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "chat-bison-32k", - "matchPattern": "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "text-unicorn", - "matchPattern": "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "output": 0.0000075 - } - } - ] - }, - { - "modelName": "text-bison", - "matchPattern": "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "textembedding-gecko", - "matchPattern": "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "textembedding-gecko-multilingual", - "matchPattern": "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "total": 1e-7 - } - } - ] - }, - { - "modelName": "code-gecko", - "matchPattern": "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "code-bison", - "matchPattern": "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "code-bison-32k", - "matchPattern": "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-01-31T13:25:02.141Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "gpt-3.5-turbo-16k", - "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", - "startDate": "2024-02-13T12:00:37.424Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "output": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-preview)$", - "startDate": "2024-02-15T21:21:50.947Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "claude-3-opus-20240229", - "matchPattern": "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", - "startDate": "2024-03-07T17:55:38.139Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.000075 - } - } - ] - }, - { - "modelName": "claude-3-sonnet-20240229", - "matchPattern": "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", - "startDate": "2024-03-07T17:55:38.139Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3-haiku-20240307", - "matchPattern": "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", - "startDate": "2024-03-14T09:41:18.736Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro-latest", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "output": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-1.0-pro-001", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.25e-7, - "output": 3.75e-7 - } - } - ] - }, - { - "modelName": "gemini-1.5-pro-latest", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", - "startDate": "2024-04-11T10:27:46.517Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "output": 0.0000075 - } - } - ] - }, - { - "modelName": "gpt-4-turbo-2024-04-09", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-turbo", - "matchPattern": "(?i)^(openai/)?(gpt-4-turbo)$", - "startDate": "2024-04-11T21:13:44.989Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "gpt-4-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4-preview)$", - "startDate": "2024-04-23T10:37:17.092Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00001, - "output": 0.00003 - } - } - ] - }, - { - "modelName": "claude-3-5-sonnet-20240620", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", - "startDate": "2024-06-25T11:47:24.475Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4o-mini", - "matchPattern": "(?i)^(openai/)?(gpt-4o-mini)$", - "startDate": "2024-07-18T17:56:09.591Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.5e-7, - "output": 6e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8 - } - } - ] - }, - { - "modelName": "gpt-4o-mini-2024-07-18", - "matchPattern": "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", - "startDate": "2024-07-18T17:56:09.591Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 6e-7 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-08-06", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-08-06)$", - "startDate": "2024-08-07T11:54:31.298Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "o1-preview", - "matchPattern": "(?i)^(openai/)?(o1-preview)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-preview-2024-09-12", - "matchPattern": "(?i)^(openai/)?(o1-preview-2024-09-12)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-mini", - "matchPattern": "(?i)^(openai/)?(o1-mini)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o1-mini-2024-09-12", - "matchPattern": "(?i)^(openai/)?(o1-mini-2024-09-12)$", - "startDate": "2024-09-13T10:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "claude-3.5-sonnet-20241022", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", - "startDate": "2024-10-22T18:48:01.676Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.5-sonnet-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", - "startDate": "2024-10-22T18:48:01.676Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3-5-haiku-20241022", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", - "startDate": "2024-11-05T10:30:50.566Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 8e-7, - "input_tokens": 8e-7, - "output": 0.000004, - "output_tokens": 0.000004, - "cache_creation_input_tokens": 0.000001, - "input_cache_creation": 0.000001, - "input_cache_creation_5m": 0.000001, - "input_cache_creation_1h": 0.0000016, - "cache_read_input_tokens": 8e-8, - "input_cache_read": 8e-8 - } - } - ] - }, - { - "modelName": "claude-3.5-haiku-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", - "startDate": "2024-11-05T10:30:50.566Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 8e-7, - "input_tokens": 8e-7, - "output": 0.000004, - "output_tokens": 0.000004, - "cache_creation_input_tokens": 0.000001, - "input_cache_creation": 0.000001, - "input_cache_creation_5m": 0.000001, - "input_cache_creation_1h": 0.0000016, - "cache_read_input_tokens": 8e-8, - "input_cache_read": 8e-8 - } - } - ] - }, - { - "modelName": "chatgpt-4o-latest", - "matchPattern": "(?i)^(chatgpt-4o-latest)$", - "startDate": "2024-11-25T12:47:17.504Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "output": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-4o-2024-11-20", - "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-11-20)$", - "startDate": "2024-12-03T10:06:12.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 0.00000125, - "input_cache_read": 0.00000125, - "output": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-4o-audio-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.0000025, - "output_text_tokens": 0.00001, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-audio-preview-2024-10-01", - "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.0000025, - "output_text_tokens": 0.00001, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-realtime-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.000005, - "input_cached_text_tokens": 0.0000025, - "output_text_tokens": 0.00002, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "input_cached_audio_tokens": 0.00002, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "gpt-4o-realtime-preview-2024-10-01", - "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", - "startDate": "2024-12-03T10:19:56.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text_tokens": 0.000005, - "input_cached_text_tokens": 0.0000025, - "output_text_tokens": 0.00002, - "input_audio_tokens": 0.0001, - "input_audio": 0.0001, - "input_cached_audio_tokens": 0.00002, - "output_audio_tokens": 0.0002, - "output_audio": 0.0002 - } - } - ] - }, - { - "modelName": "o1", - "matchPattern": "(?i)^(openai/)?(o1)$", - "startDate": "2025-01-17T00:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o1-2024-12-17", - "matchPattern": "(?i)^(openai/)?(o1-2024-12-17)$", - "startDate": "2025-01-17T00:01:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_cached_tokens": 0.0000075, - "input_cache_read": 0.0000075, - "output": 0.00006, - "output_reasoning_tokens": 0.00006, - "output_reasoning": 0.00006 - } - } - ] - }, - { - "modelName": "o3-mini", - "matchPattern": "(?i)^(openai/)?(o3-mini)$", - "startDate": "2025-01-31T20:41:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o3-mini-2025-01-31", - "matchPattern": "(?i)^(openai/)?(o3-mini-2025-01-31)$", - "startDate": "2025-01-31T20:41:35.373Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 5.5e-7, - "input_cache_read": 5.5e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-001", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-02-06T11:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-lite-preview-02-05", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-02-06T11:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-8, - "output": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.7-sonnet-20250219", - "matchPattern": "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", - "startDate": "2025-02-25T09:35:39.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-3.7-sonnet-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", - "startDate": "2025-02-25T09:35:39.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4.5-preview", - "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview)$", - "startDate": "2025-02-27T21:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000075, - "input_cached_tokens": 0.0000375, - "input_cached_text_tokens": 0.0000375, - "input_cache_read": 0.0000375, - "output": 0.00015 - } - } - ] - }, - { - "modelName": "gpt-4.5-preview-2025-02-27", - "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", - "startDate": "2025-02-27T21:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000075, - "input_cached_tokens": 0.0000375, - "input_cached_text_tokens": 0.0000375, - "input_cache_read": 0.0000375, - "output": 0.00015 - } - } - ] - }, - { - "modelName": "gpt-4.1", - "matchPattern": "(?i)^(openai/)?(gpt-4.1)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cached_text_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008 - } - } - ] - }, - { - "modelName": "gpt-4.1-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cached_text_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008 - } - } - ] - }, - { - "modelName": "gpt-4.1-mini-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "input_cached_tokens": 1e-7, - "input_cached_text_tokens": 1e-7, - "input_cache_read": 1e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "gpt-4.1-nano-2025-04-14", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", - "startDate": "2025-04-15T10:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_cached_tokens": 2.5e-8, - "input_cached_text_tokens": 2.5e-8, - "input_cache_read": 2.5e-8, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "o3", - "matchPattern": "(?i)^(openai/)?(o3)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008, - "output_reasoning_tokens": 0.000008, - "output_reasoning": 0.000008 - } - } - ] - }, - { - "modelName": "o3-2025-04-16", - "matchPattern": "(?i)^(openai/)?(o3-2025-04-16)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_cached_tokens": 5e-7, - "input_cache_read": 5e-7, - "output": 0.000008, - "output_reasoning_tokens": 0.000008, - "output_reasoning": 0.000008 - } - } - ] - }, - { - "modelName": "o4-mini", - "matchPattern": "(?i)^(o4-mini)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 2.75e-7, - "input_cache_read": 2.75e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "o4-mini-2025-04-16", - "matchPattern": "(?i)^(o4-mini-2025-04-16)$", - "startDate": "2025-04-16T23:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000011, - "input_cached_tokens": 2.75e-7, - "input_cache_read": 2.75e-7, - "output": 0.0000044, - "output_reasoning_tokens": 0.0000044, - "output_reasoning": 0.0000044 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gemini-2.0-flash-lite-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-8, - "output": 3e-7 - } - } - ] - }, - { - "modelName": "gpt-4.1-nano", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano)$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_cached_tokens": 2.5e-8, - "input_cached_text_tokens": 2.5e-8, - "input_cache_read": 2.5e-8, - "output": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-4.1-mini", - "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini)$", - "startDate": "2025-04-22T10:11:35.241Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 4e-7, - "input_cached_tokens": 1e-7, - "input_cached_text_tokens": 1e-7, - "input_cache_read": 1e-7, - "output": 0.0000016 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-5-20250929", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", - "startDate": "2025-09-29T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + modelName: "gpt-4o", + matchPattern: "(?i)^(openai/)?(gpt-4o)$", + startDate: "2024-05-13T23:15:07.670Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-05-13", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-05-13)$", + startDate: "2024-05-13T23:15:07.670Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + output: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-4-1106-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-1106-preview)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-vision", + matchPattern: "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-32k", + matchPattern: "(?i)^(openai/)?(gpt-4-32k)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-4-32k-0613", + matchPattern: "(?i)^(openai/)?(gpt-4-32k-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-1106", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000001, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0613", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000015, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-4-0613", + matchPattern: "(?i)^(openai/)?(gpt-4-0613)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-instruct", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000015, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "text-ada-001", + matchPattern: "(?i)^(text-ada-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.000004, + }, + }, + ], + }, + { + modelName: "text-babbage-001", + matchPattern: "(?i)^(text-babbage-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 5e-7, + }, + }, + ], + }, + { + modelName: "text-curie-001", + matchPattern: "(?i)^(text-curie-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-001", + matchPattern: "(?i)^(text-davinci-001)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-002", + matchPattern: "(?i)^(text-davinci-002)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-davinci-003", + matchPattern: "(?i)^(text-davinci-003)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 0.00002, + }, + }, + ], + }, + { + modelName: "text-embedding-ada-002-v2", + matchPattern: "(?i)^(text-embedding-ada-002-v2)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "text-embedding-ada-002", + matchPattern: "(?i)^(text-embedding-ada-002)$", + startDate: "2024-01-24T18:18:50.861Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-16k-0613", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", + startDate: "2024-02-03T17:29:57.350Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + output: 0.000004, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0301", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + output: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-4-32k-0314", + matchPattern: "(?i)^(openai/)?(gpt-4-32k-0314)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00006, + output: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-4-0314", + matchPattern: "(?i)^(openai/)?(gpt-4-0314)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "gpt-4", + matchPattern: "(?i)^(openai/)?(gpt-4)$", + startDate: "2024-01-24T10:19:21.693Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00006, + }, + }, + ], + }, + { + modelName: "claude-instant-1.2", + matchPattern: "(?i)^(anthropic/)?(claude-instant-1.2)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000163, + output: 0.00000551, + }, + }, + ], + }, + { + modelName: "claude-2.0", + matchPattern: "(?i)^(anthropic/)?(claude-2.0)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-2.1", + matchPattern: "(?i)^(anthropic/)?(claude-2.1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.3", + matchPattern: "(?i)^(anthropic/)?(claude-1.3)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.2", + matchPattern: "(?i)^(anthropic/)?(claude-1.2)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-1.1", + matchPattern: "(?i)^(anthropic/)?(claude-1.1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000008, + output: 0.000024, + }, + }, + ], + }, + { + modelName: "claude-instant-1", + matchPattern: "(?i)^(anthropic/)?(claude-instant-1)$", + startDate: "2024-01-30T15:44:13.447Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000163, + output: 0.00000551, + }, + }, + ], + }, + { + modelName: "babbage-002", + matchPattern: "(?i)^(babbage-002)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "davinci-002", + matchPattern: "(?i)^(davinci-002)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000006, + output: 0.000012, + }, + }, + ], + }, + { + modelName: "text-embedding-3-small", + matchPattern: "(?i)^(text-embedding-3-small)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 2e-8, + }, + }, + ], + }, + { + modelName: "text-embedding-3-large", + matchPattern: "(?i)^(text-embedding-3-large)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1.3e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-0125", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", + startDate: "2024-02-13T12:00:37.424Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-4-0125-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-0125-preview)$", + startDate: "2024-01-26T17:35:21.129Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "ft:gpt-3.5-turbo-1106", + matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + output: 0.000006, + }, + }, + ], + }, + { + modelName: "ft:gpt-3.5-turbo-0613", + matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000012, + output: 0.000016, + }, + }, + ], + }, + { + modelName: "ft:davinci-002", + matchPattern: "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000012, + output: 0.000012, + }, + }, + ], + }, + { + modelName: "ft:babbage-002", + matchPattern: "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000016, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "chat-bison", + matchPattern: "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "codechat-bison-32k", + matchPattern: "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "codechat-bison", + matchPattern: "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "text-bison-32k", + matchPattern: "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "chat-bison-32k", + matchPattern: "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "text-unicorn", + matchPattern: "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + output: 0.0000075, + }, + }, + ], + }, + { + modelName: "text-bison", + matchPattern: "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "textembedding-gecko", + matchPattern: "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "textembedding-gecko-multilingual", + matchPattern: "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + total: 1e-7, + }, + }, + ], + }, + { + modelName: "code-gecko", + matchPattern: "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "code-bison", + matchPattern: "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "code-bison-32k", + matchPattern: "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", + startDate: "2024-01-31T13:25:02.141Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "gpt-3.5-turbo-16k", + matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", + startDate: "2024-02-13T12:00:37.424Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + output: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo-preview)$", + startDate: "2024-02-15T21:21:50.947Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "claude-3-opus-20240229", + matchPattern: + "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", + startDate: "2024-03-07T17:55:38.139Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.000075, + }, + }, + ], + }, + { + modelName: "claude-3-sonnet-20240229", + matchPattern: + "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", + startDate: "2024-03-07T17:55:38.139Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3-haiku-20240307", + matchPattern: + "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", + startDate: "2024-03-14T09:41:18.736Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro-latest", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + output: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.0-pro-001", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.25e-7, + output: 3.75e-7, + }, + }, + ], + }, + { + modelName: "gemini-1.5-pro-latest", + matchPattern: "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", + startDate: "2024-04-11T10:27:46.517Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + output: 0.0000075, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo-2024-04-09", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-turbo", + matchPattern: "(?i)^(openai/)?(gpt-4-turbo)$", + startDate: "2024-04-11T21:13:44.989Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "gpt-4-preview", + matchPattern: "(?i)^(openai/)?(gpt-4-preview)$", + startDate: "2024-04-23T10:37:17.092Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00001, + output: 0.00003, + }, + }, + ], + }, + { + modelName: "claude-3-5-sonnet-20240620", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", + startDate: "2024-06-25T11:47:24.475Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4o-mini", + matchPattern: "(?i)^(openai/)?(gpt-4o-mini)$", + startDate: "2024-07-18T17:56:09.591Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.5e-7, + output: 6e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + }, + }, + ], + }, + { + modelName: "gpt-4o-mini-2024-07-18", + matchPattern: "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", + startDate: "2024-07-18T17:56:09.591Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 6e-7, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-08-06", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-08-06)$", + startDate: "2024-08-07T11:54:31.298Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "o1-preview", + matchPattern: "(?i)^(openai/)?(o1-preview)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-preview-2024-09-12", + matchPattern: "(?i)^(openai/)?(o1-preview-2024-09-12)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-mini", + matchPattern: "(?i)^(openai/)?(o1-mini)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o1-mini-2024-09-12", + matchPattern: "(?i)^(openai/)?(o1-mini-2024-09-12)$", + startDate: "2024-09-13T10:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "claude-3.5-sonnet-20241022", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", + startDate: "2024-10-22T18:48:01.676Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.5-sonnet-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", + startDate: "2024-10-22T18:48:01.676Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3-5-haiku-20241022", + matchPattern: + "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", + startDate: "2024-11-05T10:30:50.566Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 8e-7, + input_tokens: 8e-7, + output: 0.000004, + output_tokens: 0.000004, + cache_creation_input_tokens: 0.000001, + input_cache_creation: 0.000001, + input_cache_creation_5m: 0.000001, + input_cache_creation_1h: 0.0000016, + cache_read_input_tokens: 8e-8, + input_cache_read: 8e-8, + }, + }, + ], + }, + { + modelName: "claude-3.5-haiku-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", + startDate: "2024-11-05T10:30:50.566Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 8e-7, + input_tokens: 8e-7, + output: 0.000004, + output_tokens: 0.000004, + cache_creation_input_tokens: 0.000001, + input_cache_creation: 0.000001, + input_cache_creation_5m: 0.000001, + input_cache_creation_1h: 0.0000016, + cache_read_input_tokens: 8e-8, + input_cache_read: 8e-8, + }, + }, + ], + }, + { + modelName: "chatgpt-4o-latest", + matchPattern: "(?i)^(chatgpt-4o-latest)$", + startDate: "2024-11-25T12:47:17.504Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + output: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-4o-2024-11-20", + matchPattern: "(?i)^(openai/)?(gpt-4o-2024-11-20)$", + startDate: "2024-12-03T10:06:12.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 0.00000125, + input_cache_read: 0.00000125, + output: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-4o-audio-preview", + matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.0000025, + output_text_tokens: 0.00001, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-audio-preview-2024-10-01", + matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.0000025, + output_text_tokens: 0.00001, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-realtime-preview", + matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.000005, + input_cached_text_tokens: 0.0000025, + output_text_tokens: 0.00002, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + input_cached_audio_tokens: 0.00002, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "gpt-4o-realtime-preview-2024-10-01", + matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", + startDate: "2024-12-03T10:19:56.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text_tokens: 0.000005, + input_cached_text_tokens: 0.0000025, + output_text_tokens: 0.00002, + input_audio_tokens: 0.0001, + input_audio: 0.0001, + input_cached_audio_tokens: 0.00002, + output_audio_tokens: 0.0002, + output_audio: 0.0002, + }, + }, + ], + }, + { + modelName: "o1", + matchPattern: "(?i)^(openai/)?(o1)$", + startDate: "2025-01-17T00:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o1-2024-12-17", + matchPattern: "(?i)^(openai/)?(o1-2024-12-17)$", + startDate: "2025-01-17T00:01:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_cached_tokens: 0.0000075, + input_cache_read: 0.0000075, + output: 0.00006, + output_reasoning_tokens: 0.00006, + output_reasoning: 0.00006, + }, + }, + ], + }, + { + modelName: "o3-mini", + matchPattern: "(?i)^(openai/)?(o3-mini)$", + startDate: "2025-01-31T20:41:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o3-mini-2025-01-31", + matchPattern: "(?i)^(openai/)?(o3-mini-2025-01-31)$", + startDate: "2025-01-31T20:41:35.373Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 5.5e-7, + input_cache_read: 5.5e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-001", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", + startDate: "2025-02-06T11:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-lite-preview-02-05", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", + startDate: "2025-02-06T11:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-8, + output: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.7-sonnet-20250219", + matchPattern: + "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", + startDate: "2025-02-25T09:35:39.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-3.7-sonnet-latest", + matchPattern: "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", + startDate: "2025-02-25T09:35:39.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.5-preview", + matchPattern: "(?i)^(openai/)?(gpt-4.5-preview)$", + startDate: "2025-02-27T21:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000075, + input_cached_tokens: 0.0000375, + input_cached_text_tokens: 0.0000375, + input_cache_read: 0.0000375, + output: 0.00015, + }, + }, + ], + }, + { + modelName: "gpt-4.5-preview-2025-02-27", + matchPattern: "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", + startDate: "2025-02-27T21:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000075, + input_cached_tokens: 0.0000375, + input_cached_text_tokens: 0.0000375, + input_cache_read: 0.0000375, + output: 0.00015, + }, + }, + ], + }, + { + modelName: "gpt-4.1", + matchPattern: "(?i)^(openai/)?(gpt-4.1)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cached_text_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + }, + }, + ], + }, + { + modelName: "gpt-4.1-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cached_text_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + }, + }, + ], + }, + { + modelName: "gpt-4.1-mini-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + input_cached_tokens: 1e-7, + input_cached_text_tokens: 1e-7, + input_cache_read: 1e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "gpt-4.1-nano-2025-04-14", + matchPattern: "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", + startDate: "2025-04-15T10:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_cached_tokens: 2.5e-8, + input_cached_text_tokens: 2.5e-8, + input_cache_read: 2.5e-8, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "o3", + matchPattern: "(?i)^(openai/)?(o3)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + output_reasoning_tokens: 0.000008, + output_reasoning: 0.000008, + }, + }, + ], + }, + { + modelName: "o3-2025-04-16", + matchPattern: "(?i)^(openai/)?(o3-2025-04-16)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_cached_tokens: 5e-7, + input_cache_read: 5e-7, + output: 0.000008, + output_reasoning_tokens: 0.000008, + output_reasoning: 0.000008, + }, + }, + ], + }, + { + modelName: "o4-mini", + matchPattern: "(?i)^(o4-mini)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 2.75e-7, + input_cache_read: 2.75e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "o4-mini-2025-04-16", + matchPattern: "(?i)^(o4-mini-2025-04-16)$", + startDate: "2025-04-16T23:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000011, + input_cached_tokens: 2.75e-7, + input_cache_read: 2.75e-7, + output: 0.0000044, + output_reasoning_tokens: 0.0000044, + output_reasoning: 0.0000044, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.0-flash-lite-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-8, + output: 3e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.1-nano", + matchPattern: "(?i)^(openai/)?(gpt-4.1-nano)$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_cached_tokens: 2.5e-8, + input_cached_text_tokens: 2.5e-8, + input_cache_read: 2.5e-8, + output: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-4.1-mini", + matchPattern: "(?i)^(openai/)?(gpt-4.1-mini)$", + startDate: "2025-04-22T10:11:35.241Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 4e-7, + input_cached_tokens: 1e-7, + input_cached_text_tokens: 1e-7, + input_cache_read: 1e-7, + output: 0.0000016, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-5-20250929", + matchPattern: + "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", + startDate: "2025-09-29T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "input", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "input", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000006, - "input_tokens": 0.000006, - "output": 0.0000225, - "output_tokens": 0.0000225, - "cache_creation_input_tokens": 0.0000075, - "input_cache_creation": 0.0000075, - "input_cache_creation_5m": 0.0000075, - "input_cache_creation_1h": 0.000012, - "cache_read_input_tokens": 6e-7, - "input_cache_read": 6e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-20250514", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-latest", - "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-20250514", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", - "startDate": "2025-05-22T17:09:02.131Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_tokens": 0.000015, - "output": 0.000075, - "output_tokens": 0.000075, - "cache_creation_input_tokens": 0.00001875, - "input_cache_creation": 0.00001875, - "input_cache_creation_5m": 0.00001875, - "input_cache_creation_1h": 0.00003, - "cache_read_input_tokens": 0.0000015, - "input_cache_read": 0.0000015 - } - } - ] - }, - { - "modelName": "o3-pro", - "matchPattern": "(?i)^(openai/)?(o3-pro)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00002, - "output": 0.00008, - "output_reasoning_tokens": 0.00008, - "output_reasoning": 0.00008 - } - } - ] - }, - { - "modelName": "o3-pro-2025-06-10", - "matchPattern": "(?i)^(openai/)?(o3-pro-2025-06-10)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00002, - "output": 0.00008, - "output_reasoning_tokens": 0.00008, - "output_reasoning": 0.00008 - } - } - ] - }, - { - "modelName": "o1-pro", - "matchPattern": "(?i)^(openai/)?(o1-pro)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00015, - "output": 0.0006, - "output_reasoning_tokens": 0.0006, - "output_reasoning": 0.0006 - } - } - ] - }, - { - "modelName": "o1-pro-2025-03-19", - "matchPattern": "(?i)^(openai/)?(o1-pro-2025-03-19)$", - "startDate": "2025-06-10T22:26:54.132Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00015, - "output": 0.0006, - "output_reasoning_tokens": 0.0006, - "output_reasoning": 0.0006 - } - } - ] - }, - { - "modelName": "gemini-2.5-flash", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", - "startDate": "2025-07-03T13:44:06.964Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 3e-7, - "input_text": 3e-7, - "input_modality_1": 3e-7, - "prompt_token_count": 3e-7, - "promptTokenCount": 3e-7, - "input_cached_tokens": 3e-8, - "cached_content_token_count": 3e-8, - "output": 0.0000025, - "output_text": 0.0000025, - "output_modality_1": 0.0000025, - "candidates_token_count": 0.0000025, - "candidatesTokenCount": 0.0000025, - "thoughtsTokenCount": 0.0000025, - "thoughts_token_count": 0.0000025, - "output_reasoning": 0.0000025, - "input_audio_tokens": 0.000001 - } - } - ] - }, - { - "modelName": "gemini-2.5-flash-lite", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", - "startDate": "2025-07-03T13:44:06.964Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 1e-7, - "input_text": 1e-7, - "input_modality_1": 1e-7, - "prompt_token_count": 1e-7, - "promptTokenCount": 1e-7, - "input_cached_tokens": 2.5e-8, - "cached_content_token_count": 2.5e-8, - "output": 4e-7, - "output_text": 4e-7, - "output_modality_1": 4e-7, - "candidates_token_count": 4e-7, - "candidatesTokenCount": 4e-7, - "thoughtsTokenCount": 4e-7, - "thoughts_token_count": 4e-7, - "output_reasoning": 4e-7, - "input_audio_tokens": 5e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-1-20250805", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", - "startDate": "2025-08-05T15:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "input_tokens": 0.000015, - "output": 0.000075, - "output_tokens": 0.000075, - "cache_creation_input_tokens": 0.00001875, - "input_cache_creation": 0.00001875, - "input_cache_creation_5m": 0.00001875, - "input_cache_creation_1h": 0.00003, - "cache_read_input_tokens": 0.0000015, - "input_cache_read": 0.0000015 - } - } - ] - }, - { - "modelName": "gpt-5", - "matchPattern": "(?i)^(openai/)?(gpt-5)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-mini", - "matchPattern": "(?i)^(openai/)?(gpt-5-mini)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "output": 0.000002, - "input_cache_read": 2.5e-8, - "output_reasoning_tokens": 0.000002, - "output_reasoning": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-5-mini-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "output": 0.000002, - "input_cache_read": 2.5e-8, - "output_reasoning_tokens": 0.000002, - "output_reasoning": 0.000002 - } - } - ] - }, - { - "modelName": "gpt-5-nano", - "matchPattern": "(?i)^(openai/)?(gpt-5-nano)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-8, - "input_cached_tokens": 5e-9, - "output": 4e-7, - "input_cache_read": 5e-9, - "output_reasoning_tokens": 4e-7, - "output_reasoning": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-5-nano-2025-08-07", - "matchPattern": "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", - "startDate": "2025-08-11T08:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-8, - "input_cached_tokens": 5e-9, - "output": 4e-7, - "input_cache_read": 5e-9, - "output_reasoning_tokens": 4e-7, - "output_reasoning": 4e-7 - } - } - ] - }, - { - "modelName": "gpt-5-chat-latest", - "matchPattern": "(?i)^(openai/)?(gpt-5-chat-latest)$", - "startDate": "2025-08-07T16:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5-pro)$", - "startDate": "2025-10-07T08:03:54.727Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.00012, - "output_reasoning_tokens": 0.00012, - "output_reasoning": 0.00012 - } - } - ] - }, - { - "modelName": "gpt-5-pro-2025-10-06", - "matchPattern": "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", - "startDate": "2025-10-07T08:03:54.727Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000015, - "output": 0.00012, - "output_reasoning_tokens": 0.00012, - "output_reasoning": 0.00012 - } - } - ] - }, - { - "modelName": "claude-haiku-4-5-20251001", - "matchPattern": "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", - "startDate": "2025-10-16T08:20:44.558Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000001, - "input_tokens": 0.000001, - "output": 0.000005, - "output_tokens": 0.000005, - "cache_creation_input_tokens": 0.00000125, - "input_cache_creation": 0.00000125, - "input_cache_creation_5m": 0.00000125, - "input_cache_creation_1h": 0.000002, - "cache_read_input_tokens": 1e-7, - "input_cache_read": 1e-7 - } - } - ] - }, - { - "modelName": "gpt-5.1", - "matchPattern": "(?i)^(openai/)?(gpt-5.1)$", - "startDate": "2025-11-14T08:57:23.481Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "gpt-5.1-2025-11-13", - "matchPattern": "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", - "startDate": "2025-11-14T08:57:23.481Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_cached_tokens": 1.25e-7, - "output": 0.00001, - "input_cache_read": 1.25e-7, - "output_reasoning_tokens": 0.00001, - "output_reasoning": 0.00001 - } - } - ] - }, - { - "modelName": "claude-opus-4-5-20251101", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", - "startDate": "2025-11-24T20:53:27.571Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "input_tokens": 0.000005, - "output": 0.000025, - "output_tokens": 0.000025, - "cache_creation_input_tokens": 0.00000625, - "input_cache_creation": 0.00000625, - "input_cache_creation_5m": 0.00000625, - "input_cache_creation_1h": 0.00001, - "cache_read_input_tokens": 5e-7, - "input_cache_read": 5e-7 - } - } - ] - }, - { - "modelName": "claude-sonnet-4-6", - "matchPattern": "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", - "startDate": "2026-02-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000003, - "input_tokens": 0.000003, - "output": 0.000015, - "output_tokens": 0.000015, - "cache_creation_input_tokens": 0.00000375, - "input_cache_creation": 0.00000375, - "input_cache_creation_5m": 0.00000375, - "input_cache_creation_1h": 0.000006, - "cache_read_input_tokens": 3e-7, - "input_cache_read": 3e-7 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000006, + input_tokens: 0.000006, + output: 0.0000225, + output_tokens: 0.0000225, + cache_creation_input_tokens: 0.0000075, + input_cache_creation: 0.0000075, + input_cache_creation_5m: 0.0000075, + input_cache_creation_1h: 0.000012, + cache_read_input_tokens: 6e-7, + input_cache_read: 6e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-20250514", + matchPattern: + "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-latest", + matchPattern: "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-20250514", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", + startDate: "2025-05-22T17:09:02.131Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_tokens: 0.000015, + output: 0.000075, + output_tokens: 0.000075, + cache_creation_input_tokens: 0.00001875, + input_cache_creation: 0.00001875, + input_cache_creation_5m: 0.00001875, + input_cache_creation_1h: 0.00003, + cache_read_input_tokens: 0.0000015, + input_cache_read: 0.0000015, + }, + }, + ], + }, + { + modelName: "o3-pro", + matchPattern: "(?i)^(openai/)?(o3-pro)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00002, + output: 0.00008, + output_reasoning_tokens: 0.00008, + output_reasoning: 0.00008, + }, + }, + ], + }, + { + modelName: "o3-pro-2025-06-10", + matchPattern: "(?i)^(openai/)?(o3-pro-2025-06-10)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00002, + output: 0.00008, + output_reasoning_tokens: 0.00008, + output_reasoning: 0.00008, + }, + }, + ], + }, + { + modelName: "o1-pro", + matchPattern: "(?i)^(openai/)?(o1-pro)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00015, + output: 0.0006, + output_reasoning_tokens: 0.0006, + output_reasoning: 0.0006, + }, + }, + ], + }, + { + modelName: "o1-pro-2025-03-19", + matchPattern: "(?i)^(openai/)?(o1-pro-2025-03-19)$", + startDate: "2025-06-10T22:26:54.132Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00015, + output: 0.0006, + output_reasoning_tokens: 0.0006, + output_reasoning: 0.0006, + }, + }, + ], + }, + { + modelName: "gemini-2.5-flash", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", + startDate: "2025-07-03T13:44:06.964Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 3e-7, + input_text: 3e-7, + input_modality_1: 3e-7, + prompt_token_count: 3e-7, + promptTokenCount: 3e-7, + input_cached_tokens: 3e-8, + cached_content_token_count: 3e-8, + output: 0.0000025, + output_text: 0.0000025, + output_modality_1: 0.0000025, + candidates_token_count: 0.0000025, + candidatesTokenCount: 0.0000025, + thoughtsTokenCount: 0.0000025, + thoughts_token_count: 0.0000025, + output_reasoning: 0.0000025, + input_audio_tokens: 0.000001, + }, + }, + ], + }, + { + modelName: "gemini-2.5-flash-lite", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", + startDate: "2025-07-03T13:44:06.964Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 1e-7, + input_text: 1e-7, + input_modality_1: 1e-7, + prompt_token_count: 1e-7, + promptTokenCount: 1e-7, + input_cached_tokens: 2.5e-8, + cached_content_token_count: 2.5e-8, + output: 4e-7, + output_text: 4e-7, + output_modality_1: 4e-7, + candidates_token_count: 4e-7, + candidatesTokenCount: 4e-7, + thoughtsTokenCount: 4e-7, + thoughts_token_count: 4e-7, + output_reasoning: 4e-7, + input_audio_tokens: 5e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-1-20250805", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", + startDate: "2025-08-05T15:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + input_tokens: 0.000015, + output: 0.000075, + output_tokens: 0.000075, + cache_creation_input_tokens: 0.00001875, + input_cache_creation: 0.00001875, + input_cache_creation_5m: 0.00001875, + input_cache_creation_1h: 0.00003, + cache_read_input_tokens: 0.0000015, + input_cache_read: 0.0000015, + }, + }, + ], + }, + { + modelName: "gpt-5", + matchPattern: "(?i)^(openai/)?(gpt-5)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-mini", + matchPattern: "(?i)^(openai/)?(gpt-5-mini)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_cached_tokens: 2.5e-8, + output: 0.000002, + input_cache_read: 2.5e-8, + output_reasoning_tokens: 0.000002, + output_reasoning: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-5-mini-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_cached_tokens: 2.5e-8, + output: 0.000002, + input_cache_read: 2.5e-8, + output_reasoning_tokens: 0.000002, + output_reasoning: 0.000002, + }, + }, + ], + }, + { + modelName: "gpt-5-nano", + matchPattern: "(?i)^(openai/)?(gpt-5-nano)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-8, + input_cached_tokens: 5e-9, + output: 4e-7, + input_cache_read: 5e-9, + output_reasoning_tokens: 4e-7, + output_reasoning: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-5-nano-2025-08-07", + matchPattern: "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", + startDate: "2025-08-11T08:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-8, + input_cached_tokens: 5e-9, + output: 4e-7, + input_cache_read: 5e-9, + output_reasoning_tokens: 4e-7, + output_reasoning: 4e-7, + }, + }, + ], + }, + { + modelName: "gpt-5-chat-latest", + matchPattern: "(?i)^(openai/)?(gpt-5-chat-latest)$", + startDate: "2025-08-07T16:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5-pro", + matchPattern: "(?i)^(openai/)?(gpt-5-pro)$", + startDate: "2025-10-07T08:03:54.727Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.00012, + output_reasoning_tokens: 0.00012, + output_reasoning: 0.00012, + }, + }, + ], + }, + { + modelName: "gpt-5-pro-2025-10-06", + matchPattern: "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", + startDate: "2025-10-07T08:03:54.727Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000015, + output: 0.00012, + output_reasoning_tokens: 0.00012, + output_reasoning: 0.00012, + }, + }, + ], + }, + { + modelName: "claude-haiku-4-5-20251001", + matchPattern: + "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", + startDate: "2025-10-16T08:20:44.558Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000001, + input_tokens: 0.000001, + output: 0.000005, + output_tokens: 0.000005, + cache_creation_input_tokens: 0.00000125, + input_cache_creation: 0.00000125, + input_cache_creation_5m: 0.00000125, + input_cache_creation_1h: 0.000002, + cache_read_input_tokens: 1e-7, + input_cache_read: 1e-7, + }, + }, + ], + }, + { + modelName: "gpt-5.1", + matchPattern: "(?i)^(openai/)?(gpt-5.1)$", + startDate: "2025-11-14T08:57:23.481Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "gpt-5.1-2025-11-13", + matchPattern: "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", + startDate: "2025-11-14T08:57:23.481Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_cached_tokens: 1.25e-7, + output: 0.00001, + input_cache_read: 1.25e-7, + output_reasoning_tokens: 0.00001, + output_reasoning: 0.00001, + }, + }, + ], + }, + { + modelName: "claude-opus-4-5-20251101", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", + startDate: "2025-11-24T20:53:27.571Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + input_tokens: 0.000005, + output: 0.000025, + output_tokens: 0.000025, + cache_creation_input_tokens: 0.00000625, + input_cache_creation: 0.00000625, + input_cache_creation_5m: 0.00000625, + input_cache_creation_1h: 0.00001, + cache_read_input_tokens: 5e-7, + input_cache_read: 5e-7, + }, + }, + ], + }, + { + modelName: "claude-sonnet-4-6", + matchPattern: + "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", + startDate: "2026-02-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000003, + input_tokens: 0.000003, + output: 0.000015, + output_tokens: 0.000015, + cache_creation_input_tokens: 0.00000375, + input_cache_creation: 0.00000375, + input_cache_creation_5m: 0.00000375, + input_cache_creation_1h: 0.000006, + cache_read_input_tokens: 3e-7, + input_cache_read: 3e-7, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "input", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "input", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000006, - "input_tokens": 0.000006, - "output": 0.0000225, - "output_tokens": 0.0000225, - "cache_creation_input_tokens": 0.0000075, - "input_cache_creation": 0.0000075, - "input_cache_creation_5m": 0.0000075, - "input_cache_creation_1h": 0.000012, - "cache_read_input_tokens": 6e-7, - "input_cache_read": 6e-7 - } - } - ] - }, - { - "modelName": "claude-opus-4-6", - "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", - "startDate": "2026-02-09T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000005, - "input_tokens": 0.000005, - "output": 0.000025, - "output_tokens": 0.000025, - "cache_creation_input_tokens": 0.00000625, - "input_cache_creation": 0.00000625, - "input_cache_creation_5m": 0.00000625, - "input_cache_creation_1h": 0.00001, - "cache_read_input_tokens": 5e-7, - "input_cache_read": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-2.5-pro", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", - "startDate": "2025-11-26T13:27:53.545Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000125, - "input_text": 0.00000125, - "input_modality_1": 0.00000125, - "prompt_token_count": 0.00000125, - "promptTokenCount": 0.00000125, - "input_cached_tokens": 1.25e-7, - "cached_content_token_count": 1.25e-7, - "output": 0.00001, - "output_text": 0.00001, - "output_modality_1": 0.00001, - "candidates_token_count": 0.00001, - "candidatesTokenCount": 0.00001, - "thoughtsTokenCount": 0.00001, - "thoughts_token_count": 0.00001, - "output_reasoning": 0.00001 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000006, + input_tokens: 0.000006, + output: 0.0000225, + output_tokens: 0.0000225, + cache_creation_input_tokens: 0.0000075, + input_cache_creation: 0.0000075, + input_cache_creation_5m: 0.0000075, + input_cache_creation_1h: 0.000012, + cache_read_input_tokens: 6e-7, + input_cache_read: 6e-7, + }, + }, + ], + }, + { + modelName: "claude-opus-4-6", + matchPattern: + "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", + startDate: "2026-02-09T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000005, + input_tokens: 0.000005, + output: 0.000025, + output_tokens: 0.000025, + cache_creation_input_tokens: 0.00000625, + input_cache_creation: 0.00000625, + input_cache_creation_5m: 0.00000625, + input_cache_creation_1h: 0.00001, + cache_read_input_tokens: 5e-7, + input_cache_read: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-2.5-pro", + matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", + startDate: "2025-11-26T13:27:53.545Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000125, + input_text: 0.00000125, + input_modality_1: 0.00000125, + prompt_token_count: 0.00000125, + promptTokenCount: 0.00000125, + input_cached_tokens: 1.25e-7, + cached_content_token_count: 1.25e-7, + output: 0.00001, + output_text: 0.00001, + output_modality_1: 0.00001, + candidates_token_count: 0.00001, + candidatesTokenCount: 0.00001, + thoughtsTokenCount: 0.00001, + thoughts_token_count: 0.00001, + output_reasoning: 0.00001, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.0000025, - "input_text": 0.0000025, - "input_modality_1": 0.0000025, - "prompt_token_count": 0.0000025, - "promptTokenCount": 0.0000025, - "input_cached_tokens": 2.5e-7, - "cached_content_token_count": 2.5e-7, - "output": 0.000015, - "output_text": 0.000015, - "output_modality_1": 0.000015, - "candidates_token_count": 0.000015, - "candidatesTokenCount": 0.000015, - "thoughtsTokenCount": 0.000015, - "thoughts_token_count": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gemini-3-pro-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", - "startDate": "2025-11-26T13:27:53.545Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_text": 0.000002, - "input_modality_1": 0.000002, - "prompt_token_count": 0.000002, - "promptTokenCount": 0.000002, - "input_cached_tokens": 2e-7, - "cached_content_token_count": 2e-7, - "output": 0.000012, - "output_text": 0.000012, - "output_modality_1": 0.000012, - "candidates_token_count": 0.000012, - "candidatesTokenCount": 0.000012, - "thoughtsTokenCount": 0.000012, - "thoughts_token_count": 0.000012, - "output_reasoning": 0.000012 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.0000025, + input_text: 0.0000025, + input_modality_1: 0.0000025, + prompt_token_count: 0.0000025, + promptTokenCount: 0.0000025, + input_cached_tokens: 2.5e-7, + cached_content_token_count: 2.5e-7, + output: 0.000015, + output_text: 0.000015, + output_modality_1: 0.000015, + candidates_token_count: 0.000015, + candidatesTokenCount: 0.000015, + thoughtsTokenCount: 0.000015, + thoughts_token_count: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gemini-3-pro-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", + startDate: "2025-11-26T13:27:53.545Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_text: 0.000002, + input_modality_1: 0.000002, + prompt_token_count: 0.000002, + promptTokenCount: 0.000002, + input_cached_tokens: 2e-7, + cached_content_token_count: 2e-7, + output: 0.000012, + output_text: 0.000012, + output_modality_1: 0.000012, + candidates_token_count: 0.000012, + candidatesTokenCount: 0.000012, + thoughtsTokenCount: 0.000012, + thoughts_token_count: 0.000012, + output_reasoning: 0.000012, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000004, - "input_text": 0.000004, - "input_modality_1": 0.000004, - "prompt_token_count": 0.000004, - "promptTokenCount": 0.000004, - "input_cached_tokens": 4e-7, - "cached_content_token_count": 4e-7, - "output": 0.000018, - "output_text": 0.000018, - "output_modality_1": 0.000018, - "candidates_token_count": 0.000018, - "candidatesTokenCount": 0.000018, - "thoughtsTokenCount": 0.000018, - "thoughts_token_count": 0.000018, - "output_reasoning": 0.000018 - } - } - ] - }, - { - "modelName": "gemini-3.1-pro-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", - "startDate": "2026-02-19T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000002, - "input_modality_1": 0.000002, - "input_text": 0.000002, - "prompt_token_count": 0.000002, - "promptTokenCount": 0.000002, - "input_cached_tokens": 2e-7, - "cached_content_token_count": 2e-7, - "output": 0.000012, - "output_text": 0.000012, - "output_modality_1": 0.000012, - "candidates_token_count": 0.000012, - "candidatesTokenCount": 0.000012, - "thoughtsTokenCount": 0.000012, - "thoughts_token_count": 0.000012, - "output_reasoning": 0.000012 - } - }, - { - "name": "Large Context", - "isDefault": false, - "priority": 1, - "conditions": [ + prices: { + input: 0.000004, + input_text: 0.000004, + input_modality_1: 0.000004, + prompt_token_count: 0.000004, + promptTokenCount: 0.000004, + input_cached_tokens: 4e-7, + cached_content_token_count: 4e-7, + output: 0.000018, + output_text: 0.000018, + output_modality_1: 0.000018, + candidates_token_count: 0.000018, + candidatesTokenCount: 0.000018, + thoughtsTokenCount: 0.000018, + thoughts_token_count: 0.000018, + output_reasoning: 0.000018, + }, + }, + ], + }, + { + modelName: "gemini-3.1-pro-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", + startDate: "2026-02-19T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000002, + input_modality_1: 0.000002, + input_text: 0.000002, + prompt_token_count: 0.000002, + promptTokenCount: 0.000002, + input_cached_tokens: 2e-7, + cached_content_token_count: 2e-7, + output: 0.000012, + output_text: 0.000012, + output_modality_1: 0.000012, + candidates_token_count: 0.000012, + candidatesTokenCount: 0.000012, + thoughtsTokenCount: 0.000012, + thoughts_token_count: 0.000012, + output_reasoning: 0.000012, + }, + }, + { + name: "Large Context", + isDefault: false, + priority: 1, + conditions: [ { - "usageDetailPattern": "(input|prompt|cached)", - "operator": "gt", - "value": 200000 - } + usageDetailPattern: "(input|prompt|cached)", + operator: "gt", + value: 200000, + }, ], - "prices": { - "input": 0.000004, - "input_modality_1": 0.000004, - "input_text": 0.000004, - "prompt_token_count": 0.000004, - "promptTokenCount": 0.000004, - "input_cached_tokens": 4e-7, - "cached_content_token_count": 4e-7, - "output": 0.000018, - "output_text": 0.000018, - "output_modality_1": 0.000018, - "candidates_token_count": 0.000018, - "candidatesTokenCount": 0.000018, - "thoughtsTokenCount": 0.000018, - "thoughts_token_count": 0.000018, - "output_reasoning": 0.000018 - } - } - ] - }, - { - "modelName": "gpt-5.2", - "matchPattern": "(?i)^(openai/)?(gpt-5.2)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000175, - "input_cached_tokens": 1.75e-7, - "input_cache_read": 1.75e-7, - "output": 0.000014, - "output_reasoning_tokens": 0.000014, - "output_reasoning": 0.000014 - } - } - ] - }, - { - "modelName": "gpt-5.2-2025-12-11", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00000175, - "input_cached_tokens": 1.75e-7, - "input_cache_read": 1.75e-7, - "output": 0.000014, - "output_reasoning_tokens": 0.000014, - "output_reasoning": 0.000014 - } - } - ] - }, - { - "modelName": "gpt-5.2-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000021, - "output": 0.000168, - "output_reasoning_tokens": 0.000168, - "output_reasoning": 0.000168 - } - } - ] - }, - { - "modelName": "gpt-5.2-pro-2025-12-11", - "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", - "startDate": "2025-12-12T09:00:06.513Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.000021, - "output": 0.000168, - "output_reasoning_tokens": 0.000168, - "output_reasoning": 0.000168 - } - } - ] - }, - { - "modelName": "gpt-5.4", - "matchPattern": "(?i)^(openai/)?(gpt-5.4)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 2.5e-7, - "input_cache_read": 2.5e-7, - "output": 0.000015, - "output_reasoning_tokens": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-5.4-pro", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00018, - "output_reasoning_tokens": 0.00018, - "output_reasoning": 0.00018 - } - } - ] - }, - { - "modelName": "gpt-5.4-2026-03-05", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.0000025, - "input_cached_tokens": 2.5e-7, - "input_cache_read": 2.5e-7, - "output": 0.000015, - "output_reasoning_tokens": 0.000015, - "output_reasoning": 0.000015 - } - } - ] - }, - { - "modelName": "gpt-5.4-pro-2026-03-05", - "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", - "startDate": "2026-03-05T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 0.00003, - "output": 0.00018, - "output_reasoning_tokens": 0.00018, - "output_reasoning": 0.00018 - } - } - ] - }, - { - "modelName": "gpt-5.4-mini", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 0.0000045 - } - } - ] - }, - { - "modelName": "gpt-5.4-mini-2026-03-17", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 7.5e-7, - "input_cached_tokens": 7.5e-8, - "input_cache_read": 7.5e-8, - "output": 0.0000045 - } - } - ] - }, - { - "modelName": "gpt-5.4-nano", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2e-7, - "input_cached_tokens": 2e-8, - "input_cache_read": 2e-8, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gpt-5.4-nano-2026-03-17", - "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", - "startDate": "2026-03-18T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2e-7, - "input_cached_tokens": 2e-8, - "input_cache_read": 2e-8, - "output": 0.00000125 - } - } - ] - }, - { - "modelName": "gemini-3-flash-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", - "startDate": "2025-12-21T12:01:42.282Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 5e-7, - "input_text": 5e-7, - "input_modality_1": 5e-7, - "prompt_token_count": 5e-7, - "promptTokenCount": 5e-7, - "input_cached_tokens": 5e-8, - "cached_content_token_count": 5e-8, - "output": 0.000003, - "output_text": 0.000003, - "output_modality_1": 0.000003, - "candidates_token_count": 0.000003, - "candidatesTokenCount": 0.000003, - "thoughtsTokenCount": 0.000003, - "thoughts_token_count": 0.000003, - "output_reasoning": 0.000003 - } - } - ] - }, - { - "modelName": "gemini-3.1-flash-lite-preview", - "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", - "startDate": "2026-03-03T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input": 2.5e-7, - "input_modality_1": 2.5e-7, - "input_text": 2.5e-7, - "prompt_token_count": 2.5e-7, - "promptTokenCount": 2.5e-7, - "input_cached_tokens": 2.5e-8, - "cached_content_token_count": 2.5e-8, - "output": 0.0000015, - "output_text": 0.0000015, - "output_modality_1": 0.0000015, - "candidates_token_count": 0.0000015, - "candidatesTokenCount": 0.0000015, - "thoughtsTokenCount": 0.0000015, - "thoughts_token_count": 0.0000015, - "output_reasoning": 0.0000015, - "input_audio_tokens": 5e-7 - } - } - ] - }, - { - "modelName": "gemini-live-2.5-flash-native-audio", - "matchPattern": "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", - "startDate": "2026-03-16T00:00:00.000Z", - "pricingTiers": [ - { - "name": "Standard", - "isDefault": true, - "priority": 0, - "conditions": [], - "prices": { - "input_text": 5e-7, - "input_audio": 0.000003, - "input_image": 0.000003, - "output_text": 0.000002, - "output_audio": 0.000012 - } - } - ] - } + prices: { + input: 0.000004, + input_modality_1: 0.000004, + input_text: 0.000004, + prompt_token_count: 0.000004, + promptTokenCount: 0.000004, + input_cached_tokens: 4e-7, + cached_content_token_count: 4e-7, + output: 0.000018, + output_text: 0.000018, + output_modality_1: 0.000018, + candidates_token_count: 0.000018, + candidatesTokenCount: 0.000018, + thoughtsTokenCount: 0.000018, + thoughts_token_count: 0.000018, + output_reasoning: 0.000018, + }, + }, + ], + }, + { + modelName: "gpt-5.2", + matchPattern: "(?i)^(openai/)?(gpt-5.2)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000175, + input_cached_tokens: 1.75e-7, + input_cache_read: 1.75e-7, + output: 0.000014, + output_reasoning_tokens: 0.000014, + output_reasoning: 0.000014, + }, + }, + ], + }, + { + modelName: "gpt-5.2-2025-12-11", + matchPattern: "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00000175, + input_cached_tokens: 1.75e-7, + input_cache_read: 1.75e-7, + output: 0.000014, + output_reasoning_tokens: 0.000014, + output_reasoning: 0.000014, + }, + }, + ], + }, + { + modelName: "gpt-5.2-pro", + matchPattern: "(?i)^(openai/)?(gpt-5.2-pro)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000021, + output: 0.000168, + output_reasoning_tokens: 0.000168, + output_reasoning: 0.000168, + }, + }, + ], + }, + { + modelName: "gpt-5.2-pro-2025-12-11", + matchPattern: "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", + startDate: "2025-12-12T09:00:06.513Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.000021, + output: 0.000168, + output_reasoning_tokens: 0.000168, + output_reasoning: 0.000168, + }, + }, + ], + }, + { + modelName: "gpt-5.4", + matchPattern: "(?i)^(openai/)?(gpt-5.4)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 2.5e-7, + input_cache_read: 2.5e-7, + output: 0.000015, + output_reasoning_tokens: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-5.4-pro", + matchPattern: "(?i)^(openai/)?(gpt-5.4-pro)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00018, + output_reasoning_tokens: 0.00018, + output_reasoning: 0.00018, + }, + }, + ], + }, + { + modelName: "gpt-5.4-2026-03-05", + matchPattern: "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.0000025, + input_cached_tokens: 2.5e-7, + input_cache_read: 2.5e-7, + output: 0.000015, + output_reasoning_tokens: 0.000015, + output_reasoning: 0.000015, + }, + }, + ], + }, + { + modelName: "gpt-5.4-pro-2026-03-05", + matchPattern: "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", + startDate: "2026-03-05T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 0.00003, + output: 0.00018, + output_reasoning_tokens: 0.00018, + output_reasoning: 0.00018, + }, + }, + ], + }, + { + modelName: "gpt-5.4-mini", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 0.0000045, + }, + }, + ], + }, + { + modelName: "gpt-5.4-mini-2026-03-17", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 7.5e-7, + input_cached_tokens: 7.5e-8, + input_cache_read: 7.5e-8, + output: 0.0000045, + }, + }, + ], + }, + { + modelName: "gpt-5.4-nano", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2e-7, + input_cached_tokens: 2e-8, + input_cache_read: 2e-8, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gpt-5.4-nano-2026-03-17", + matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", + startDate: "2026-03-18T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2e-7, + input_cached_tokens: 2e-8, + input_cache_read: 2e-8, + output: 0.00000125, + }, + }, + ], + }, + { + modelName: "gemini-3-flash-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", + startDate: "2025-12-21T12:01:42.282Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 5e-7, + input_text: 5e-7, + input_modality_1: 5e-7, + prompt_token_count: 5e-7, + promptTokenCount: 5e-7, + input_cached_tokens: 5e-8, + cached_content_token_count: 5e-8, + output: 0.000003, + output_text: 0.000003, + output_modality_1: 0.000003, + candidates_token_count: 0.000003, + candidatesTokenCount: 0.000003, + thoughtsTokenCount: 0.000003, + thoughts_token_count: 0.000003, + output_reasoning: 0.000003, + }, + }, + ], + }, + { + modelName: "gemini-3.1-flash-lite-preview", + matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", + startDate: "2026-03-03T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input: 2.5e-7, + input_modality_1: 2.5e-7, + input_text: 2.5e-7, + prompt_token_count: 2.5e-7, + promptTokenCount: 2.5e-7, + input_cached_tokens: 2.5e-8, + cached_content_token_count: 2.5e-8, + output: 0.0000015, + output_text: 0.0000015, + output_modality_1: 0.0000015, + candidates_token_count: 0.0000015, + candidatesTokenCount: 0.0000015, + thoughtsTokenCount: 0.0000015, + thoughts_token_count: 0.0000015, + output_reasoning: 0.0000015, + input_audio_tokens: 5e-7, + }, + }, + ], + }, + { + modelName: "gemini-live-2.5-flash-native-audio", + matchPattern: "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", + startDate: "2026-03-16T00:00:00.000Z", + pricingTiers: [ + { + name: "Standard", + isDefault: true, + priority: 0, + conditions: [], + prices: { + input_text: 5e-7, + input_audio: 0.000003, + input_image: 0.000003, + output_text: 0.000002, + output_audio: 0.000012, + }, + }, + ], + }, ]; diff --git a/internal-packages/llm-model-catalog/src/model-catalog.json b/internal-packages/llm-model-catalog/src/model-catalog.json index 0f602683528..23056f5fdae 100644 --- a/internal-packages/llm-model-catalog/src/model-catalog.json +++ b/internal-packages/llm-model-catalog/src/model-catalog.json @@ -27,9 +27,7 @@ "description": "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", "contextWindow": 9000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -44,9 +42,7 @@ "description": "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", "contextWindow": 9000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": null, "isHidden": true, "supportsStructuredOutput": false, @@ -61,9 +57,7 @@ "description": "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", "contextWindow": 100000, "maxOutputTokens": null, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -78,9 +72,7 @@ "description": "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", "contextWindow": 100000, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-07-11", "isHidden": true, "supportsStructuredOutput": false, @@ -95,10 +87,7 @@ "description": "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "tool_use" - ], + "capabilities": ["streaming", "tool_use"], "releaseDate": "2023-11-21", "isHidden": true, "supportsStructuredOutput": false, @@ -113,12 +102,7 @@ "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-10-22", "isHidden": false, "supportsStructuredOutput": true, @@ -133,12 +117,7 @@ "description": "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -153,12 +132,7 @@ "description": "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-13", "isHidden": true, "supportsStructuredOutput": false, @@ -173,12 +147,7 @@ "description": "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-04", "isHidden": true, "supportsStructuredOutput": true, @@ -193,12 +162,7 @@ "description": "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", "contextWindow": 200000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-03-04", "isHidden": true, "supportsStructuredOutput": true, @@ -213,12 +177,7 @@ "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-10-29", "isHidden": false, "supportsStructuredOutput": true, @@ -233,12 +192,7 @@ "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -253,12 +207,7 @@ "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", "contextWindow": 200000, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-06-20", "isHidden": true, "supportsStructuredOutput": true, @@ -273,13 +222,7 @@ "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-02-24", "isHidden": true, "supportsStructuredOutput": true, @@ -294,13 +237,7 @@ "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-02-24", "isHidden": true, "supportsStructuredOutput": true, @@ -315,13 +252,7 @@ "description": "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-10-01", "isHidden": false, "supportsStructuredOutput": true, @@ -336,9 +267,7 @@ "description": "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", "contextWindow": 100000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -353,9 +282,7 @@ "description": "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", "contextWindow": 100000, "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-08-09", "isHidden": true, "supportsStructuredOutput": false, @@ -370,13 +297,7 @@ "description": "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", "contextWindow": 200000, "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-08-05", "isHidden": true, "supportsStructuredOutput": true, @@ -391,13 +312,7 @@ "description": "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", "contextWindow": 200000, "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": false, "supportsStructuredOutput": true, @@ -412,13 +327,7 @@ "description": "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-11-01", "isHidden": false, "supportsStructuredOutput": true, @@ -433,13 +342,7 @@ "description": "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", "contextWindow": 1000000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-02-05", "isHidden": false, "supportsStructuredOutput": true, @@ -454,13 +357,7 @@ "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": true, "supportsStructuredOutput": true, @@ -475,13 +372,7 @@ "description": "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-09-29", "isHidden": false, "supportsStructuredOutput": true, @@ -496,13 +387,7 @@ "description": "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", "contextWindow": 1000000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-02-17", "isHidden": false, "supportsStructuredOutput": true, @@ -517,13 +402,7 @@ "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", "contextWindow": 200000, "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-05-14", "isHidden": true, "supportsStructuredOutput": true, @@ -538,11 +417,7 @@ "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", "contextWindow": 32760, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -557,11 +432,7 @@ "description": "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", "contextWindow": 30720, "maxOutputTokens": 2048, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2024-02-15", "isHidden": true, "supportsStructuredOutput": false, @@ -576,11 +447,7 @@ "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", "contextWindow": 32760, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -595,13 +462,7 @@ "description": "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", "contextWindow": 2097152, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "audio_input" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "audio_input"], "releaseDate": "2024-02-15", "isHidden": true, "supportsStructuredOutput": true, @@ -662,12 +523,7 @@ "description": "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", "contextWindow": 1048576, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-05", "isHidden": true, "supportsStructuredOutput": true, @@ -682,12 +538,7 @@ "description": "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", "contextWindow": 1048576, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-05", "isHidden": true, "supportsStructuredOutput": true, @@ -863,11 +714,7 @@ "description": "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", "contextWindow": 32768, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-12-13", "isHidden": true, "supportsStructuredOutput": false, @@ -882,12 +729,7 @@ "description": "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-03-01", "isHidden": true, "supportsStructuredOutput": false, @@ -902,12 +744,7 @@ "description": "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -922,10 +759,7 @@ "description": "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "fine_tunable" - ], + "capabilities": ["streaming", "fine_tunable"], "releaseDate": "2023-03-01", "isHidden": true, "supportsStructuredOutput": false, @@ -940,12 +774,7 @@ "description": "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -960,12 +789,7 @@ "description": "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", "contextWindow": 16385, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -980,12 +804,7 @@ "description": "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", "contextWindow": 16384, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable", - "tool_use" - ], + "capabilities": ["streaming", "json_mode", "fine_tunable", "tool_use"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1000,11 +819,7 @@ "description": "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", "contextWindow": 16384, "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1019,9 +834,7 @@ "description": "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", "contextWindow": 4096, "maxOutputTokens": 4096, - "capabilities": [ - "fine_tunable" - ], + "capabilities": ["fine_tunable"], "releaseDate": "2023-09-19", "isHidden": true, "supportsStructuredOutput": false, @@ -1036,12 +849,7 @@ "description": "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", "contextWindow": 8192, "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1056,11 +864,7 @@ "description": "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -1075,9 +879,7 @@ "description": "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", "contextWindow": 8192, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1092,12 +894,7 @@ "description": "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", "contextWindow": 8192, "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1112,12 +909,7 @@ "description": "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1132,11 +924,7 @@ "description": "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1151,9 +939,7 @@ "description": "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2023-03-14", "isHidden": true, "supportsStructuredOutput": false, @@ -1168,11 +954,7 @@ "description": "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", "contextWindow": 32768, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-06-13", "isHidden": true, "supportsStructuredOutput": false, @@ -1187,11 +969,7 @@ "description": "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1206,12 +984,7 @@ "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-04-09", "isHidden": false, "supportsStructuredOutput": false, @@ -1226,12 +999,7 @@ "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-04-09", "isHidden": false, "supportsStructuredOutput": false, @@ -1246,12 +1014,7 @@ "description": "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-01-25", "isHidden": true, "supportsStructuredOutput": false, @@ -1266,12 +1029,7 @@ "description": "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2023-11-06", "isHidden": true, "supportsStructuredOutput": false, @@ -1286,12 +1044,7 @@ "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1306,12 +1059,7 @@ "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1326,13 +1074,7 @@ "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", "contextWindow": 1000000, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1347,13 +1089,7 @@ "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", "contextWindow": 1000000, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1368,12 +1104,7 @@ "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1388,12 +1119,7 @@ "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", "contextWindow": 1047576, "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-04-14", "isHidden": false, "supportsStructuredOutput": true, @@ -1408,12 +1134,7 @@ "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-27", "isHidden": true, "supportsStructuredOutput": true, @@ -1428,12 +1149,7 @@ "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-02-27", "isHidden": true, "supportsStructuredOutput": true, @@ -1540,12 +1256,7 @@ "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1560,12 +1271,7 @@ "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1580,13 +1286,7 @@ "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-07-18", "isHidden": false, "supportsStructuredOutput": true, @@ -1601,13 +1301,7 @@ "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], "releaseDate": "2024-07-18", "isHidden": false, "supportsStructuredOutput": true, @@ -1622,12 +1316,7 @@ "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1642,12 +1331,7 @@ "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", "contextWindow": 128000, "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], + "capabilities": ["audio_input", "audio_output", "tool_use", "streaming"], "releaseDate": "2024-10-01", "isHidden": false, "supportsStructuredOutput": false, @@ -1706,12 +1390,7 @@ "description": "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", "contextWindow": 128000, "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1726,12 +1405,7 @@ "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1746,12 +1420,7 @@ "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1766,12 +1435,7 @@ "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1786,12 +1450,7 @@ "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-08-07", "isHidden": false, "supportsStructuredOutput": true, @@ -1806,12 +1465,7 @@ "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-10-06", "isHidden": false, "supportsStructuredOutput": true, @@ -1826,12 +1480,7 @@ "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-10-06", "isHidden": false, "supportsStructuredOutput": true, @@ -1846,12 +1495,7 @@ "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-11-13", "isHidden": false, "supportsStructuredOutput": true, @@ -1866,12 +1510,7 @@ "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2025-11-13", "isHidden": false, "supportsStructuredOutput": true, @@ -1886,13 +1525,7 @@ "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": true, @@ -1907,13 +1540,7 @@ "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": true, @@ -1928,12 +1555,7 @@ "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": false, @@ -1948,12 +1570,7 @@ "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2025-12-11", "isHidden": false, "supportsStructuredOutput": false, @@ -1968,13 +1585,7 @@ "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "code_execution"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": true, @@ -1989,13 +1600,7 @@ "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "code_execution"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": true, @@ -2010,13 +1615,7 @@ "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2031,13 +1630,7 @@ "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2052,12 +1645,7 @@ "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2072,12 +1660,7 @@ "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", "contextWindow": 400000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2026-03-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2092,12 +1675,7 @@ "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": false, @@ -2112,12 +1690,7 @@ "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", "contextWindow": 1050000, "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "extended_thinking"], "releaseDate": "2026-03-05", "isHidden": false, "supportsStructuredOutput": false, @@ -2132,12 +1705,7 @@ "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-12-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2152,12 +1720,7 @@ "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode"], "releaseDate": "2024-12-17", "isHidden": false, "supportsStructuredOutput": true, @@ -2172,10 +1735,7 @@ "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", "contextWindow": 128000, "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], + "capabilities": ["streaming", "json_mode"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2190,10 +1750,7 @@ "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", "contextWindow": 128000, "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], + "capabilities": ["streaming", "json_mode"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2208,9 +1765,7 @@ "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", "contextWindow": 128000, "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2225,9 +1780,7 @@ "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", "contextWindow": 128000, "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], + "capabilities": ["streaming"], "releaseDate": "2024-09-12", "isHidden": true, "supportsStructuredOutput": false, @@ -2242,12 +1795,7 @@ "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "json_mode", "extended_thinking"], "releaseDate": "2025-03-19", "isHidden": false, "supportsStructuredOutput": true, @@ -2262,12 +1810,7 @@ "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "json_mode", "extended_thinking"], "releaseDate": "2025-03-19", "isHidden": false, "supportsStructuredOutput": true, @@ -2282,13 +1825,7 @@ "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2303,13 +1840,7 @@ "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2324,12 +1855,7 @@ "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-01-31", "isHidden": false, "supportsStructuredOutput": true, @@ -2344,12 +1870,7 @@ "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-01-31", "isHidden": false, "supportsStructuredOutput": true, @@ -2364,13 +1885,7 @@ "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-06-10", "isHidden": false, "supportsStructuredOutput": true, @@ -2385,13 +1900,7 @@ "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-06-10", "isHidden": false, "supportsStructuredOutput": true, @@ -2406,13 +1915,7 @@ "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, @@ -2427,13 +1930,7 @@ "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", "contextWindow": 200000, "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], + "capabilities": ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], "releaseDate": "2025-04-16", "isHidden": false, "supportsStructuredOutput": true, diff --git a/internal-packages/llm-model-catalog/src/modelCatalog.ts b/internal-packages/llm-model-catalog/src/modelCatalog.ts index 71ae921c3e7..d90eb1a992b 100644 --- a/internal-packages/llm-model-catalog/src/modelCatalog.ts +++ b/internal-packages/llm-model-catalog/src/modelCatalog.ts @@ -5,677 +5,564 @@ import type { ModelCatalogEntry } from "./types.js"; export const modelCatalog: Record = { "chatgpt-4o-latest": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T10:55:46.469Z", - "baseModelName": "chatgpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T10:55:46.469Z", + baseModelName: "chatgpt-4o", }, "claude-1.1": { - "provider": "anthropic", - "description": "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", - "contextWindow": 9000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:47.906Z", - "baseModelName": null + provider: "anthropic", + description: + "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", + contextWindow: 9000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:47.906Z", + baseModelName: null, }, "claude-1.2": { - "provider": "anthropic", - "description": "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", - "contextWindow": 9000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": null, - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:46.760Z", - "baseModelName": null + provider: "anthropic", + description: + "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", + contextWindow: 9000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: null, + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:46.760Z", + baseModelName: null, }, "claude-1.3": { - "provider": "anthropic", - "description": "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", - "contextWindow": 100000, - "maxOutputTokens": null, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": null, - "resolvedAt": "2026-03-24T10:55:46.227Z", - "baseModelName": null + provider: "anthropic", + description: + "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", + contextWindow: 100000, + maxOutputTokens: null, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: null, + resolvedAt: "2026-03-24T10:55:46.227Z", + baseModelName: null, }, "claude-2.0": { - "provider": "anthropic", - "description": "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", - "contextWindow": 100000, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-07-11", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-02-01", - "resolvedAt": "2026-03-24T10:55:45.922Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", + contextWindow: 100000, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-07-11", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-02-01", + resolvedAt: "2026-03-24T10:55:45.922Z", + baseModelName: null, }, "claude-2.1": { - "provider": "anthropic", - "description": "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "tool_use" - ], - "releaseDate": "2023-11-21", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:56:22.743Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["streaming", "tool_use"], + releaseDate: "2023-11-21", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:56:22.743Z", + baseModelName: null, }, "claude-3-5-haiku-20241022": { - "provider": "anthropic", - "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-10-22", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-07-01", - "resolvedAt": "2026-03-24T10:56:25.724Z", - "baseModelName": "claude-3-5-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-10-22", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-07-01", + resolvedAt: "2026-03-24T10:56:25.724Z", + baseModelName: "claude-3-5-haiku", }, "claude-3-5-sonnet-20240620": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:56:35.401Z", - "baseModelName": "claude-3-5-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:56:35.401Z", + baseModelName: "claude-3-5-sonnet", }, "claude-3-haiku-20240307": { - "provider": "anthropic", - "description": "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-08-01", - "resolvedAt": "2026-03-24T10:56:25.288Z", - "baseModelName": "claude-3-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-08-01", + resolvedAt: "2026-03-24T10:56:25.288Z", + baseModelName: "claude-3-haiku", }, "claude-3-opus-20240229": { - "provider": "anthropic", - "description": "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-04", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-08-01", - "resolvedAt": "2026-03-24T10:56:26.008Z", - "baseModelName": "claude-3-opus" + provider: "anthropic", + description: + "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-04", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-08-01", + resolvedAt: "2026-03-24T10:56:26.008Z", + baseModelName: "claude-3-opus", }, "claude-3-sonnet-20240229": { - "provider": "anthropic", - "description": "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", - "contextWindow": 200000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-03-04", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-02-01", - "resolvedAt": "2026-03-24T10:56:59.532Z", - "baseModelName": "claude-3-sonnet" + provider: "anthropic", + description: + "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", + contextWindow: 200000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-03-04", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-02-01", + resolvedAt: "2026-03-24T10:56:59.532Z", + baseModelName: "claude-3-sonnet", }, "claude-3.5-haiku-latest": { - "provider": "anthropic", - "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-10-29", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-07-01", - "resolvedAt": "2026-03-24T10:57:04.392Z", - "baseModelName": "claude-3.5-haiku" + provider: "anthropic", + description: + "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-10-29", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-07-01", + resolvedAt: "2026-03-24T10:57:04.392Z", + baseModelName: "claude-3.5-haiku", }, "claude-3.5-sonnet-20241022": { - "provider": "anthropic", - "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:57:13.346Z", - "baseModelName": "claude-3.5-sonnet" + provider: "anthropic", + description: + "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:57:13.346Z", + baseModelName: "claude-3.5-sonnet", }, "claude-3.5-sonnet-latest": { - "provider": "anthropic", - "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - "contextWindow": 200000, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-06-20", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:57:13.346Z", - "baseModelName": "claude-3.5-sonnet" + provider: "anthropic", + description: + "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + contextWindow: 200000, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-06-20", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:57:13.346Z", + baseModelName: "claude-3.5-sonnet", }, "claude-3.7-sonnet-20250219": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-02-24", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-04-01", - "resolvedAt": "2026-03-24T10:57:12.967Z", - "baseModelName": "claude-3.7-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-02-24", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-04-01", + resolvedAt: "2026-03-24T10:57:12.967Z", + baseModelName: "claude-3.7-sonnet", }, "claude-3.7-sonnet-latest": { - "provider": "anthropic", - "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-02-24", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-04-01", - "resolvedAt": "2026-03-24T10:57:12.967Z", - "baseModelName": "claude-3.7-sonnet" + provider: "anthropic", + description: + "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-02-24", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-04-01", + resolvedAt: "2026-03-24T10:57:12.967Z", + baseModelName: "claude-3.7-sonnet", }, "claude-haiku-4-5-20251001": { - "provider": "anthropic", - "description": "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-10-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-07-01", - "resolvedAt": "2026-03-24T10:57:29.685Z", - "baseModelName": "claude-haiku-4-5" + provider: "anthropic", + description: + "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-10-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-07-01", + resolvedAt: "2026-03-24T10:57:29.685Z", + baseModelName: "claude-haiku-4-5", }, "claude-instant-1": { - "provider": "anthropic", - "description": "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", - "contextWindow": 100000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-01-06", - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:57:36.888Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", + contextWindow: 100000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-01-06", + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:57:36.888Z", + baseModelName: null, }, "claude-instant-1.2": { - "provider": "anthropic", - "description": "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", - "contextWindow": 100000, - "maxOutputTokens": 8191, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-08-09", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-01-01", - "resolvedAt": "2026-03-24T10:57:41.865Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", + contextWindow: 100000, + maxOutputTokens: 8191, + capabilities: ["streaming"], + releaseDate: "2023-08-09", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-01-01", + resolvedAt: "2026-03-24T10:57:41.865Z", + baseModelName: null, }, "claude-opus-4-1-20250805": { - "provider": "anthropic", - "description": "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", - "contextWindow": 200000, - "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-08-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:36.876Z", - "baseModelName": "claude-opus-4-1" + provider: "anthropic", + description: + "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", + contextWindow: 200000, + maxOutputTokens: 32000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-08-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:36.876Z", + baseModelName: "claude-opus-4-1", }, "claude-opus-4-20250514": { - "provider": "anthropic", - "description": "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", - "contextWindow": 200000, - "maxOutputTokens": 32000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:47.518Z", - "baseModelName": "claude-opus-4" + provider: "anthropic", + description: + "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", + contextWindow: 200000, + maxOutputTokens: 32000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:47.518Z", + baseModelName: "claude-opus-4", }, "claude-opus-4-5-20251101": { - "provider": "anthropic", - "description": "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-11-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:48.961Z", - "baseModelName": "claude-opus-4-5" + provider: "anthropic", + description: + "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-11-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:48.961Z", + baseModelName: "claude-opus-4-5", }, "claude-opus-4-6": { - "provider": "anthropic", - "description": "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", - "contextWindow": 1000000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-02-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-05-01", - "resolvedAt": "2026-03-24T10:58:42.061Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", + contextWindow: 1000000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-02-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-05-01", + resolvedAt: "2026-03-24T10:58:42.061Z", + baseModelName: null, }, "claude-sonnet-4-20250514": { - "provider": "anthropic", - "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:39.601Z", - "baseModelName": "claude-sonnet-4" + provider: "anthropic", + description: + "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:39.601Z", + baseModelName: "claude-sonnet-4", }, "claude-sonnet-4-5-20250929": { - "provider": "anthropic", - "description": "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-09-29", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T10:59:54.426Z", - "baseModelName": "claude-sonnet-4-5" + provider: "anthropic", + description: + "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-09-29", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T10:59:54.426Z", + baseModelName: "claude-sonnet-4-5", }, "claude-sonnet-4-6": { - "provider": "anthropic", - "description": "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", - "contextWindow": 1000000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-02-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2026-01-01", - "resolvedAt": "2026-03-24T10:59:59.014Z", - "baseModelName": null + provider: "anthropic", + description: + "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", + contextWindow: 1000000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-02-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2026-01-01", + resolvedAt: "2026-03-24T10:59:59.014Z", + baseModelName: null, }, "claude-sonnet-4-latest": { - "provider": "anthropic", - "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - "contextWindow": 200000, - "maxOutputTokens": 64000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-05-14", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-03-01", - "resolvedAt": "2026-03-24T10:58:39.601Z", - "baseModelName": "claude-sonnet-4" + provider: "anthropic", + description: + "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + contextWindow: 200000, + maxOutputTokens: 64000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-05-14", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-03-01", + resolvedAt: "2026-03-24T10:58:39.601Z", + baseModelName: "claude-sonnet-4", }, "gemini-1.0-pro": { - "provider": "google", - "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - "contextWindow": 32760, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:26.767Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + contextWindow: 32760, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:26.767Z", + baseModelName: null, }, "gemini-1.0-pro-001": { - "provider": "google", - "description": "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", - "contextWindow": 30720, - "maxOutputTokens": 2048, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-02-15", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:27.391Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", + contextWindow: 30720, + maxOutputTokens: 2048, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2024-02-15", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:27.391Z", + baseModelName: null, }, "gemini-1.0-pro-latest": { - "provider": "google", - "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - "contextWindow": 32760, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-02-15", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T10:59:26.767Z", - "baseModelName": "gemini-1.0-pro" + provider: "google", + description: + "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + contextWindow: 32760, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-02-15", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T10:59:26.767Z", + baseModelName: "gemini-1.0-pro", }, "gemini-1.5-pro-latest": { - "provider": "google", - "description": "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", - "contextWindow": 2097152, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "audio_input" - ], - "releaseDate": "2024-02-15", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-24", - "knowledgeCutoff": "2024-04-01", - "resolvedAt": "2026-03-24T10:59:25.463Z", - "baseModelName": "gemini-1.5-pro" + provider: "google", + description: + "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", + contextWindow: 2097152, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "audio_input"], + releaseDate: "2024-02-15", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-24", + knowledgeCutoff: "2024-04-01", + resolvedAt: "2026-03-24T10:59:25.463Z", + baseModelName: "gemini-1.5-pro", }, "gemini-2.0-flash": { - "provider": "google", - "description": "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution", - "audio_input" - ], - "releaseDate": "2025-02-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:01:15.429Z", - "baseModelName": null + provider: "google", + description: + "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution", "audio_input"], + releaseDate: "2025-02-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:01:15.429Z", + baseModelName: null, }, "gemini-2.0-flash-001": { - "provider": "google", - "description": "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ + provider: "google", + description: + "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: [ "vision", "tool_use", "streaming", @@ -683,1890 +570,1614 @@ export const modelCatalog: Record = { "audio_input", "image_generation", "audio_output", - "code_execution" + "code_execution", ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-08-01", - "resolvedAt": "2026-03-24T11:01:04.084Z", - "baseModelName": null + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-08-01", + resolvedAt: "2026-03-24T11:01:04.084Z", + baseModelName: null, }, "gemini-2.0-flash-lite-preview": { - "provider": "google", - "description": "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-01", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:00:56.775Z", - "baseModelName": null + provider: "google", + description: + "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-01", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:00:56.775Z", + baseModelName: null, }, "gemini-2.0-flash-lite-preview-02-05": { - "provider": "google", - "description": "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", - "contextWindow": 1048576, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-05", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-12-09", - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:01:34.165Z", - "baseModelName": null + provider: "google", + description: + "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", + contextWindow: 1048576, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-05", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-12-09", + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:01:34.165Z", + baseModelName: null, }, "gemini-2.5-flash": { - "provider": "google", - "description": "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-06-01", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:01:25.200Z", - "baseModelName": null + releaseDate: "2025-06-01", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:01:25.200Z", + baseModelName: null, }, "gemini-2.5-flash-lite": { - "provider": "google", - "description": "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", - "contextWindow": 1048576, - "maxOutputTokens": 65535, - "capabilities": [ + provider: "google", + description: + "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", + contextWindow: 1048576, + maxOutputTokens: 65535, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-07-22", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-07-22", - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:30.060Z", - "baseModelName": null + releaseDate: "2025-07-22", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-07-22", + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:30.060Z", + baseModelName: null, }, "gemini-2.5-pro": { - "provider": "google", - "description": "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", - "contextWindow": 1048576, - "maxOutputTokens": 65535, - "capabilities": [ + provider: "google", + description: + "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", + contextWindow: 1048576, + maxOutputTokens: 65535, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-03-25", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-06-17", - "knowledgeCutoff": "2025-01-31", - "resolvedAt": "2026-03-24T11:02:25.573Z", - "baseModelName": null + releaseDate: "2025-03-25", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-06-17", + knowledgeCutoff: "2025-01-31", + resolvedAt: "2026-03-24T11:02:25.573Z", + baseModelName: null, }, "gemini-3-flash-preview": { - "provider": "google", - "description": "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:13.388Z", - "baseModelName": null + releaseDate: "2025-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:13.388Z", + baseModelName: null, }, "gemini-3-pro-preview": { - "provider": "google", - "description": "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2025-11-01", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-03-09", - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:29.313Z", - "baseModelName": null + releaseDate: "2025-11-01", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-03-09", + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:29.313Z", + baseModelName: null, }, "gemini-3.1-flash-lite-preview": { - "provider": "google", - "description": "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2026-03-03", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:02:29.253Z", - "baseModelName": null + releaseDate: "2026-03-03", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:02:29.253Z", + baseModelName: null, }, "gemini-3.1-pro-preview": { - "provider": "google", - "description": "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", - "contextWindow": 1048576, - "maxOutputTokens": 65536, - "capabilities": [ + provider: "google", + description: + "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", + contextWindow: 1048576, + maxOutputTokens: 65536, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input" + "audio_input", ], - "releaseDate": "2026-02-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:03:33.071Z", - "baseModelName": null + releaseDate: "2026-02-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:03:33.071Z", + baseModelName: null, }, "gemini-pro": { - "provider": "google", - "description": "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", - "contextWindow": 32768, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-12-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-04-09", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:03:45.401Z", - "baseModelName": null + provider: "google", + description: + "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", + contextWindow: 32768, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-12-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2025-04-09", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:03:45.401Z", + baseModelName: null, }, "gpt-3.5-turbo": { - "provider": "openai", - "description": "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-03-01", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:11.412Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-03-01", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:11.412Z", + baseModelName: null, }, "gpt-3.5-turbo-0125": { - "provider": "openai", - "description": "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:11.310Z", - "baseModelName": null + provider: "openai", + description: + "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:11.310Z", + baseModelName: null, }, "gpt-3.5-turbo-0301": { - "provider": "openai", - "description": "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "fine_tunable" - ], - "releaseDate": "2023-03-01", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:03:12.060Z", - "baseModelName": null + provider: "openai", + description: + "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["streaming", "fine_tunable"], + releaseDate: "2023-03-01", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:03:12.060Z", + baseModelName: null, }, "gpt-3.5-turbo-0613": { - "provider": "openai", - "description": "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:04.463Z", - "baseModelName": null + provider: "openai", + description: + "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:04.463Z", + baseModelName: null, }, "gpt-3.5-turbo-1106": { - "provider": "openai", - "description": "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", - "contextWindow": 16385, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:23.054Z", - "baseModelName": null + provider: "openai", + description: + "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", + contextWindow: 16385, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:23.054Z", + baseModelName: null, }, "gpt-3.5-turbo-16k": { - "provider": "openai", - "description": "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", - "contextWindow": 16384, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable", - "tool_use" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:36.307Z", - "baseModelName": null + provider: "openai", + description: + "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", + contextWindow: 16384, + maxOutputTokens: 4096, + capabilities: ["streaming", "json_mode", "fine_tunable", "tool_use"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:36.307Z", + baseModelName: null, }, "gpt-3.5-turbo-16k-0613": { - "provider": "openai", - "description": "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", - "contextWindow": 16384, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-09-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:22.894Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", + contextWindow: 16384, + maxOutputTokens: 4096, + capabilities: ["streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-09-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:22.894Z", + baseModelName: null, }, "gpt-3.5-turbo-instruct": { - "provider": "openai", - "description": "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", - "contextWindow": 4096, - "maxOutputTokens": 4096, - "capabilities": [ - "fine_tunable" - ], - "releaseDate": "2023-09-19", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-01-27", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:04:22.309Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", + contextWindow: 4096, + maxOutputTokens: 4096, + capabilities: ["fine_tunable"], + releaseDate: "2023-09-19", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-01-27", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:04:22.309Z", + baseModelName: null, }, "gpt-4": { - "provider": "openai", - "description": "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", - "contextWindow": 8192, - "maxOutputTokens": 8192, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:04:36.773Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", + contextWindow: 8192, + maxOutputTokens: 8192, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:04:36.773Z", + baseModelName: null, }, "gpt-4-0125-preview": { - "provider": "openai", - "description": "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:04:54.196Z", - "baseModelName": null + provider: "openai", + description: + "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:04:54.196Z", + baseModelName: null, }, "gpt-4-0314": { - "provider": "openai", - "description": "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", - "contextWindow": 8192, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:14.112Z", - "baseModelName": null + provider: "openai", + description: + "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", + contextWindow: 8192, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:14.112Z", + baseModelName: null, }, "gpt-4-0613": { - "provider": "openai", - "description": "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", - "contextWindow": 8192, - "maxOutputTokens": 8192, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:13.885Z", - "baseModelName": null + provider: "openai", + description: + "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", + contextWindow: 8192, + maxOutputTokens: 8192, + capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:13.885Z", + baseModelName: null, }, "gpt-4-1106-preview": { - "provider": "openai", - "description": "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:05:12.960Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:05:12.960Z", + baseModelName: null, }, "gpt-4-32k": { - "provider": "openai", - "description": "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:14.584Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:14.584Z", + baseModelName: null, }, "gpt-4-32k-0314": { - "provider": "openai", - "description": "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "streaming" - ], - "releaseDate": "2023-03-14", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2024-06-13", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:32.044Z", - "baseModelName": null + provider: "openai", + description: + "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["streaming"], + releaseDate: "2023-03-14", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2024-06-13", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:32.044Z", + baseModelName: null, }, "gpt-4-32k-0613": { - "provider": "openai", - "description": "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", - "contextWindow": 32768, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-06-13", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2021-09-01", - "resolvedAt": "2026-03-24T11:05:53.070Z", - "baseModelName": null + provider: "openai", + description: + "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", + contextWindow: 32768, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-06-13", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2021-09-01", + resolvedAt: "2026-03-24T11:05:53.070Z", + baseModelName: null, }, "gpt-4-preview": { - "provider": "openai", - "description": "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-06-06", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:06:54.248Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-06-06", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:06:54.248Z", + baseModelName: null, }, "gpt-4-turbo": { - "provider": "openai", - "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-04-09", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:51.415Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-04-09", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:51.415Z", + baseModelName: null, }, "gpt-4-turbo-2024-04-09": { - "provider": "openai", - "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-04-09", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:51.415Z", - "baseModelName": "gpt-4-turbo" + provider: "openai", + description: + "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-04-09", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:51.415Z", + baseModelName: "gpt-4-turbo", }, "gpt-4-turbo-preview": { - "provider": "openai", - "description": "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-01-25", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-12-01", - "resolvedAt": "2026-03-24T11:05:52.346Z", - "baseModelName": null + provider: "openai", + description: + "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-01-25", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-12-01", + resolvedAt: "2026-03-24T11:05:52.346Z", + baseModelName: null, }, "gpt-4-turbo-vision": { - "provider": "openai", - "description": "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2023-11-06", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2024-12-06", - "knowledgeCutoff": "2023-04-01", - "resolvedAt": "2026-03-24T11:06:38.455Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2023-11-06", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2024-12-06", + knowledgeCutoff: "2023-04-01", + resolvedAt: "2026-03-24T11:06:38.455Z", + baseModelName: null, }, "gpt-4.1": { - "provider": "openai", - "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:07:00.439Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:07:00.439Z", + baseModelName: null, }, "gpt-4.1-2025-04-14": { - "provider": "openai", - "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:07:00.439Z", - "baseModelName": "gpt-4.1" + provider: "openai", + description: + "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:07:00.439Z", + baseModelName: "gpt-4.1", }, "gpt-4.1-mini": { - "provider": "openai", - "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - "contextWindow": 1000000, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:14.524Z", - "baseModelName": null + provider: "openai", + description: + "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + contextWindow: 1000000, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:14.524Z", + baseModelName: null, }, "gpt-4.1-mini-2025-04-14": { - "provider": "openai", - "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - "contextWindow": 1000000, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:14.524Z", - "baseModelName": "gpt-4.1-mini" + provider: "openai", + description: + "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + contextWindow: 1000000, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:14.524Z", + baseModelName: "gpt-4.1-mini", }, "gpt-4.1-nano": { - "provider": "openai", - "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:04.533Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:04.533Z", + baseModelName: null, }, "gpt-4.1-nano-2025-04-14": { - "provider": "openai", - "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - "contextWindow": 1047576, - "maxOutputTokens": 32768, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-04-14", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:08:04.533Z", - "baseModelName": "gpt-4.1-nano" + provider: "openai", + description: + "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + contextWindow: 1047576, + maxOutputTokens: 32768, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-04-14", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:08:04.533Z", + baseModelName: "gpt-4.1-nano", }, "gpt-4.5-preview": { - "provider": "openai", - "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-27", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-07-14", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:57.880Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-27", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-07-14", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:57.880Z", + baseModelName: null, }, "gpt-4.5-preview-2025-02-27": { - "provider": "openai", - "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-02-27", - "isHidden": true, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2025-07-14", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:57.880Z", - "baseModelName": "gpt-4.5-preview" + provider: "openai", + description: + "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-02-27", + isHidden: true, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2025-07-14", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:57.880Z", + baseModelName: "gpt-4.5-preview", }, "gpt-4o": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": null + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: null, }, "gpt-4o-2024-05-13": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-2024-08-06": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-2024-11-20": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable" + "fine_tunable", ], - "releaseDate": "2024-05-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:07:31.638Z", - "baseModelName": "gpt-4o" + releaseDate: "2024-05-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:07:31.638Z", + baseModelName: "gpt-4o", }, "gpt-4o-audio-preview": { - "provider": "openai", - "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:08:09.590Z", - "baseModelName": null + provider: "openai", + description: + "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:08:09.590Z", + baseModelName: null, }, "gpt-4o-audio-preview-2024-10-01": { - "provider": "openai", - "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:08:09.590Z", - "baseModelName": "gpt-4o-audio-preview" + provider: "openai", + description: + "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:08:09.590Z", + baseModelName: "gpt-4o-audio-preview", }, "gpt-4o-mini": { - "provider": "openai", - "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-07-18", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:50.130Z", - "baseModelName": null + provider: "openai", + description: + "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-07-18", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:50.130Z", + baseModelName: null, }, "gpt-4o-mini-2024-07-18": { - "provider": "openai", - "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "fine_tunable" - ], - "releaseDate": "2024-07-18", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:50.130Z", - "baseModelName": "gpt-4o-mini" + provider: "openai", + description: + "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], + releaseDate: "2024-07-18", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:50.130Z", + baseModelName: "gpt-4o-mini", }, "gpt-4o-realtime-preview": { - "provider": "openai", - "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:35.495Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:35.495Z", + baseModelName: null, }, "gpt-4o-realtime-preview-2024-10-01": { - "provider": "openai", - "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - "contextWindow": 128000, - "maxOutputTokens": 4096, - "capabilities": [ - "audio_input", - "audio_output", - "tool_use", - "streaming" - ], - "releaseDate": "2024-10-01", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": false, - "deprecationDate": "2026-05-07", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:09:35.495Z", - "baseModelName": "gpt-4o-realtime-preview" + provider: "openai", + description: + "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + contextWindow: 128000, + maxOutputTokens: 4096, + capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], + releaseDate: "2024-10-01", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: false, + deprecationDate: "2026-05-07", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:09:35.495Z", + baseModelName: "gpt-4o-realtime-preview", }, "gpt-5": { - "provider": "openai", - "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution" + "code_execution", ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:28.216Z", - "baseModelName": null + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:28.216Z", + baseModelName: null, }, "gpt-5-2025-08-07": { - "provider": "openai", - "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ + provider: "openai", + description: + "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution" + "code_execution", ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:28.216Z", - "baseModelName": "gpt-5" + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:28.216Z", + baseModelName: "gpt-5", }, "gpt-5-chat-latest": { - "provider": "openai", - "description": "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", - "contextWindow": 128000, - "maxOutputTokens": 16384, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:09:24.834Z", - "baseModelName": "gpt-5-chat" + provider: "openai", + description: + "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", + contextWindow: 128000, + maxOutputTokens: 16384, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:09:24.834Z", + baseModelName: "gpt-5-chat", }, "gpt-5-mini": { - "provider": "openai", - "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:09:42.822Z", - "baseModelName": null + provider: "openai", + description: + "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:09:42.822Z", + baseModelName: null, }, "gpt-5-mini-2025-08-07": { - "provider": "openai", - "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:09:42.822Z", - "baseModelName": "gpt-5-mini" + provider: "openai", + description: + "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:09:42.822Z", + baseModelName: "gpt-5-mini", }, "gpt-5-nano": { - "provider": "openai", - "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:11:24.884Z", - "baseModelName": null + provider: "openai", + description: + "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:11:24.884Z", + baseModelName: null, }, "gpt-5-nano-2025-08-07": { - "provider": "openai", - "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-08-07", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-05-31", - "resolvedAt": "2026-03-24T11:11:24.884Z", - "baseModelName": "gpt-5-nano" + provider: "openai", + description: + "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-08-07", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-05-31", + resolvedAt: "2026-03-24T11:11:24.884Z", + baseModelName: "gpt-5-nano", }, "gpt-5-pro": { - "provider": "openai", - "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-10-06", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-10-01", - "resolvedAt": "2026-03-24T11:11:37.048Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-10-06", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-10-01", + resolvedAt: "2026-03-24T11:11:37.048Z", + baseModelName: null, }, "gpt-5-pro-2025-10-06": { - "provider": "openai", - "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-10-06", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-10-01", - "resolvedAt": "2026-03-24T11:11:37.048Z", - "baseModelName": "gpt-5-pro" + provider: "openai", + description: + "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-10-06", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-10-01", + resolvedAt: "2026-03-24T11:11:37.048Z", + baseModelName: "gpt-5-pro", }, "gpt-5.1": { - "provider": "openai", - "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-11-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:11:47.327Z", - "baseModelName": null + provider: "openai", + description: + "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-11-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:11:47.327Z", + baseModelName: null, }, "gpt-5.1-2025-11-13": { - "provider": "openai", - "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2025-11-13", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-09-30", - "resolvedAt": "2026-03-24T11:11:47.327Z", - "baseModelName": "gpt-5.1" + provider: "openai", + description: + "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2025-11-13", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-09-30", + resolvedAt: "2026-03-24T11:11:47.327Z", + baseModelName: "gpt-5.1", }, "gpt-5.2": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:13.129Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:13.129Z", + baseModelName: null, }, "gpt-5.2-2025-12-11": { - "provider": "openai", - "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:13.129Z", - "baseModelName": "gpt-5.2" + provider: "openai", + description: + "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:13.129Z", + baseModelName: "gpt-5.2", }, "gpt-5.2-pro": { - "provider": "openai", - "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:12.711Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:12.711Z", + baseModelName: null, }, "gpt-5.2-pro-2025-12-11": { - "provider": "openai", - "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2025-12-11", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:11:12.711Z", - "baseModelName": "gpt-5.2-pro" + provider: "openai", + description: + "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2025-12-11", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:11:12.711Z", + baseModelName: "gpt-5.2-pro", }, "gpt-5.4": { - "provider": "openai", - "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:09.220Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:09.220Z", + baseModelName: null, }, "gpt-5.4-2026-03-05": { - "provider": "openai", - "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "code_execution" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:09.220Z", - "baseModelName": "gpt-5.4" + provider: "openai", + description: + "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:09.220Z", + baseModelName: "gpt-5.4", }, "gpt-5.4-mini": { - "provider": "openai", - "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:35.473Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:35.473Z", + baseModelName: null, }, "gpt-5.4-mini-2026-03-17": { - "provider": "openai", - "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:35.473Z", - "baseModelName": "gpt-5.4-mini" + provider: "openai", + description: + "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:35.473Z", + baseModelName: "gpt-5.4-mini", }, "gpt-5.4-nano": { - "provider": "openai", - "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:52.285Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:52.285Z", + baseModelName: null, }, "gpt-5.4-nano-2026-03-17": { - "provider": "openai", - "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - "contextWindow": 400000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2026-03-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:52.285Z", - "baseModelName": "gpt-5.4-nano" + provider: "openai", + description: + "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + contextWindow: 400000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2026-03-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:52.285Z", + baseModelName: "gpt-5.4-nano", }, "gpt-5.4-pro": { - "provider": "openai", - "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:56.903Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:56.903Z", + baseModelName: null, }, "gpt-5.4-pro-2026-03-05": { - "provider": "openai", - "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - "contextWindow": 1050000, - "maxOutputTokens": 128000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "extended_thinking" - ], - "releaseDate": "2026-03-05", - "isHidden": false, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2025-08-31", - "resolvedAt": "2026-03-24T11:12:56.903Z", - "baseModelName": "gpt-5.4-pro" - }, - "o1": { - "provider": "openai", - "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:23.948Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + contextWindow: 1050000, + maxOutputTokens: 128000, + capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], + releaseDate: "2026-03-05", + isHidden: false, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2025-08-31", + resolvedAt: "2026-03-24T11:12:56.903Z", + baseModelName: "gpt-5.4-pro", + }, + o1: { + provider: "openai", + description: + "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:23.948Z", + baseModelName: null, }, "o1-2024-12-17": { - "provider": "openai", - "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode" - ], - "releaseDate": "2024-12-17", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:23.948Z", - "baseModelName": "o1" + provider: "openai", + description: + "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode"], + releaseDate: "2024-12-17", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:23.948Z", + baseModelName: "o1", }, "o1-mini": { - "provider": "openai", - "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - "contextWindow": 128000, - "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-30", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:37.030Z", - "baseModelName": null + provider: "openai", + description: + "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + contextWindow: 128000, + maxOutputTokens: 65536, + capabilities: ["streaming", "json_mode"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-30", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:37.030Z", + baseModelName: null, }, "o1-mini-2024-09-12": { - "provider": "openai", - "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - "contextWindow": 128000, - "maxOutputTokens": 65536, - "capabilities": [ - "streaming", - "json_mode" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-06-30", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:37.030Z", - "baseModelName": "o1-mini" + provider: "openai", + description: + "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + contextWindow: 128000, + maxOutputTokens: 65536, + capabilities: ["streaming", "json_mode"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-06-30", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:37.030Z", + baseModelName: "o1-mini", }, "o1-preview": { - "provider": "openai", - "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - "contextWindow": 128000, - "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-10-31", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:59.198Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + contextWindow: 128000, + maxOutputTokens: 32768, + capabilities: ["streaming"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-10-31", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:59.198Z", + baseModelName: null, }, "o1-preview-2024-09-12": { - "provider": "openai", - "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - "contextWindow": 128000, - "maxOutputTokens": 32768, - "capabilities": [ - "streaming" - ], - "releaseDate": "2024-09-12", - "isHidden": true, - "supportsStructuredOutput": false, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": "2025-10-31", - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:12:59.198Z", - "baseModelName": "o1-preview" + provider: "openai", + description: + "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + contextWindow: 128000, + maxOutputTokens: 32768, + capabilities: ["streaming"], + releaseDate: "2024-09-12", + isHidden: true, + supportsStructuredOutput: false, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: "2025-10-31", + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:12:59.198Z", + baseModelName: "o1-preview", }, "o1-pro": { - "provider": "openai", - "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-03-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:13:57.532Z", - "baseModelName": null + provider: "openai", + description: + "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], + releaseDate: "2025-03-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:13:57.532Z", + baseModelName: null, }, "o1-pro-2025-03-19": { - "provider": "openai", - "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-03-19", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2023-10-01", - "resolvedAt": "2026-03-24T11:13:57.532Z", - "baseModelName": "o1-pro" - }, - "o3": { - "provider": "openai", - "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:04.906Z", - "baseModelName": null + provider: "openai", + description: + "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], + releaseDate: "2025-03-19", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2023-10-01", + resolvedAt: "2026-03-24T11:13:57.532Z", + baseModelName: "o1-pro", + }, + o3: { + provider: "openai", + description: + "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:04.906Z", + baseModelName: null, }, "o3-2025-04-16": { - "provider": "openai", - "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:04.906Z", - "baseModelName": "o3" + provider: "openai", + description: + "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:04.906Z", + baseModelName: "o3", }, "o3-mini": { - "provider": "openai", - "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-01-31", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:13:33.788Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-01-31", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:13:33.788Z", + baseModelName: null, }, "o3-mini-2025-01-31": { - "provider": "openai", - "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-01-31", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2025-01-01", - "resolvedAt": "2026-03-24T11:13:33.788Z", - "baseModelName": "o3-mini" + provider: "openai", + description: + "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-01-31", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2025-01-01", + resolvedAt: "2026-03-24T11:13:33.788Z", + baseModelName: "o3-mini", }, "o3-pro": { - "provider": "openai", - "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-06-10", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:10.900Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-06-10", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:10.900Z", + baseModelName: null, }, "o3-pro-2025-06-10": { - "provider": "openai", - "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-06-10", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": false, - "supportsStreamingToolCalls": false, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:10.900Z", - "baseModelName": "o3-pro" + provider: "openai", + description: + "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-06-10", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: false, + supportsStreamingToolCalls: false, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:10.900Z", + baseModelName: "o3-pro", }, "o4-mini": { - "provider": "openai", - "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:16.050Z", - "baseModelName": null + provider: "openai", + description: + "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:16.050Z", + baseModelName: null, }, "o4-mini-2025-04-16": { - "provider": "openai", - "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - "contextWindow": 200000, - "maxOutputTokens": 100000, - "capabilities": [ - "vision", - "tool_use", - "streaming", - "json_mode", - "extended_thinking" - ], - "releaseDate": "2025-04-16", - "isHidden": false, - "supportsStructuredOutput": true, - "supportsParallelToolCalls": true, - "supportsStreamingToolCalls": true, - "deprecationDate": null, - "knowledgeCutoff": "2024-06-01", - "resolvedAt": "2026-03-24T11:14:16.050Z", - "baseModelName": "o4-mini" - } + provider: "openai", + description: + "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + contextWindow: 200000, + maxOutputTokens: 100000, + capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], + releaseDate: "2025-04-16", + isHidden: false, + supportsStructuredOutput: true, + supportsParallelToolCalls: true, + supportsStreamingToolCalls: true, + deprecationDate: null, + knowledgeCutoff: "2024-06-01", + resolvedAt: "2026-03-24T11:14:16.050Z", + baseModelName: "o4-mini", + }, }; diff --git a/internal-packages/llm-model-catalog/src/registry.test.ts b/internal-packages/llm-model-catalog/src/registry.test.ts index 349ba2622e6..cfda7177273 100644 --- a/internal-packages/llm-model-catalog/src/registry.test.ts +++ b/internal-packages/llm-model-catalog/src/registry.test.ts @@ -303,9 +303,7 @@ describe("ModelPricingRegistry", () => { name: "Large Context", isDefault: false, priority: 0, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 200000 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 200000 }], prices: [ { usageType: "input", price: 0.0000025 }, { usageType: "output", price: 0.00001 }, @@ -371,9 +369,7 @@ describe("ModelPricingRegistry", () => { name: "Conditional", isDefault: false, priority: 1, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 100 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 100 }], prices: [{ usageType: "input", price: 0.0001 }], }, { @@ -405,9 +401,7 @@ describe("ModelPricingRegistry", () => { name: "Conditional", isDefault: false, priority: 0, - conditions: [ - { usageDetailPattern: "input", operator: "gt" as const, value: 999999 }, - ], + conditions: [{ usageDetailPattern: "input", operator: "gt" as const, value: 999999 }], prices: [{ usageType: "input", price: 0.001 }], }, { diff --git a/internal-packages/llm-model-catalog/src/registry.ts b/internal-packages/llm-model-catalog/src/registry.ts index 6a3d52814cb..880bed39511 100644 --- a/internal-packages/llm-model-catalog/src/registry.ts +++ b/internal-packages/llm-model-catalog/src/registry.ts @@ -134,10 +134,7 @@ export class ModelPricingRegistry { return null; } - calculateCost( - responseModel: string, - usageDetails: Record - ): LlmCostResult | null { + calculateCost(responseModel: string, usageDetails: Record): LlmCostResult | null { const model = this.match(responseModel); if (!model) return null; diff --git a/internal-packages/llm-model-catalog/src/sync.test.ts b/internal-packages/llm-model-catalog/src/sync.test.ts index d2138564ef6..a5c6ed4841c 100644 --- a/internal-packages/llm-model-catalog/src/sync.test.ts +++ b/internal-packages/llm-model-catalog/src/sync.test.ts @@ -32,10 +32,7 @@ const STALE_BASE_MODEL_NAME = "wrong-base-model-sentinel"; const STALE_INPUT_PRICE = 0.099; const STALE_OUTPUT_PRICE = 0.088; -async function createGpt4oWithStalePricing( - prisma: PrismaClient, - source: "default" | "admin" -) { +async function createGpt4oWithStalePricing(prisma: PrismaClient, source: "default" | "admin") { const model = await prisma.llmModel.create({ data: { friendlyId: generateFriendlyId("llm_model"), @@ -129,7 +126,9 @@ async function loadGpt4oWithTiers(prisma: PrismaClient) { }); } -function expectBundledGpt4oPricing(model: NonNullable>>) { +function expectBundledGpt4oPricing( + model: NonNullable>> +) { expect(model.matchPattern).toBe(gpt4oDef.matchPattern); expect(model.pricingTiers).toHaveLength(gpt4oDef.pricingTiers.length); diff --git a/internal-packages/llm-model-catalog/src/sync.ts b/internal-packages/llm-model-catalog/src/sync.ts index 8761df0fe4a..378a9bf9f26 100644 --- a/internal-packages/llm-model-catalog/src/sync.ts +++ b/internal-packages/llm-model-catalog/src/sync.ts @@ -71,9 +71,7 @@ export async function syncLlmCatalog(prisma: PrismaClient): Promise<{ capabilities: catalog?.capabilities ?? existing.capabilities, isHidden: catalog?.isHidden ?? existing.isHidden, baseModelName: - catalog?.baseModelName === undefined - ? existing.baseModelName - : catalog.baseModelName, + catalog?.baseModelName === undefined ? existing.baseModelName : catalog.baseModelName, pricingUnit: existing.pricingUnit ?? "tokens", }, }); diff --git a/internal-packages/otlp-importer/package.json b/internal-packages/otlp-importer/package.json index 6f5cd39665f..5c488ce1aaa 100644 --- a/internal-packages/otlp-importer/package.json +++ b/internal-packages/otlp-importer/package.json @@ -38,4 +38,4 @@ "long": "^5.2.3", "protobufjs": "^7.2.6" } -} \ No newline at end of file +} diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts index fa4a1e3666f..50d82933e00 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/logs/v1/logs_service.ts @@ -108,10 +108,14 @@ export const ExportLogsServiceRequest = { return obj; }, - create, I>>(base?: I): ExportLogsServiceRequest { + create, I>>( + base?: I + ): ExportLogsServiceRequest { return ExportLogsServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsServiceRequest { + fromPartial, I>>( + object: I + ): ExportLogsServiceRequest { const message = createBaseExportLogsServiceRequest(); message.resourceLogs = object.resourceLogs?.map((e) => ResourceLogs.fromPartial(e)) || []; return message; @@ -169,14 +173,19 @@ export const ExportLogsServiceResponse = { return obj; }, - create, I>>(base?: I): ExportLogsServiceResponse { + create, I>>( + base?: I + ): ExportLogsServiceResponse { return ExportLogsServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsServiceResponse { + fromPartial, I>>( + object: I + ): ExportLogsServiceResponse { const message = createBaseExportLogsServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportLogsPartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportLogsPartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -189,7 +198,9 @@ export const ExportLogsPartialSuccess = { encode(message: ExportLogsPartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.rejectedLogRecords !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedLogRecords) !== message.rejectedLogRecords) { - throw new globalThis.Error("value provided for field message.rejectedLogRecords of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedLogRecords of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedLogRecords.toString()); } @@ -231,7 +242,9 @@ export const ExportLogsPartialSuccess = { fromJSON(object: any): ExportLogsPartialSuccess { return { - rejectedLogRecords: isSet(object.rejectedLogRecords) ? BigInt(object.rejectedLogRecords) : BigInt("0"), + rejectedLogRecords: isSet(object.rejectedLogRecords) + ? BigInt(object.rejectedLogRecords) + : BigInt("0"), errorMessage: isSet(object.errorMessage) ? globalThis.String(object.errorMessage) : "", }; }, @@ -247,10 +260,14 @@ export const ExportLogsPartialSuccess = { return obj; }, - create, I>>(base?: I): ExportLogsPartialSuccess { + create, I>>( + base?: I + ): ExportLogsPartialSuccess { return ExportLogsPartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportLogsPartialSuccess { + fromPartial, I>>( + object: I + ): ExportLogsPartialSuccess { const message = createBaseExportLogsPartialSuccess(); message.rejectedLogRecords = object.rejectedLogRecords ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +310,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts index 53bda7b01d8..9f7c913c34b 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/metrics/v1/metrics_service.ts @@ -62,7 +62,10 @@ function createBaseExportMetricsServiceRequest(): ExportMetricsServiceRequest { } export const ExportMetricsServiceRequest = { - encode(message: ExportMetricsServiceRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsServiceRequest, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { for (const v of message.resourceMetrics) { ResourceMetrics.encode(v!, writer.uint32(10).fork()).ldelim(); } @@ -108,12 +111,17 @@ export const ExportMetricsServiceRequest = { return obj; }, - create, I>>(base?: I): ExportMetricsServiceRequest { + create, I>>( + base?: I + ): ExportMetricsServiceRequest { return ExportMetricsServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsServiceRequest { + fromPartial, I>>( + object: I + ): ExportMetricsServiceRequest { const message = createBaseExportMetricsServiceRequest(); - message.resourceMetrics = object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; + message.resourceMetrics = + object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; return message; }, }; @@ -123,7 +131,10 @@ function createBaseExportMetricsServiceResponse(): ExportMetricsServiceResponse } export const ExportMetricsServiceResponse = { - encode(message: ExportMetricsServiceResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsServiceResponse, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.partialSuccess !== undefined) { ExportMetricsPartialSuccess.encode(message.partialSuccess, writer.uint32(10).fork()).ldelim(); } @@ -169,14 +180,19 @@ export const ExportMetricsServiceResponse = { return obj; }, - create, I>>(base?: I): ExportMetricsServiceResponse { + create, I>>( + base?: I + ): ExportMetricsServiceResponse { return ExportMetricsServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsServiceResponse { + fromPartial, I>>( + object: I + ): ExportMetricsServiceResponse { const message = createBaseExportMetricsServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportMetricsPartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportMetricsPartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -186,10 +202,15 @@ function createBaseExportMetricsPartialSuccess(): ExportMetricsPartialSuccess { } export const ExportMetricsPartialSuccess = { - encode(message: ExportMetricsPartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportMetricsPartialSuccess, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.rejectedDataPoints !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedDataPoints) !== message.rejectedDataPoints) { - throw new globalThis.Error("value provided for field message.rejectedDataPoints of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedDataPoints of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedDataPoints.toString()); } @@ -231,7 +252,9 @@ export const ExportMetricsPartialSuccess = { fromJSON(object: any): ExportMetricsPartialSuccess { return { - rejectedDataPoints: isSet(object.rejectedDataPoints) ? BigInt(object.rejectedDataPoints) : BigInt("0"), + rejectedDataPoints: isSet(object.rejectedDataPoints) + ? BigInt(object.rejectedDataPoints) + : BigInt("0"), errorMessage: isSet(object.errorMessage) ? globalThis.String(object.errorMessage) : "", }; }, @@ -247,10 +270,14 @@ export const ExportMetricsPartialSuccess = { return obj; }, - create, I>>(base?: I): ExportMetricsPartialSuccess { + create, I>>( + base?: I + ): ExportMetricsPartialSuccess { return ExportMetricsPartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportMetricsPartialSuccess { + fromPartial, I>>( + object: I + ): ExportMetricsPartialSuccess { const message = createBaseExportMetricsPartialSuccess(); message.rejectedDataPoints = object.rejectedDataPoints ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +320,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts index 608e6c91279..ee38c623fab 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/collector/trace/v1/trace_service.ts @@ -108,10 +108,14 @@ export const ExportTraceServiceRequest = { return obj; }, - create, I>>(base?: I): ExportTraceServiceRequest { + create, I>>( + base?: I + ): ExportTraceServiceRequest { return ExportTraceServiceRequest.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTraceServiceRequest { + fromPartial, I>>( + object: I + ): ExportTraceServiceRequest { const message = createBaseExportTraceServiceRequest(); message.resourceSpans = object.resourceSpans?.map((e) => ResourceSpans.fromPartial(e)) || []; return message; @@ -123,7 +127,10 @@ function createBaseExportTraceServiceResponse(): ExportTraceServiceResponse { } export const ExportTraceServiceResponse = { - encode(message: ExportTraceServiceResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExportTraceServiceResponse, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.partialSuccess !== undefined) { ExportTracePartialSuccess.encode(message.partialSuccess, writer.uint32(10).fork()).ldelim(); } @@ -169,14 +176,19 @@ export const ExportTraceServiceResponse = { return obj; }, - create, I>>(base?: I): ExportTraceServiceResponse { + create, I>>( + base?: I + ): ExportTraceServiceResponse { return ExportTraceServiceResponse.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTraceServiceResponse { + fromPartial, I>>( + object: I + ): ExportTraceServiceResponse { const message = createBaseExportTraceServiceResponse(); - message.partialSuccess = (object.partialSuccess !== undefined && object.partialSuccess !== null) - ? ExportTracePartialSuccess.fromPartial(object.partialSuccess) - : undefined; + message.partialSuccess = + object.partialSuccess !== undefined && object.partialSuccess !== null + ? ExportTracePartialSuccess.fromPartial(object.partialSuccess) + : undefined; return message; }, }; @@ -189,7 +201,9 @@ export const ExportTracePartialSuccess = { encode(message: ExportTracePartialSuccess, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.rejectedSpans !== BigInt("0")) { if (BigInt.asIntN(64, message.rejectedSpans) !== message.rejectedSpans) { - throw new globalThis.Error("value provided for field message.rejectedSpans of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.rejectedSpans of type int64 too large" + ); } writer.uint32(8).int64(message.rejectedSpans.toString()); } @@ -247,10 +261,14 @@ export const ExportTracePartialSuccess = { return obj; }, - create, I>>(base?: I): ExportTracePartialSuccess { + create, I>>( + base?: I + ): ExportTracePartialSuccess { return ExportTracePartialSuccess.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExportTracePartialSuccess { + fromPartial, I>>( + object: I + ): ExportTracePartialSuccess { const message = createBaseExportTracePartialSuccess(); message.rejectedSpans = object.rejectedSpans ?? BigInt("0"); message.errorMessage = object.errorMessage ?? ""; @@ -293,14 +311,19 @@ interface Rpc { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts index 7db2022e71f..2a307a667c6 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/common/v1/common.ts @@ -93,7 +93,9 @@ export const AnyValue = { } if (message.intValue !== undefined) { if (BigInt.asIntN(64, message.intValue) !== message.intValue) { - throw new globalThis.Error("value provided for field message.intValue of type int64 too large"); + throw new globalThis.Error( + "value provided for field message.intValue of type int64 too large" + ); } writer.uint32(24).int64(message.intValue.toString()); } @@ -184,8 +186,12 @@ export const AnyValue = { intValue: isSet(object.intValue) ? BigInt(object.intValue) : undefined, doubleValue: isSet(object.doubleValue) ? globalThis.Number(object.doubleValue) : undefined, arrayValue: isSet(object.arrayValue) ? ArrayValue.fromJSON(object.arrayValue) : undefined, - kvlistValue: isSet(object.kvlistValue) ? KeyValueList.fromJSON(object.kvlistValue) : undefined, - bytesValue: isSet(object.bytesValue) ? Buffer.from(bytesFromBase64(object.bytesValue)) : undefined, + kvlistValue: isSet(object.kvlistValue) + ? KeyValueList.fromJSON(object.kvlistValue) + : undefined, + bytesValue: isSet(object.bytesValue) + ? Buffer.from(bytesFromBase64(object.bytesValue)) + : undefined, }; }, @@ -224,12 +230,14 @@ export const AnyValue = { message.boolValue = object.boolValue ?? undefined; message.intValue = object.intValue ?? undefined; message.doubleValue = object.doubleValue ?? undefined; - message.arrayValue = (object.arrayValue !== undefined && object.arrayValue !== null) - ? ArrayValue.fromPartial(object.arrayValue) - : undefined; - message.kvlistValue = (object.kvlistValue !== undefined && object.kvlistValue !== null) - ? KeyValueList.fromPartial(object.kvlistValue) - : undefined; + message.arrayValue = + object.arrayValue !== undefined && object.arrayValue !== null + ? ArrayValue.fromPartial(object.arrayValue) + : undefined; + message.kvlistValue = + object.kvlistValue !== undefined && object.kvlistValue !== null + ? KeyValueList.fromPartial(object.kvlistValue) + : undefined; message.bytesValue = object.bytesValue ?? undefined; return message; }, @@ -272,7 +280,9 @@ export const ArrayValue = { fromJSON(object: any): ArrayValue { return { - values: globalThis.Array.isArray(object?.values) ? object.values.map((e: any) => AnyValue.fromJSON(e)) : [], + values: globalThis.Array.isArray(object?.values) + ? object.values.map((e: any) => AnyValue.fromJSON(e)) + : [], }; }, @@ -331,7 +341,9 @@ export const KeyValueList = { fromJSON(object: any): KeyValueList { return { - values: globalThis.Array.isArray(object?.values) ? object.values.map((e: any) => KeyValue.fromJSON(e)) : [], + values: globalThis.Array.isArray(object?.values) + ? object.values.map((e: any) => KeyValue.fromJSON(e)) + : [], }; }, @@ -422,9 +434,10 @@ export const KeyValue = { fromPartial, I>>(object: I): KeyValue { const message = createBaseKeyValue(); message.key = object.key ?? ""; - message.value = (object.value !== undefined && object.value !== null) - ? AnyValue.fromPartial(object.value) - : undefined; + message.value = + object.value !== undefined && object.value !== null + ? AnyValue.fromPartial(object.value) + : undefined; return message; }, }; @@ -527,7 +540,9 @@ export const InstrumentationScope = { create, I>>(base?: I): InstrumentationScope { return InstrumentationScope.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): InstrumentationScope { + fromPartial, I>>( + object: I + ): InstrumentationScope { const message = createBaseInstrumentationScope(); message.name = object.name ?? ""; message.version = object.version ?? ""; @@ -564,14 +579,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts index 503abbb7e05..2d0b3ebf56a 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/logs/v1/logs.ts @@ -255,9 +255,7 @@ export interface ResourceLogs { * The resource for the logs in this message. * If this field is not set then resource info is unknown. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of ScopeLogs that originate from a resource. */ scopeLogs: ScopeLogs[]; /** @@ -277,9 +275,7 @@ export interface ScopeLogs { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of log records. */ logRecords: LogRecord[]; /** @@ -335,9 +331,7 @@ export interface LogRecord { * string message (including multi-line) describing the event in a free form or it can * be a structured data composed of arrays and maps of other values. [Optional]. */ - body: - | AnyValue - | undefined; + body: AnyValue | undefined; /** * Additional attributes that describe the specific event occurrence. [Optional]. * Attribute keys MUST be unique (it is not allowed to have more than one @@ -529,9 +523,10 @@ export const ResourceLogs = { }, fromPartial, I>>(object: I): ResourceLogs { const message = createBaseResourceLogs(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeLogs = object.scopeLogs?.map((e) => ScopeLogs.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -622,9 +617,10 @@ export const ScopeLogs = { }, fromPartial, I>>(object: I): ScopeLogs { const message = createBaseScopeLogs(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.logRecords = object.logRecords?.map((e) => LogRecord.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -650,13 +646,17 @@ export const LogRecord = { encode(message: LogRecord, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(9).fixed64(message.timeUnixNano.toString()); } if (message.observedTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.observedTimeUnixNano) !== message.observedTimeUnixNano) { - throw new globalThis.Error("value provided for field message.observedTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.observedTimeUnixNano of type fixed64 too large" + ); } writer.uint32(89).fixed64(message.observedTimeUnixNano.toString()); } @@ -776,8 +776,12 @@ export const LogRecord = { fromJSON(object: any): LogRecord { return { timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), - observedTimeUnixNano: isSet(object.observedTimeUnixNano) ? BigInt(object.observedTimeUnixNano) : BigInt("0"), - severityNumber: isSet(object.severityNumber) ? severityNumberFromJSON(object.severityNumber) : 0, + observedTimeUnixNano: isSet(object.observedTimeUnixNano) + ? BigInt(object.observedTimeUnixNano) + : BigInt("0"), + severityNumber: isSet(object.severityNumber) + ? severityNumberFromJSON(object.severityNumber) + : 0, severityText: isSet(object.severityText) ? globalThis.String(object.severityText) : "", body: isSet(object.body) ? AnyValue.fromJSON(object.body) : undefined, attributes: globalThis.Array.isArray(object?.attributes) @@ -787,7 +791,9 @@ export const LogRecord = { ? globalThis.Number(object.droppedAttributesCount) : 0, flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), }; }, @@ -836,7 +842,10 @@ export const LogRecord = { message.observedTimeUnixNano = object.observedTimeUnixNano ?? BigInt("0"); message.severityNumber = object.severityNumber ?? 0; message.severityText = object.severityText ?? ""; - message.body = (object.body !== undefined && object.body !== null) ? AnyValue.fromPartial(object.body) : undefined; + message.body = + object.body !== undefined && object.body !== null + ? AnyValue.fromPartial(object.body) + : undefined; message.attributes = object.attributes?.map((e) => KeyValue.fromPartial(e)) || []; message.droppedAttributesCount = object.droppedAttributesCount ?? 0; message.flags = object.flags ?? 0; @@ -873,14 +882,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts index a09922a4ec7..0f368d54871 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/metrics/v1/metrics.ts @@ -193,9 +193,7 @@ export interface ResourceMetrics { * The resource for the metrics in this message. * If this field is not set then no resource info is known. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of metrics that originate from a resource. */ scopeMetrics: ScopeMetrics[]; /** @@ -215,9 +213,7 @@ export interface ScopeMetrics { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of metrics that originate from an instrumentation library. */ metrics: Metric[]; /** @@ -329,9 +325,7 @@ export interface Metric { sum?: Sum | undefined; histogram?: Histogram | undefined; exponentialHistogram?: ExponentialHistogram | undefined; - summary?: - | Summary - | undefined; + summary?: Summary | undefined; /** * Additional metadata attributes that describe the metric. [Optional]. * Attributes are non-identifying. @@ -440,9 +434,7 @@ export interface NumberDataPoint { */ timeUnixNano: bigint; asDouble?: number | undefined; - asInt?: - | bigint - | undefined; + asInt?: bigint | undefined; /** * (Optional) List of exemplars collected from * measurements that were used to form the data point @@ -506,9 +498,7 @@ export interface HistogramDataPoint { * doing so. This is specifically to enforce compatibility w/ OpenMetrics, * see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram */ - sum?: - | number - | undefined; + sum?: number | undefined; /** * bucket_counts is an optional field contains the count values of histogram * for each bucket. @@ -546,9 +536,7 @@ export interface HistogramDataPoint { */ flags: number; /** min is the minimum value over (start_time, end_time]. */ - min?: - | number - | undefined; + min?: number | undefined; /** max is the maximum value over (start_time, end_time]. */ max?: number | undefined; } @@ -598,9 +586,7 @@ export interface ExponentialHistogramDataPoint { * doing so. This is specifically to enforce compatibility w/ OpenMetrics, * see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram */ - sum?: - | number - | undefined; + sum?: number | undefined; /** * scale describes the resolution of the histogram. Boundaries are * located at powers of the base, where: @@ -631,13 +617,9 @@ export interface ExponentialHistogramDataPoint { */ zeroCount: bigint; /** positive carries the positive range of exponential bucket counts. */ - positive: - | ExponentialHistogramDataPoint_Buckets - | undefined; + positive: ExponentialHistogramDataPoint_Buckets | undefined; /** negative carries the negative range of exponential bucket counts. */ - negative: - | ExponentialHistogramDataPoint_Buckets - | undefined; + negative: ExponentialHistogramDataPoint_Buckets | undefined; /** * Flags that apply to this specific data point. See DataPointFlags * for the available flags and their meaning. @@ -649,13 +631,9 @@ export interface ExponentialHistogramDataPoint { */ exemplars: Exemplar[]; /** min is the minimum value over (start_time, end_time]. */ - min?: - | number - | undefined; + min?: number | undefined; /** max is the maximum value over (start_time, end_time]. */ - max?: - | number - | undefined; + max?: number | undefined; /** * ZeroThreshold may be optionally set to convey the width of the zero * region. Where the zero region is defined as the closed interval @@ -789,9 +767,7 @@ export interface Exemplar { */ timeUnixNano: bigint; asDouble?: number | undefined; - asInt?: - | bigint - | undefined; + asInt?: bigint | undefined; /** * (Optional) Span ID of the exemplar trace. * span_id may be missing if the measurement is not recorded inside a trace @@ -862,7 +838,8 @@ export const MetricsData = { }, fromPartial, I>>(object: I): MetricsData { const message = createBaseMetricsData(); - message.resourceMetrics = object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; + message.resourceMetrics = + object.resourceMetrics?.map((e) => ResourceMetrics.fromPartial(e)) || []; return message; }, }; @@ -951,9 +928,10 @@ export const ResourceMetrics = { }, fromPartial, I>>(object: I): ResourceMetrics { const message = createBaseResourceMetrics(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeMetrics = object.scopeMetrics?.map((e) => ScopeMetrics.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -1018,7 +996,9 @@ export const ScopeMetrics = { fromJSON(object: any): ScopeMetrics { return { scope: isSet(object.scope) ? InstrumentationScope.fromJSON(object.scope) : undefined, - metrics: globalThis.Array.isArray(object?.metrics) ? object.metrics.map((e: any) => Metric.fromJSON(e)) : [], + metrics: globalThis.Array.isArray(object?.metrics) + ? object.metrics.map((e: any) => Metric.fromJSON(e)) + : [], schemaUrl: isSet(object.schemaUrl) ? globalThis.String(object.schemaUrl) : "", }; }, @@ -1042,9 +1022,10 @@ export const ScopeMetrics = { }, fromPartial, I>>(object: I): ScopeMetrics { const message = createBaseScopeMetrics(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.metrics = object.metrics?.map((e) => Metric.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -1188,7 +1169,9 @@ export const Metric = { ? ExponentialHistogram.fromJSON(object.exponentialHistogram) : undefined, summary: isSet(object.summary) ? Summary.fromJSON(object.summary) : undefined, - metadata: globalThis.Array.isArray(object?.metadata) ? object.metadata.map((e: any) => KeyValue.fromJSON(e)) : [], + metadata: globalThis.Array.isArray(object?.metadata) + ? object.metadata.map((e: any) => KeyValue.fromJSON(e)) + : [], }; }, @@ -1232,17 +1215,24 @@ export const Metric = { message.name = object.name ?? ""; message.description = object.description ?? ""; message.unit = object.unit ?? ""; - message.gauge = (object.gauge !== undefined && object.gauge !== null) ? Gauge.fromPartial(object.gauge) : undefined; - message.sum = (object.sum !== undefined && object.sum !== null) ? Sum.fromPartial(object.sum) : undefined; - message.histogram = (object.histogram !== undefined && object.histogram !== null) - ? Histogram.fromPartial(object.histogram) - : undefined; - message.exponentialHistogram = (object.exponentialHistogram !== undefined && object.exponentialHistogram !== null) - ? ExponentialHistogram.fromPartial(object.exponentialHistogram) - : undefined; - message.summary = (object.summary !== undefined && object.summary !== null) - ? Summary.fromPartial(object.summary) - : undefined; + message.gauge = + object.gauge !== undefined && object.gauge !== null + ? Gauge.fromPartial(object.gauge) + : undefined; + message.sum = + object.sum !== undefined && object.sum !== null ? Sum.fromPartial(object.sum) : undefined; + message.histogram = + object.histogram !== undefined && object.histogram !== null + ? Histogram.fromPartial(object.histogram) + : undefined; + message.exponentialHistogram = + object.exponentialHistogram !== undefined && object.exponentialHistogram !== null + ? ExponentialHistogram.fromPartial(object.exponentialHistogram) + : undefined; + message.summary = + object.summary !== undefined && object.summary !== null + ? Summary.fromPartial(object.summary) + : undefined; message.metadata = object.metadata?.map((e) => KeyValue.fromPartial(e)) || []; return message; }, @@ -1550,9 +1540,12 @@ export const ExponentialHistogram = { create, I>>(base?: I): ExponentialHistogram { return ExponentialHistogram.fromPartial(base ?? ({} as any)); }, - fromPartial, I>>(object: I): ExponentialHistogram { + fromPartial, I>>( + object: I + ): ExponentialHistogram { const message = createBaseExponentialHistogram(); - message.dataPoints = object.dataPoints?.map((e) => ExponentialHistogramDataPoint.fromPartial(e)) || []; + message.dataPoints = + object.dataPoints?.map((e) => ExponentialHistogramDataPoint.fromPartial(e)) || []; message.aggregationTemporality = object.aggregationTemporality ?? 0; return message; }, @@ -1638,13 +1631,17 @@ export const NumberDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } @@ -1653,7 +1650,9 @@ export const NumberDataPoint = { } if (message.asInt !== undefined) { if (BigInt.asIntN(64, message.asInt) !== message.asInt) { - throw new globalThis.Error("value provided for field message.asInt of type sfixed64 too large"); + throw new globalThis.Error( + "value provided for field message.asInt of type sfixed64 too large" + ); } writer.uint32(49).sfixed64(message.asInt.toString()); } @@ -1736,7 +1735,9 @@ export const NumberDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), asDouble: isSet(object.asDouble) ? globalThis.Number(object.asDouble) : undefined, asInt: isSet(object.asInt) ? BigInt(object.asInt) : undefined, @@ -1812,19 +1813,25 @@ export const HistogramDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -1834,7 +1841,9 @@ export const HistogramDataPoint = { writer.uint32(50).fork(); for (const v of message.bucketCounts) { if (BigInt.asUintN(64, v) !== v) { - throw new globalThis.Error("a value provided in array field bucketCounts of type fixed64 is too large"); + throw new globalThis.Error( + "a value provided in array field bucketCounts of type fixed64 is too large" + ); } writer.fixed64(v.toString()); } @@ -1977,7 +1986,9 @@ export const HistogramDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : undefined, @@ -2074,25 +2085,34 @@ function createBaseExponentialHistogramDataPoint(): ExponentialHistogramDataPoin } export const ExponentialHistogramDataPoint = { - encode(message: ExponentialHistogramDataPoint, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExponentialHistogramDataPoint, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { for (const v of message.attributes) { KeyValue.encode(v!, writer.uint32(10).fork()).ldelim(); } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -2104,15 +2124,23 @@ export const ExponentialHistogramDataPoint = { } if (message.zeroCount !== BigInt("0")) { if (BigInt.asUintN(64, message.zeroCount) !== message.zeroCount) { - throw new globalThis.Error("value provided for field message.zeroCount of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.zeroCount of type fixed64 too large" + ); } writer.uint32(57).fixed64(message.zeroCount.toString()); } if (message.positive !== undefined) { - ExponentialHistogramDataPoint_Buckets.encode(message.positive, writer.uint32(66).fork()).ldelim(); + ExponentialHistogramDataPoint_Buckets.encode( + message.positive, + writer.uint32(66).fork() + ).ldelim(); } if (message.negative !== undefined) { - ExponentialHistogramDataPoint_Buckets.encode(message.negative, writer.uint32(74).fork()).ldelim(); + ExponentialHistogramDataPoint_Buckets.encode( + message.negative, + writer.uint32(74).fork() + ).ldelim(); } if (message.flags !== 0) { writer.uint32(80).uint32(message.flags); @@ -2251,14 +2279,20 @@ export const ExponentialHistogramDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : undefined, scale: isSet(object.scale) ? globalThis.Number(object.scale) : 0, zeroCount: isSet(object.zeroCount) ? BigInt(object.zeroCount) : BigInt("0"), - positive: isSet(object.positive) ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.positive) : undefined, - negative: isSet(object.negative) ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.negative) : undefined, + positive: isSet(object.positive) + ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.positive) + : undefined, + negative: isSet(object.negative) + ? ExponentialHistogramDataPoint_Buckets.fromJSON(object.negative) + : undefined, flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, exemplars: globalThis.Array.isArray(object?.exemplars) ? object.exemplars.map((e: any) => Exemplar.fromJSON(e)) @@ -2316,11 +2350,13 @@ export const ExponentialHistogramDataPoint = { return obj; }, - create, I>>(base?: I): ExponentialHistogramDataPoint { + create, I>>( + base?: I + ): ExponentialHistogramDataPoint { return ExponentialHistogramDataPoint.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): ExponentialHistogramDataPoint { const message = createBaseExponentialHistogramDataPoint(); message.attributes = object.attributes?.map((e) => KeyValue.fromPartial(e)) || []; @@ -2330,12 +2366,14 @@ export const ExponentialHistogramDataPoint = { message.sum = object.sum ?? undefined; message.scale = object.scale ?? 0; message.zeroCount = object.zeroCount ?? BigInt("0"); - message.positive = (object.positive !== undefined && object.positive !== null) - ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.positive) - : undefined; - message.negative = (object.negative !== undefined && object.negative !== null) - ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.negative) - : undefined; + message.positive = + object.positive !== undefined && object.positive !== null + ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.positive) + : undefined; + message.negative = + object.negative !== undefined && object.negative !== null + ? ExponentialHistogramDataPoint_Buckets.fromPartial(object.negative) + : undefined; message.flags = object.flags ?? 0; message.exemplars = object.exemplars?.map((e) => Exemplar.fromPartial(e)) || []; message.min = object.min ?? undefined; @@ -2350,14 +2388,19 @@ function createBaseExponentialHistogramDataPoint_Buckets(): ExponentialHistogram } export const ExponentialHistogramDataPoint_Buckets = { - encode(message: ExponentialHistogramDataPoint_Buckets, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: ExponentialHistogramDataPoint_Buckets, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.offset !== 0) { writer.uint32(8).sint32(message.offset); } writer.uint32(18).fork(); for (const v of message.bucketCounts) { if (BigInt.asUintN(64, v) !== v) { - throw new globalThis.Error("a value provided in array field bucketCounts of type uint64 is too large"); + throw new globalThis.Error( + "a value provided in array field bucketCounts of type uint64 is too large" + ); } writer.uint64(v.toString()); } @@ -2426,12 +2469,12 @@ export const ExponentialHistogramDataPoint_Buckets = { }, create, I>>( - base?: I, + base?: I ): ExponentialHistogramDataPoint_Buckets { return ExponentialHistogramDataPoint_Buckets.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): ExponentialHistogramDataPoint_Buckets { const message = createBaseExponentialHistogramDataPoint_Buckets(); message.offset = object.offset ?? 0; @@ -2459,19 +2502,25 @@ export const SummaryDataPoint = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.startTimeUnixNano.toString()); } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(25).fixed64(message.timeUnixNano.toString()); } if (message.count !== BigInt("0")) { if (BigInt.asUintN(64, message.count) !== message.count) { - throw new globalThis.Error("value provided for field message.count of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.count of type fixed64 too large" + ); } writer.uint32(33).fixed64(message.count.toString()); } @@ -2534,7 +2583,9 @@ export const SummaryDataPoint = { break; } - message.quantileValues.push(SummaryDataPoint_ValueAtQuantile.decode(reader, reader.uint32())); + message.quantileValues.push( + SummaryDataPoint_ValueAtQuantile.decode(reader, reader.uint32()) + ); continue; case 8: if (tag !== 64) { @@ -2557,7 +2608,9 @@ export const SummaryDataPoint = { attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) : [], - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), timeUnixNano: isSet(object.timeUnixNano) ? BigInt(object.timeUnixNano) : BigInt("0"), count: isSet(object.count) ? BigInt(object.count) : BigInt("0"), sum: isSet(object.sum) ? globalThis.Number(object.sum) : 0, @@ -2586,7 +2639,9 @@ export const SummaryDataPoint = { obj.sum = message.sum; } if (message.quantileValues?.length) { - obj.quantileValues = message.quantileValues.map((e) => SummaryDataPoint_ValueAtQuantile.toJSON(e)); + obj.quantileValues = message.quantileValues.map((e) => + SummaryDataPoint_ValueAtQuantile.toJSON(e) + ); } if (message.flags !== 0) { obj.flags = Math.round(message.flags); @@ -2604,7 +2659,8 @@ export const SummaryDataPoint = { message.timeUnixNano = object.timeUnixNano ?? BigInt("0"); message.count = object.count ?? BigInt("0"); message.sum = object.sum ?? 0; - message.quantileValues = object.quantileValues?.map((e) => SummaryDataPoint_ValueAtQuantile.fromPartial(e)) || []; + message.quantileValues = + object.quantileValues?.map((e) => SummaryDataPoint_ValueAtQuantile.fromPartial(e)) || []; message.flags = object.flags ?? 0; return message; }, @@ -2615,7 +2671,10 @@ function createBaseSummaryDataPoint_ValueAtQuantile(): SummaryDataPoint_ValueAtQ } export const SummaryDataPoint_ValueAtQuantile = { - encode(message: SummaryDataPoint_ValueAtQuantile, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + encode( + message: SummaryDataPoint_ValueAtQuantile, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { if (message.quantile !== 0) { writer.uint32(9).double(message.quantile); } @@ -2674,12 +2733,12 @@ export const SummaryDataPoint_ValueAtQuantile = { }, create, I>>( - base?: I, + base?: I ): SummaryDataPoint_ValueAtQuantile { return SummaryDataPoint_ValueAtQuantile.fromPartial(base ?? ({} as any)); }, fromPartial, I>>( - object: I, + object: I ): SummaryDataPoint_ValueAtQuantile { const message = createBaseSummaryDataPoint_ValueAtQuantile(); message.quantile = object.quantile ?? 0; @@ -2706,7 +2765,9 @@ export const Exemplar = { } if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(17).fixed64(message.timeUnixNano.toString()); } @@ -2715,7 +2776,9 @@ export const Exemplar = { } if (message.asInt !== undefined) { if (BigInt.asIntN(64, message.asInt) !== message.asInt) { - throw new globalThis.Error("value provided for field message.asInt of type sfixed64 too large"); + throw new globalThis.Error( + "value provided for field message.asInt of type sfixed64 too large" + ); } writer.uint32(49).sfixed64(message.asInt.toString()); } @@ -2795,7 +2858,9 @@ export const Exemplar = { asDouble: isSet(object.asDouble) ? globalThis.Number(object.asDouble) : undefined, asInt: isSet(object.asInt) ? BigInt(object.asInt) : undefined, spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), }; }, @@ -2827,7 +2892,8 @@ export const Exemplar = { }, fromPartial, I>>(object: I): Exemplar { const message = createBaseExemplar(); - message.filteredAttributes = object.filteredAttributes?.map((e) => KeyValue.fromPartial(e)) || []; + message.filteredAttributes = + object.filteredAttributes?.map((e) => KeyValue.fromPartial(e)) || []; message.timeUnixNano = object.timeUnixNano ?? BigInt("0"); message.asDouble = object.asDouble ?? undefined; message.asInt = object.asInt ?? undefined; @@ -2864,14 +2930,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts index b634f23134d..60ac2239951 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/resource/v1/resource.ts @@ -99,14 +99,19 @@ export const Resource = { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function isSet(value: any): boolean { diff --git a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts index 835205809bf..b90afa3c2c9 100644 --- a/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts +++ b/internal-packages/otlp-importer/src/generated/opentelemetry/proto/trace/v1/trace.ts @@ -106,9 +106,7 @@ export interface ResourceSpans { * The resource for the spans in this message. * If this field is not set then no resource info is known. */ - resource: - | Resource - | undefined; + resource: Resource | undefined; /** A list of ScopeSpans that originate from a resource. */ scopeSpans: ScopeSpans[]; /** @@ -128,9 +126,7 @@ export interface ScopeSpans { * Semantically when InstrumentationScope isn't set, it is equivalent with * an empty instrumentation scope name (unknown). */ - scope: - | InstrumentationScope - | undefined; + scope: InstrumentationScope | undefined; /** A list of Spans that originate from an instrumentation scope. */ spans: Span[]; /** @@ -648,9 +644,10 @@ export const ResourceSpans = { }, fromPartial, I>>(object: I): ResourceSpans { const message = createBaseResourceSpans(); - message.resource = (object.resource !== undefined && object.resource !== null) - ? Resource.fromPartial(object.resource) - : undefined; + message.resource = + object.resource !== undefined && object.resource !== null + ? Resource.fromPartial(object.resource) + : undefined; message.scopeSpans = object.scopeSpans?.map((e) => ScopeSpans.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -715,7 +712,9 @@ export const ScopeSpans = { fromJSON(object: any): ScopeSpans { return { scope: isSet(object.scope) ? InstrumentationScope.fromJSON(object.scope) : undefined, - spans: globalThis.Array.isArray(object?.spans) ? object.spans.map((e: any) => Span.fromJSON(e)) : [], + spans: globalThis.Array.isArray(object?.spans) + ? object.spans.map((e: any) => Span.fromJSON(e)) + : [], schemaUrl: isSet(object.schemaUrl) ? globalThis.String(object.schemaUrl) : "", }; }, @@ -739,9 +738,10 @@ export const ScopeSpans = { }, fromPartial, I>>(object: I): ScopeSpans { const message = createBaseScopeSpans(); - message.scope = (object.scope !== undefined && object.scope !== null) - ? InstrumentationScope.fromPartial(object.scope) - : undefined; + message.scope = + object.scope !== undefined && object.scope !== null + ? InstrumentationScope.fromPartial(object.scope) + : undefined; message.spans = object.spans?.map((e) => Span.fromPartial(e)) || []; message.schemaUrl = object.schemaUrl ?? ""; return message; @@ -794,13 +794,17 @@ export const Span = { } if (message.startTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.startTimeUnixNano) !== message.startTimeUnixNano) { - throw new globalThis.Error("value provided for field message.startTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.startTimeUnixNano of type fixed64 too large" + ); } writer.uint32(57).fixed64(message.startTimeUnixNano.toString()); } if (message.endTimeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.endTimeUnixNano) !== message.endTimeUnixNano) { - throw new globalThis.Error("value provided for field message.endTimeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.endTimeUnixNano of type fixed64 too large" + ); } writer.uint32(65).fixed64(message.endTimeUnixNano.toString()); } @@ -958,14 +962,20 @@ export const Span = { fromJSON(object: any): Span { return { - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), traceState: isSet(object.traceState) ? globalThis.String(object.traceState) : "", - parentSpanId: isSet(object.parentSpanId) ? Buffer.from(bytesFromBase64(object.parentSpanId)) : Buffer.alloc(0), + parentSpanId: isSet(object.parentSpanId) + ? Buffer.from(bytesFromBase64(object.parentSpanId)) + : Buffer.alloc(0), flags: isSet(object.flags) ? globalThis.Number(object.flags) : 0, name: isSet(object.name) ? globalThis.String(object.name) : "", kind: isSet(object.kind) ? span_SpanKindFromJSON(object.kind) : 0, - startTimeUnixNano: isSet(object.startTimeUnixNano) ? BigInt(object.startTimeUnixNano) : BigInt("0"), + startTimeUnixNano: isSet(object.startTimeUnixNano) + ? BigInt(object.startTimeUnixNano) + : BigInt("0"), endTimeUnixNano: isSet(object.endTimeUnixNano) ? BigInt(object.endTimeUnixNano) : BigInt("0"), attributes: globalThis.Array.isArray(object?.attributes) ? object.attributes.map((e: any) => KeyValue.fromJSON(e)) @@ -973,10 +983,18 @@ export const Span = { droppedAttributesCount: isSet(object.droppedAttributesCount) ? globalThis.Number(object.droppedAttributesCount) : 0, - events: globalThis.Array.isArray(object?.events) ? object.events.map((e: any) => Span_Event.fromJSON(e)) : [], - droppedEventsCount: isSet(object.droppedEventsCount) ? globalThis.Number(object.droppedEventsCount) : 0, - links: globalThis.Array.isArray(object?.links) ? object.links.map((e: any) => Span_Link.fromJSON(e)) : [], - droppedLinksCount: isSet(object.droppedLinksCount) ? globalThis.Number(object.droppedLinksCount) : 0, + events: globalThis.Array.isArray(object?.events) + ? object.events.map((e: any) => Span_Event.fromJSON(e)) + : [], + droppedEventsCount: isSet(object.droppedEventsCount) + ? globalThis.Number(object.droppedEventsCount) + : 0, + links: globalThis.Array.isArray(object?.links) + ? object.links.map((e: any) => Span_Link.fromJSON(e)) + : [], + droppedLinksCount: isSet(object.droppedLinksCount) + ? globalThis.Number(object.droppedLinksCount) + : 0, status: isSet(object.status) ? Status.fromJSON(object.status) : undefined, }; }, @@ -1054,9 +1072,10 @@ export const Span = { message.droppedEventsCount = object.droppedEventsCount ?? 0; message.links = object.links?.map((e) => Span_Link.fromPartial(e)) || []; message.droppedLinksCount = object.droppedLinksCount ?? 0; - message.status = (object.status !== undefined && object.status !== null) - ? Status.fromPartial(object.status) - : undefined; + message.status = + object.status !== undefined && object.status !== null + ? Status.fromPartial(object.status) + : undefined; return message; }, }; @@ -1069,7 +1088,9 @@ export const Span_Event = { encode(message: Span_Event, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.timeUnixNano !== BigInt("0")) { if (BigInt.asUintN(64, message.timeUnixNano) !== message.timeUnixNano) { - throw new globalThis.Error("value provided for field message.timeUnixNano of type fixed64 too large"); + throw new globalThis.Error( + "value provided for field message.timeUnixNano of type fixed64 too large" + ); } writer.uint32(9).fixed64(message.timeUnixNano.toString()); } @@ -1266,7 +1287,9 @@ export const Span_Link = { fromJSON(object: any): Span_Link { return { - traceId: isSet(object.traceId) ? Buffer.from(bytesFromBase64(object.traceId)) : Buffer.alloc(0), + traceId: isSet(object.traceId) + ? Buffer.from(bytesFromBase64(object.traceId)) + : Buffer.alloc(0), spanId: isSet(object.spanId) ? Buffer.from(bytesFromBase64(object.spanId)) : Buffer.alloc(0), traceState: isSet(object.traceState) ? globalThis.String(object.traceState) : "", attributes: globalThis.Array.isArray(object?.attributes) @@ -1418,14 +1441,19 @@ function base64FromBytes(arr: Uint8Array): string { type Builtin = Date | Function | Uint8Array | string | number | boolean | bigint | undefined; -export type DeepPartial = T extends Builtin ? T - : T extends globalThis.Array ? globalThis.Array> - : T extends ReadonlyArray ? ReadonlyArray> - : T extends {} ? { [K in keyof T]?: DeepPartial } - : Partial; +export type DeepPartial = T extends Builtin + ? T + : T extends globalThis.Array + ? globalThis.Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; -export type Exact = P extends Builtin ? P +export type Exact = P extends Builtin + ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; function longToBigint(long: Long) { diff --git a/internal-packages/rbac/src/ability.test.ts b/internal-packages/rbac/src/ability.test.ts index e361bf0d544..a3250bd0030 100644 --- a/internal-packages/rbac/src/ability.test.ts +++ b/internal-packages/rbac/src/ability.test.ts @@ -1,5 +1,11 @@ import { describe, it, expect } from "vitest"; -import { permissiveAbility, superAbility, denyAbility, buildFallbackAbility, buildJwtAbility } from "./ability.js"; +import { + permissiveAbility, + superAbility, + denyAbility, + buildFallbackAbility, + buildJwtAbility, +} from "./ability.js"; describe("permissiveAbility", () => { it("allows any action on any resource type", () => { diff --git a/internal-packages/rbac/src/fallback.ts b/internal-packages/rbac/src/fallback.ts index eed301e7d87..7ada941f755 100644 --- a/internal-packages/rbac/src/fallback.ts +++ b/internal-packages/rbac/src/fallback.ts @@ -91,7 +91,10 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { // pre-RBAC routes that haven't been migrated, but it's a dead // code path for any route that uses `createLoaderApiRoute` / // `createActionApiRoute`. - const rawToken = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const rawToken = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); if (!rawToken) return { ok: false, status: 401, error: "Invalid or Missing API key" }; if (options?.allowJWT && isPublicJWT(rawToken)) { @@ -165,9 +168,7 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { }, }, parentEnvironment: { select: { id: true, apiKey: true } }, - childEnvironments: branchName - ? { where: { branchName, archivedAt: null } } - : undefined, + childEnvironments: branchName ? { where: { branchName, archivedAt: null } } : undefined, } as const; let env = await this.replica.runtimeEnvironment.findFirst({ where: { apiKey: rawToken }, @@ -338,7 +339,10 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController { request: Request, context: { organizationId?: string; projectId?: string } ): Promise { - const rawToken = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); + const rawToken = request.headers + .get("Authorization") + ?.replace(/^Bearer /, "") + .trim(); if (!rawToken || !isUserActorToken(rawToken)) { return { ok: false, status: 401, error: "Invalid or Missing user-actor token" }; } @@ -431,7 +435,9 @@ function isPublicJWT(token: string): boolean { const parts = token.split("."); if (parts.length !== 3) return false; try { - const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")); + const payload = JSON.parse( + Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8") + ); return payload !== null && typeof payload === "object" && payload.pub === true; } catch { return false; @@ -442,7 +448,9 @@ function extractJWTSub(token: string): string | undefined { const parts = token.split("."); if (parts.length !== 3) return undefined; try { - const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")); + const payload = JSON.parse( + Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8") + ); return payload !== null && typeof payload === "object" && typeof payload.sub === "string" ? payload.sub : undefined; diff --git a/internal-packages/rbac/src/index.ts b/internal-packages/rbac/src/index.ts index 7dbd9f83d1f..258dc12f3b1 100644 --- a/internal-packages/rbac/src/index.ts +++ b/internal-packages/rbac/src/index.ts @@ -108,10 +108,8 @@ class LazyController implements RoleBaseAccessController { // specifier in the error message is the plugin's own moduleName. const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); - const isModuleNotFound = - code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = - isModuleNotFound && message.includes(moduleName); + const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); if (!isPluginItselfMissing) { // Either the error wasn't a missing-module error at all, or the @@ -122,9 +120,7 @@ class LazyController implements RoleBaseAccessController { err ); } else if (process.env.RBAC_LOG_FALLBACK === "1") { - console.log( - "RBAC: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback" - ); + console.log("RBAC: no plugin installed (ERR_MODULE_NOT_FOUND); using fallback"); } // Fail-fast for deployments that require plugins to be present. Set @@ -135,9 +131,7 @@ class LazyController implements RoleBaseAccessController { // rolled back. Self-hosters leave REQUIRE_PLUGINS unset and continue // to use the fallback when no plugin is installed. if (process.env.REQUIRE_PLUGINS === "1") { - throw new Error( - `REQUIRE_PLUGINS=1 but plugin "${moduleName}" did not load: ${message}` - ); + throw new Error(`REQUIRE_PLUGINS=1 but plugin "${moduleName}" did not load: ${message}`); } return new RoleBaseAccessFallback(prisma, { @@ -289,10 +283,7 @@ class LazyController implements RoleBaseAccessController { class RoleBaseAccess { // Synchronous — returns a lazy controller that resolves any installed // plugin on first call. - create( - prisma: RbacPrismaInput, - options?: RbacCreateOptions - ): RoleBaseAccessController { + create(prisma: RbacPrismaInput, options?: RbacCreateOptions): RoleBaseAccessController { return new LazyController(prisma, options); } } diff --git a/internal-packages/redis/package.json b/internal-packages/redis/package.json index 6c7d8aa2608..14cf7f33d61 100644 --- a/internal-packages/redis/package.json +++ b/internal-packages/redis/package.json @@ -12,4 +12,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/internal-packages/redis/src/index.ts b/internal-packages/redis/src/index.ts index a1283d6153a..6fb30b9a4bf 100644 --- a/internal-packages/redis/src/index.ts +++ b/internal-packages/redis/src/index.ts @@ -24,11 +24,7 @@ export { Redis, type Callback, type RedisOptions, type Result, type RedisCommand */ export function defaultReconnectOnError(err: Error): boolean | 1 | 2 { const msg = err.message ?? ""; - if ( - msg.startsWith("READONLY") || - msg.startsWith("LOADING") || - msg.startsWith("UNBLOCKED") - ) { + if (msg.startsWith("READONLY") || msg.startsWith("LOADING") || msg.startsWith("UNBLOCKED")) { return 2; } return false; diff --git a/internal-packages/replication/package.json b/internal-packages/replication/package.json index 3aedabb3ed2..626aa42fede 100644 --- a/internal-packages/replication/package.json +++ b/internal-packages/replication/package.json @@ -25,4 +25,4 @@ "test": "vitest --sequence.concurrent=false --no-file-parallelism", "test:coverage": "vitest --sequence.concurrent=false --no-file-parallelism --coverage.enabled" } -} \ No newline at end of file +} diff --git a/internal-packages/run-engine/package.json b/internal-packages/run-engine/package.json index 414452da3b2..8d53974d10b 100644 --- a/internal-packages/run-engine/package.json +++ b/internal-packages/run-engine/package.json @@ -48,4 +48,4 @@ "build": "pnpm run clean && tsc -p tsconfig.build.json", "dev": "tsc --watch -p tsconfig.build.json" } -} \ No newline at end of file +} diff --git a/internal-packages/run-engine/src/batch-queue/completionTracker.ts b/internal-packages/run-engine/src/batch-queue/completionTracker.ts index 05793002fe5..f6570cfc54e 100644 --- a/internal-packages/run-engine/src/batch-queue/completionTracker.ts +++ b/internal-packages/run-engine/src/batch-queue/completionTracker.ts @@ -45,9 +45,9 @@ export class BatchCompletionTracker { }) { this.redis = createRedisClient(options.redis); this.logger = options.logger ?? { - debug: () => { }, - info: () => { }, - error: () => { }, + debug: () => {}, + info: () => {}, + error: () => {}, }; this.#registerCommands(); diff --git a/internal-packages/run-engine/src/batch-queue/index.ts b/internal-packages/run-engine/src/batch-queue/index.ts index 1f7a6221f48..10354e77c52 100644 --- a/internal-packages/run-engine/src/batch-queue/index.ts +++ b/internal-packages/run-engine/src/batch-queue/index.ts @@ -186,17 +186,17 @@ export class BatchQueue { // so we don't need the DLQ - we just need the retry scheduling. ...(options.retry ? { - retry: { - strategy: new ExponentialBackoffRetry({ - maxAttempts: options.retry.maxAttempts, - minTimeoutInMs: options.retry.minTimeoutInMs ?? 1_000, - maxTimeoutInMs: options.retry.maxTimeoutInMs ?? 30_000, - factor: options.retry.factor ?? 2, - randomize: options.retry.randomize ?? true, - }), - deadLetterQueue: false, - }, - } + retry: { + strategy: new ExponentialBackoffRetry({ + maxAttempts: options.retry.maxAttempts, + minTimeoutInMs: options.retry.minTimeoutInMs ?? 1_000, + maxTimeoutInMs: options.retry.maxTimeoutInMs ?? 30_000, + factor: options.retry.factor ?? 2, + randomize: options.retry.randomize ?? true, + }), + deadLetterQueue: false, + }, + } : {}), logger: this.logger, tracer: options.tracer, @@ -887,11 +887,7 @@ export class BatchQueue { }); await this.#startSpan("BatchQueue.failMessage", async () => { - return this.fairQueue.failMessage( - messageId, - queueId, - new Error(result.error) - ); + return this.fairQueue.failMessage(messageId, queueId, new Error(result.error)); }); // Don't record failure or check completion - message will be retried diff --git a/internal-packages/run-engine/src/batch-queue/tests/index.test.ts b/internal-packages/run-engine/src/batch-queue/tests/index.test.ts index ac44dae7083..56386b32ab7 100644 --- a/internal-packages/run-engine/src/batch-queue/tests/index.test.ts +++ b/internal-packages/run-engine/src/batch-queue/tests/index.test.ts @@ -717,64 +717,61 @@ describe("BatchQueue", () => { } ); - redisTest( - "should delay processing when rate limited", - async ({ redisContainer }) => { - let limitCallCount = 0; - const rateLimiter: GlobalRateLimiter = { - async limit() { - limitCallCount++; - // Rate limit the first 3 calls, then allow - if (limitCallCount <= 3) { - return { allowed: false, resetAt: Date.now() + 100 }; - } - return { allowed: true }; - }, - }; + redisTest("should delay processing when rate limited", async ({ redisContainer }) => { + let limitCallCount = 0; + const rateLimiter: GlobalRateLimiter = { + async limit() { + limitCallCount++; + // Rate limit the first 3 calls, then allow + if (limitCallCount <= 3) { + return { allowed: false, resetAt: Date.now() + 100 }; + } + return { allowed: true }; + }, + }; - const queue = new BatchQueue({ - redis: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - keyPrefix: "test:", - }, - drr: { quantum: 5, maxDeficit: 50 }, - consumerCount: 1, - consumerIntervalMs: 50, - startConsumers: true, - globalRateLimiter: rateLimiter, - }); + const queue = new BatchQueue({ + redis: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + keyPrefix: "test:", + }, + drr: { quantum: 5, maxDeficit: 50 }, + consumerCount: 1, + consumerIntervalMs: 50, + startConsumers: true, + globalRateLimiter: rateLimiter, + }); - let completionResult: CompleteBatchResult | null = null; + let completionResult: CompleteBatchResult | null = null; - try { - queue.onProcessItem(async ({ itemIndex }) => { - return { success: true, runId: `run_${itemIndex}` }; - }); + try { + queue.onProcessItem(async ({ itemIndex }) => { + return { success: true, runId: `run_${itemIndex}` }; + }); - queue.onBatchComplete(async (result) => { - completionResult = result; - }); + queue.onBatchComplete(async (result) => { + completionResult = result; + }); - await queue.initializeBatch(createInitOptions("batch1", "env1", 3)); - await enqueueItems(queue, "batch1", "env1", createBatchItems(3)); + await queue.initializeBatch(createInitOptions("batch1", "env1", 3)); + await enqueueItems(queue, "batch1", "env1", createBatchItems(3)); - // Should still complete despite initial rate limiting - await vi.waitFor( - () => { - expect(completionResult).not.toBeNull(); - }, - { timeout: 10000 } - ); + // Should still complete despite initial rate limiting + await vi.waitFor( + () => { + expect(completionResult).not.toBeNull(); + }, + { timeout: 10000 } + ); - expect(completionResult!.successfulRunCount).toBe(3); - // Rate limiter was called more times than items due to initial rejections - expect(limitCallCount).toBeGreaterThan(3); - } finally { - await queue.close(); - } + expect(completionResult!.successfulRunCount).toBe(3); + // Rate limiter was called more times than items due to initial rejections + expect(limitCallCount).toBeGreaterThan(3); + } finally { + await queue.close(); } - ); + }); }); describe("skipRetries on failed items", () => { diff --git a/internal-packages/run-engine/src/engine/index.ts b/internal-packages/run-engine/src/engine/index.ts index 5a733e0c1c2..106f5947fe3 100644 --- a/internal-packages/run-engine/src/engine/index.ts +++ b/internal-packages/run-engine/src/engine/index.ts @@ -126,7 +126,10 @@ export class RunEngine { this.logger = options.logger ?? new Logger("RunEngine", this.options.logLevel ?? "info"); this.prisma = options.prisma; this.readOnlyPrisma = options.readOnlyPrisma ?? this.prisma; - this.runStore = new PostgresRunStore({ prisma: this.prisma, readOnlyPrisma: this.readOnlyPrisma }); + this.runStore = new PostgresRunStore({ + prisma: this.prisma, + readOnlyPrisma: this.readOnlyPrisma, + }); this.runLockRedis = createRedisClient( { ...options.runLock.redis, @@ -205,14 +208,14 @@ export class RunEngine { ttlSystem: options.queue?.ttlSystem?.disabled ? undefined : { - shardCount: options.queue?.ttlSystem?.shardCount, - pollIntervalMs: options.queue?.ttlSystem?.pollIntervalMs, - batchSize: options.queue?.ttlSystem?.batchSize, - consumersDisabled: options.queue?.ttlSystem?.consumersDisabled, - workerQueueSuffix: "ttl-worker:{queue:ttl-expiration:}queue", - workerItemsSuffix: "ttl-worker:{queue:ttl-expiration:}items", - visibilityTimeoutMs: options.queue?.ttlSystem?.visibilityTimeoutMs ?? 30_000, - }, + shardCount: options.queue?.ttlSystem?.shardCount, + pollIntervalMs: options.queue?.ttlSystem?.pollIntervalMs, + batchSize: options.queue?.ttlSystem?.batchSize, + consumersDisabled: options.queue?.ttlSystem?.consumersDisabled, + workerQueueSuffix: "ttl-worker:{queue:ttl-expiration:}queue", + workerItemsSuffix: "ttl-worker:{queue:ttl-expiration:}items", + visibilityTimeoutMs: options.queue?.ttlSystem?.visibilityTimeoutMs ?? 30_000, + }, }); this.worker = new Worker({ @@ -233,9 +236,9 @@ export class RunEngine { id: payload.waitpointId, output: payload.error ? { - value: payload.error, - isError: true, - } + value: payload.error, + isError: true, + } : undefined, }); }, @@ -408,7 +411,11 @@ export class RunEngine { // Start TTL worker whenever TTL system is enabled, so expired runs enqueued by the // Lua script get processed even when the main engine worker is disabled (e.g. in tests). - if (options.queue?.ttlSystem && !options.queue.ttlSystem.disabled && !options.queue.ttlSystem.consumersDisabled) { + if ( + options.queue?.ttlSystem && + !options.queue.ttlSystem.disabled && + !options.queue.ttlSystem.consumersDisabled + ) { this.ttlWorker.start(); } @@ -529,14 +536,13 @@ export class RunEngine { const intervalMs = this.options.workerQueueObserver.intervalMs ?? 30_000; this.workerQueueObserverAbortController = new AbortController(); - this.#runWorkerQueueObserver( - intervalMs, - this.workerQueueObserverAbortController.signal - ).catch((error) => { - this.logger.error("Worker queue observer loop crashed", { - error: error instanceof Error ? error.message : String(error), - }); - }); + this.#runWorkerQueueObserver(intervalMs, this.workerQueueObserverAbortController.signal).catch( + (error) => { + this.logger.error("Worker queue observer loop crashed", { + error: error instanceof Error ? error.message : String(error), + }); + } + ); } async #runWorkerQueueObserver(intervalMs: number, signal: AbortSignal) { @@ -617,7 +623,7 @@ export class RunEngine { */ emitRunCancelledEvent?: boolean; }, - tx?: PrismaClientOrTransaction, + tx?: PrismaClientOrTransaction ): Promise { const prisma = tx ?? this.prisma; return startSpan(this.tracer, "createCancelledRun", async (span) => { @@ -665,9 +671,10 @@ export class RunEngine { // will be an empty object, which Prisma misreads as a relation // update op. Normalise to a real array (or undefined for the // empty case). - runTags: Array.isArray(snapshot.tags) && snapshot.tags.length > 0 - ? snapshot.tags - : undefined, + runTags: + Array.isArray(snapshot.tags) && snapshot.tags.length > 0 + ? snapshot.tags + : undefined, oneTimeUseToken: snapshot.oneTimeUseToken, parentTaskRunId: snapshot.parentTaskRunId, rootTaskRunId: snapshot.rootTaskRunId, @@ -733,13 +740,10 @@ export class RunEngine { // P2002 = unique constraint violation. Double-pop after a drainer // requeue can reach this. Idempotent: return the existing row // without re-emitting. - if ( - err instanceof Prisma.PrismaClientKnownRequestError && - err.code === "P2002" - ) { + if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") { this.logger.info( "createCancelledRun: row already exists, returning existing (idempotent)", - { friendlyId: snapshot.friendlyId }, + { friendlyId: snapshot.friendlyId } ); const existing = await this.runStore.findRun({ id }, prisma); if (existing) { @@ -754,7 +758,7 @@ export class RunEngine { return existing; } throw new Error( - `createCancelledRun conflict: existing run ${snapshot.friendlyId} has status ${existing.status}`, + `createCancelledRun conflict: existing run ${snapshot.friendlyId} has status ${existing.status}` ); } } @@ -843,18 +847,18 @@ export class RunEngine { debounce: debounce.mode === "trailing" ? { - ...debounce, - updateData: { - payload, - payloadType, - metadata, - metadataType, - tags, - maxAttempts, - maxDurationInSeconds, - machine, - }, - } + ...debounce, + updateData: { + payload, + payloadType, + metadata, + metadataType, + tags, + maxAttempts, + maxDurationInSeconds, + machine, + }, + } : debounce, tx: prisma, }); @@ -993,10 +997,10 @@ export class RunEngine { streamBasinName, debounce: debounce ? { - key: debounce.key, - delay: debounce.delay, - createdAt: new Date(), - } + key: debounce.key, + delay: debounce.delay, + createdAt: new Date(), + } : undefined, annotations, }, @@ -1017,9 +1021,9 @@ export class RunEngine { associatedWaitpoint: resumeParentOnCompletion && parentTaskRunId ? this.waitpointSystem.buildRunAssociatedWaitpoint({ - projectId: environment.project.id, - environmentId: environment.id, - }) + projectId: environment.project.id, + environmentId: environment.id, + }) : undefined, }, prisma @@ -1048,9 +1052,7 @@ export class RunEngine { }); if (targetFields.includes("oneTimeUseToken")) { - throw new RunOneTimeUseTokenError( - `One-time use token has already been used` - ); + throw new RunOneTimeUseTokenError(`One-time use token has already been used`); } // Only idempotency key collisions should be retried @@ -1260,9 +1262,9 @@ export class RunEngine { const waitpointData = resumeParentOnCompletion && parentTaskRunId ? this.waitpointSystem.buildRunAssociatedWaitpoint({ - projectId: environment.project.id, - environmentId: environment.id, - }) + projectId: environment.project.id, + environmentId: environment.id, + }) : undefined; // Create the run in terminal SYSTEM_FAILURE status. @@ -1307,11 +1309,7 @@ export class RunEngine { // If parent is waiting, block it with the waitpoint then immediately // complete it with the error output so the parent can resume. - if ( - resumeParentOnCompletion && - parentTaskRunId && - taskRun.associatedWaitpoint - ) { + if (resumeParentOnCompletion && parentTaskRunId && taskRun.associatedWaitpoint) { await this.waitpointSystem.blockRunAndCompleteWaitpoint({ runId: parentTaskRunId, waitpointId: taskRun.associatedWaitpoint.id, @@ -1579,7 +1577,10 @@ export class RunEngine { return this.runQueue.lengthOfEnvQueue(environment); } - async lengthOfQueue(environment: MinimalAuthenticatedEnvironment, queue: string): Promise { + async lengthOfQueue( + environment: MinimalAuthenticatedEnvironment, + queue: string + ): Promise { return this.runQueue.lengthOfQueue(environment, queue); } @@ -2499,21 +2500,21 @@ export class RunEngine { const error = latestSnapshot.environmentType === "DEVELOPMENT" ? ({ - type: "INTERNAL_ERROR", - code: taskStalledErrorCode, - message: errorMessage, - } satisfies TaskRunInternalError) - : this.options.treatProductionExecutionStallsAsOOM - ? ({ - type: "INTERNAL_ERROR", - code: "TASK_PROCESS_OOM_KILLED", - message: "Run was terminated due to running out of memory", - } satisfies TaskRunInternalError) - : ({ type: "INTERNAL_ERROR", code: taskStalledErrorCode, message: errorMessage, - } satisfies TaskRunInternalError); + } satisfies TaskRunInternalError) + : this.options.treatProductionExecutionStallsAsOOM + ? ({ + type: "INTERNAL_ERROR", + code: "TASK_PROCESS_OOM_KILLED", + message: "Run was terminated due to running out of memory", + } satisfies TaskRunInternalError) + : ({ + type: "INTERNAL_ERROR", + code: taskStalledErrorCode, + message: errorMessage, + } satisfies TaskRunInternalError); await this.runAttemptSystem.attemptFailed({ runId, @@ -2524,10 +2525,10 @@ export class RunEngine { error, retry: shouldRetry ? { - //250ms in the future - timestamp: Date.now() + retryDelay, - delay: retryDelay, - } + //250ms in the future + timestamp: Date.now() + retryDelay, + delay: retryDelay, + } : undefined, }, forceRequeue: true, diff --git a/internal-packages/run-engine/src/engine/retrying.ts b/internal-packages/run-engine/src/engine/retrying.ts index a64dfb796e1..a4a6eaf2f13 100644 --- a/internal-packages/run-engine/src/engine/retrying.ts +++ b/internal-packages/run-engine/src/engine/retrying.ts @@ -186,13 +186,16 @@ async function retryOOMOnMachine( prisma: PrismaClientOrTransaction, runStore: RunStore, runId: string -): Promise<{ - machine: string; - retrySettings: RetryOptions; - usageDurationMs: number; - costInCents: number; - machinePreset: string | null; -} | undefined> { +): Promise< + | { + machine: string; + retrySettings: RetryOptions; + usageDurationMs: number; + costInCents: number; + machinePreset: string | null; + } + | undefined +> { try { const run = await runStore.findRun( { diff --git a/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts b/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts index a77e60d05e7..cd22895429c 100644 --- a/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/delayedRunSystem.ts @@ -203,7 +203,6 @@ export class DelayedRunSystem { id: run.runtimeEnvironmentId, }, }); - }); } diff --git a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts index d8e9656f395..38d1cf79a8c 100644 --- a/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/executionSnapshotSystem.ts @@ -87,7 +87,9 @@ function enhanceExecutionSnapshotWithWaitpoints( type: w.type, completedAt: w.completedAt ?? new Date(), idempotencyKey: - w.userProvidedIdempotencyKey && !w.inactiveIdempotencyKey ? w.idempotencyKey : undefined, + w.userProvidedIdempotencyKey && !w.inactiveIdempotencyKey + ? w.idempotencyKey + : undefined, completedByTaskRun: w.completedByTaskRunId ? { id: w.completedByTaskRunId, diff --git a/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts b/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts index 741ad8a14f6..281808d7512 100644 --- a/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/pendingVersionSystem.ts @@ -73,8 +73,8 @@ export class PendingVersionSystem { // Step 1: ask the injected lookup (typically ClickHouse-backed) for // candidate run ids. Best-effort — results may be stale or incomplete. - const { runIds: candidateIds } = await this.$.pendingVersionRunIdLookup - .lookupPendingVersionRunIds({ + const { runIds: candidateIds } = + await this.$.pendingVersionRunIdLookup.lookupPendingVersionRunIds({ organizationId: backgroundWorker.runtimeEnvironment.organizationId, projectId: backgroundWorker.projectId, environmentId: backgroundWorker.runtimeEnvironmentId, diff --git a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts index 977c94a8e83..cb733919cb1 100644 --- a/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts @@ -1407,7 +1407,7 @@ export class RunAttemptSystem { const run = await this.$.runStore.cancelRun( runId, { - completedAt: finalizeRun ? completedAt ?? new Date() : completedAt, + completedAt: finalizeRun ? (completedAt ?? new Date()) : completedAt, error, ...(bulkActionId && { bulkActionId }), ...(usageUpdate && { @@ -2012,14 +2012,11 @@ export class RunAttemptSystem { if (!metadata.success) { // Customer's metadata operations don't match the schema (typically // non-JSON values in `operations[].value`). System ignores it. - this.$.logger.warn( - "RunEngine.completeRunAttempt(): failed to validate flushed metadata", - { - runId, - flushedMetadata: completion.flushedMetadata, - error: metadata.error, - } - ); + this.$.logger.warn("RunEngine.completeRunAttempt(): failed to validate flushed metadata", { + runId, + flushedMetadata: completion.flushedMetadata, + error: metadata.error, + }); return; } diff --git a/internal-packages/run-engine/src/engine/systems/ttlSystem.ts b/internal-packages/run-engine/src/engine/systems/ttlSystem.ts index faffa2c59e5..ac5950b884a 100644 --- a/internal-packages/run-engine/src/engine/systems/ttlSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/ttlSystem.ts @@ -157,136 +157,132 @@ export class TtlSystem { expired: string[]; skipped: { runId: string; reason: string }[]; }> { - return startSpan( - this.$.tracer, - "TtlSystem.expireRunsBatch", - async (span) => { - span.setAttribute("runCount", runIds.length); - - if (runIds.length === 0) { - return { expired: [], skipped: [] }; - } - - const expired: string[] = []; - const skipped: { runId: string; reason: string }[] = []; + return startSpan(this.$.tracer, "TtlSystem.expireRunsBatch", async (span) => { + span.setAttribute("runCount", runIds.length); - // Fetch all runs in a single query (no snapshot data needed) - const runs = await this.$.runStore.findRuns({ - where: { id: { in: runIds } }, - select: { - id: true, - spanId: true, - status: true, - lockedAt: true, - ttl: true, - taskEventStore: true, - createdAt: true, - associatedWaitpoint: { select: { id: true } }, - organizationId: true, - projectId: true, - runtimeEnvironmentId: true, - }, - }); - - // Filter runs that can be expired - const runsToExpire: typeof runs = []; + if (runIds.length === 0) { + return { expired: [], skipped: [] }; + } - for (const run of runs) { - if (run.status !== "PENDING") { - skipped.push({ runId: run.id, reason: `status_${run.status}` }); - continue; - } + const expired: string[] = []; + const skipped: { runId: string; reason: string }[] = []; + + // Fetch all runs in a single query (no snapshot data needed) + const runs = await this.$.runStore.findRuns({ + where: { id: { in: runIds } }, + select: { + id: true, + spanId: true, + status: true, + lockedAt: true, + ttl: true, + taskEventStore: true, + createdAt: true, + associatedWaitpoint: { select: { id: true } }, + organizationId: true, + projectId: true, + runtimeEnvironmentId: true, + }, + }); - if (run.lockedAt) { - skipped.push({ runId: run.id, reason: "locked" }); - continue; - } + // Filter runs that can be expired + const runsToExpire: typeof runs = []; - runsToExpire.push(run); + for (const run of runs) { + if (run.status !== "PENDING") { + skipped.push({ runId: run.id, reason: `status_${run.status}` }); + continue; } - // Track runs that weren't found - const foundRunIds = new Set(runs.map((r) => r.id)); - for (const runId of runIds) { - if (!foundRunIds.has(runId)) { - skipped.push({ runId, reason: "not_found" }); - } + if (run.lockedAt) { + skipped.push({ runId: run.id, reason: "locked" }); + continue; } - if (runsToExpire.length === 0) { - span.setAttribute("expiredCount", 0); - span.setAttribute("skippedCount", skipped.length); - return { expired, skipped }; + runsToExpire.push(run); + } + + // Track runs that weren't found + const foundRunIds = new Set(runs.map((r) => r.id)); + for (const runId of runIds) { + if (!foundRunIds.has(runId)) { + skipped.push({ runId, reason: "not_found" }); } + } - // Update all runs in a single SQL call (status, dates, and error JSON) - const now = new Date(); - const runIdsToExpire = runsToExpire.map((r) => r.id); - - const error: TaskRunError = { - type: "STRING_ERROR", - raw: "Run expired because the TTL was reached", - }; - - await this.$.runStore.expireRunsBatch(runIdsToExpire, { error, now }, this.$.prisma); - - // Process each run: enqueue waitpoint completion jobs and emit events - await pMap( - runsToExpire, - async (run) => { - try { - // Enqueue a finishWaitpoint worker job for resilient waitpoint completion - if (run.associatedWaitpoint) { - await this.$.worker.enqueue({ - id: `finishWaitpoint.ttl.${run.associatedWaitpoint.id}`, - job: "finishWaitpoint", - payload: { - waitpointId: run.associatedWaitpoint.id, - error: JSON.stringify(error), - }, - }); - } - - // This should really never happen - if (!run.organizationId) { - return; - } - - // Emit event - this.$.eventBus.emit("runExpired", { - run: { - id: run.id, - spanId: run.spanId, - ttl: run.ttl, - taskEventStore: run.taskEventStore, - createdAt: run.createdAt, - updatedAt: now, - completedAt: now, - expiredAt: now, - status: "EXPIRED" as TaskRunStatus, + if (runsToExpire.length === 0) { + span.setAttribute("expiredCount", 0); + span.setAttribute("skippedCount", skipped.length); + return { expired, skipped }; + } + + // Update all runs in a single SQL call (status, dates, and error JSON) + const now = new Date(); + const runIdsToExpire = runsToExpire.map((r) => r.id); + + const error: TaskRunError = { + type: "STRING_ERROR", + raw: "Run expired because the TTL was reached", + }; + + await this.$.runStore.expireRunsBatch(runIdsToExpire, { error, now }, this.$.prisma); + + // Process each run: enqueue waitpoint completion jobs and emit events + await pMap( + runsToExpire, + async (run) => { + try { + // Enqueue a finishWaitpoint worker job for resilient waitpoint completion + if (run.associatedWaitpoint) { + await this.$.worker.enqueue({ + id: `finishWaitpoint.ttl.${run.associatedWaitpoint.id}`, + job: "finishWaitpoint", + payload: { + waitpointId: run.associatedWaitpoint.id, + error: JSON.stringify(error), }, - time: now, - organization: { id: run.organizationId }, - project: { id: run.projectId }, - environment: { id: run.runtimeEnvironmentId }, }); + } - expired.push(run.id); - } catch (e) { - this.$.logger.error("Failed to process expired run", { - runId: run.id, - error: e, - }); + // This should really never happen + if (!run.organizationId) { + return; } - }, - { concurrency: 10, stopOnError: false } - ); - span.setAttribute("expiredCount", expired.length); - span.setAttribute("skippedCount", skipped.length); + // Emit event + this.$.eventBus.emit("runExpired", { + run: { + id: run.id, + spanId: run.spanId, + ttl: run.ttl, + taskEventStore: run.taskEventStore, + createdAt: run.createdAt, + updatedAt: now, + completedAt: now, + expiredAt: now, + status: "EXPIRED" as TaskRunStatus, + }, + time: now, + organization: { id: run.organizationId }, + project: { id: run.projectId }, + environment: { id: run.runtimeEnvironmentId }, + }); + + expired.push(run.id); + } catch (e) { + this.$.logger.error("Failed to process expired run", { + runId: run.id, + error: e, + }); + } + }, + { concurrency: 10, stopOnError: false } + ); - return { expired, skipped }; - } - ); + span.setAttribute("expiredCount", expired.length); + span.setAttribute("skippedCount", skipped.length); + + return { expired, skipped }; + }); } } diff --git a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts index 29eba297be5..85e1334ef40 100644 --- a/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts +++ b/internal-packages/run-engine/src/engine/systems/waitpointSystem.ts @@ -450,7 +450,7 @@ export class WaitpointSystem { // isolation, each statement gets its own snapshot. The CTE's snapshot is taken when // it starts, so if a concurrent completeWaitpoint commits during the CTE, the CTE // won't see it. This fresh query gets a new snapshot that reflects the latest commits. - const pendingCheck = await prisma.$queryRaw<{ pending_count: BigInt }[]>` + const pendingCheck = await prisma.$queryRaw<{ pending_count: bigint }[]>` SELECT COUNT(*) as pending_count FROM "Waitpoint" WHERE id IN (${Prisma.join($waitpoints)}) diff --git a/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts b/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts index a632c707390..26ec7b4f0dd 100644 --- a/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts +++ b/internal-packages/run-engine/src/engine/tests/batchTriggerAndWait.test.ts @@ -985,8 +985,8 @@ describe("RunEngine batchTriggerAndWait", () => { result.failedRunCount > 0 && result.successfulRunCount === 0 ? "ABORTED" : result.failedRunCount > 0 - ? "PARTIAL_FAILED" - : "PENDING"; + ? "PARTIAL_FAILED" + : "PENDING"; // Update batch in database await prisma.batchTaskRun.update({ diff --git a/internal-packages/run-engine/src/engine/tests/cancelling.test.ts b/internal-packages/run-engine/src/engine/tests/cancelling.test.ts index aecae7a2632..55f52aed172 100644 --- a/internal-packages/run-engine/src/engine/tests/cancelling.test.ts +++ b/internal-packages/run-engine/src/engine/tests/cancelling.test.ts @@ -217,9 +217,8 @@ describe("RunEngine cancelling", () => { expect(childEvent.run.spanId).toBe(childRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); @@ -313,9 +312,8 @@ describe("RunEngine cancelling", () => { expect(parentEvent.run.spanId).toBe(parentRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); @@ -440,9 +438,8 @@ describe("RunEngine cancelling", () => { expect(parentEvent.run.spanId).toBe(parentRun.spanId); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); } finally { await engine.quit(); diff --git a/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts b/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts index 68662074ea2..40e97179559 100644 --- a/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts +++ b/internal-packages/run-engine/src/engine/tests/createCancelledRun.test.ts @@ -97,52 +97,49 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); - containerTest( - "emits runCancelled with correct payload", - async ({ prisma, redisOptions }) => { - const env = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ prisma, ...baseEngineOptions(redisOptions) }); - const captured: EventBusEventArgs<"runCancelled">[0][] = []; - engine.eventBus.on("runCancelled", (event) => { - captured.push(event); - }); + containerTest("emits runCancelled with correct payload", async ({ prisma, redisOptions }) => { + const env = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + const engine = new RunEngine({ prisma, ...baseEngineOptions(redisOptions) }); + const captured: EventBusEventArgs<"runCancelled">[0][] = []; + engine.eventBus.on("runCancelled", (event) => { + captured.push(event); + }); - try { - const cancelledAt = new Date(); - const cancelReason = "Test cancel"; - const friendlyId = freshRunId(); - await engine.createCancelledRun({ - snapshot: { - friendlyId, - environment: env, - taskIdentifier: "test-task", - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "0000000000000000cccc000000000000", - spanId: "dddd000000000000", - queue: "task/test-task", - isTest: false, - tags: [], - }, - cancelledAt, - cancelReason, - }); + try { + const cancelledAt = new Date(); + const cancelReason = "Test cancel"; + const friendlyId = freshRunId(); + await engine.createCancelledRun({ + snapshot: { + friendlyId, + environment: env, + taskIdentifier: "test-task", + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "0000000000000000cccc000000000000", + spanId: "dddd000000000000", + queue: "task/test-task", + isTest: false, + tags: [], + }, + cancelledAt, + cancelReason, + }); - expect(captured).toHaveLength(1); - expect(captured[0]!.run.status).toBe("CANCELED"); - expect(captured[0]!.run.friendlyId).toBe(friendlyId); - expect(captured[0]!.run.error).toEqual({ type: "STRING_ERROR", raw: cancelReason }); - expect(captured[0]!.organization.id).toBe(env.organization.id); - } finally { - await engine.quit(); - } - }, - ); + expect(captured).toHaveLength(1); + expect(captured[0]!.run.status).toBe("CANCELED"); + expect(captured[0]!.run.friendlyId).toBe(friendlyId); + expect(captured[0]!.run.error).toEqual({ type: "STRING_ERROR", raw: cancelReason }); + expect(captured[0]!.organization.id).toBe(env.organization.id); + } finally { + await engine.quit(); + } + }); containerTest( "emitRunCancelledEvent: false suppresses the bus emit but still writes the CANCELED PG row", @@ -192,7 +189,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); containerTest( @@ -232,7 +229,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); // Regression: cjson encodes empty Lua tables as `{}`, not `[]`. When @@ -279,7 +276,7 @@ describe("RunEngine.createCancelledRun", () => { } finally { await engine.quit(); } - }, + } ); // Regression: the P2002-on-id idempotency path used to return ANY @@ -335,11 +332,11 @@ describe("RunEngine.createCancelledRun", () => { }, cancelledAt: new Date(), cancelReason: "Should not silently overwrite a live row", - }), + }) ).rejects.toThrow(/createCancelledRun conflict.*PENDING/); } finally { await engine.quit(); } - }, + } ); }); diff --git a/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts b/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts index 84d33baa87d..a6d2eba2a76 100644 --- a/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts +++ b/internal-packages/run-engine/src/engine/tests/createFailedTaskRun.test.ts @@ -9,105 +9,108 @@ import { setupAuthenticatedEnvironment } from "./setup.js"; vi.setConfig({ testTimeout: 60_000 }); describe("RunEngine.createFailedTaskRun", () => { - containerTest("emits runFailed so the alert pipeline wakes up", async ({ prisma, redisOptions }) => { - // The mollifier drainer (and batch-trigger over-limit path) call - // createFailedTaskRun to write a terminal SYSTEM_FAILURE PG row - // for runs that never actually executed. Without an explicit - // runFailed emit, the row lands silently — the - // runEngineHandlers' `runFailed` listener (which enqueues - // PerformTaskRunAlertsService) never fires, so customers' - // configured TASK_RUN alert channels miss the failure entirely. - // - // Regression intent: if the emit is removed or moved out of - // createFailedTaskRun's success path, this test fails. The - // shape assertions pin the fields the alert delivery service - // reads from the event payload (run.id, run.status, error, - // attemptNumber=0 as the never-ran-marker). - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest( + "emits runFailed so the alert pipeline wakes up", + async ({ prisma, redisOptions }) => { + // The mollifier drainer (and batch-trigger over-limit path) call + // createFailedTaskRun to write a terminal SYSTEM_FAILURE PG row + // for runs that never actually executed. Without an explicit + // runFailed emit, the row lands silently — the + // runEngineHandlers' `runFailed` listener (which enqueues + // PerformTaskRunAlertsService) never fires, so customers' + // configured TASK_RUN alert channels miss the failure entirely. + // + // Regression intent: if the emit is removed or moved out of + // createFailedTaskRun's success path, this test fails. The + // shape assertions pin the fields the alert delivery service + // reads from the event payload (run.id, run.status, error, + // attemptNumber=0 as the never-ran-marker). + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, - machines: { - defaultMachine: "small-1x", + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, + defaultMachine: "small-1x", + machines: { + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, + }, }, + baseCostInCents: 0.0005, }, - baseCostInCents: 0.0005, - }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - try { - const failedEvents: EventBusEventArgs<"runFailed">[0][] = []; - engine.eventBus.on("runFailed", (event) => { - failedEvents.push(event); + tracer: trace.getTracer("test", "0.0.0"), }); - const friendlyId = generateFriendlyId("run"); - const taskIdentifier = "drainer-terminal-test"; + try { + const failedEvents: EventBusEventArgs<"runFailed">[0][] = []; + engine.eventBus.on("runFailed", (event) => { + failedEvents.push(event); + }); - const failed = await engine.createFailedTaskRun({ - friendlyId, - environment: { - id: authenticatedEnvironment.id, - type: authenticatedEnvironment.type, - project: { id: authenticatedEnvironment.project.id }, - organization: { id: authenticatedEnvironment.organization.id }, - }, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - error: { - type: "STRING_ERROR", - raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", - }, - traceId: "0123456789abcdef0123456789abcdef", - spanId: "fedcba9876543210", - }); + const friendlyId = generateFriendlyId("run"); + const taskIdentifier = "drainer-terminal-test"; + + const failed = await engine.createFailedTaskRun({ + friendlyId, + environment: { + id: authenticatedEnvironment.id, + type: authenticatedEnvironment.type, + project: { id: authenticatedEnvironment.project.id }, + organization: { id: authenticatedEnvironment.organization.id }, + }, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + error: { + type: "STRING_ERROR", + raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", + }, + traceId: "0123456789abcdef0123456789abcdef", + spanId: "fedcba9876543210", + }); - expect(failed.status).toBe("SYSTEM_FAILURE"); + expect(failed.status).toBe("SYSTEM_FAILURE"); - expect(failedEvents).toHaveLength(1); - const event = failedEvents[0]; - expect(event.run.id).toBe(failed.id); - expect(event.run.status).toBe("SYSTEM_FAILURE"); - expect(event.run.spanId).toBe("fedcba9876543210"); - // attemptNumber=0 is the marker that the run never executed — - // it's a synthesised terminal failure, not an exhausted-retries - // failure. Downstream consumers can use this to distinguish. - expect(event.run.attemptNumber).toBe(0); - expect(event.run.usageDurationMs).toBe(0); - expect(event.run.costInCents).toBe(0); - expect(event.run.error).toEqual({ - type: "STRING_ERROR", - raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", - }); - expect(event.organization.id).toBe(authenticatedEnvironment.organization.id); - expect(event.project.id).toBe(authenticatedEnvironment.project.id); - expect(event.environment.id).toBe(authenticatedEnvironment.id); - } finally { - await engine.quit(); + expect(failedEvents).toHaveLength(1); + const event = failedEvents[0]; + expect(event.run.id).toBe(failed.id); + expect(event.run.status).toBe("SYSTEM_FAILURE"); + expect(event.run.spanId).toBe("fedcba9876543210"); + // attemptNumber=0 is the marker that the run never executed — + // it's a synthesised terminal failure, not an exhausted-retries + // failure. Downstream consumers can use this to distinguish. + expect(event.run.attemptNumber).toBe(0); + expect(event.run.usageDurationMs).toBe(0); + expect(event.run.costInCents).toBe(0); + expect(event.run.error).toEqual({ + type: "STRING_ERROR", + raw: "Mollifier drainer terminal failure: synthetic engine.trigger panic", + }); + expect(event.organization.id).toBe(authenticatedEnvironment.organization.id); + expect(event.project.id).toBe(authenticatedEnvironment.project.id); + expect(event.environment.id).toBe(authenticatedEnvironment.id); + } finally { + await engine.quit(); + } } - }); + ); // The TriggerFailedTaskService.call() path wraps createFailedTaskRun // inside `repository.traceEvent({ incomplete: false, isError: true })` @@ -126,7 +129,11 @@ describe("RunEngine.createFailedTaskRun", () => { const engine = new RunEngine({ prisma, worker: { redis: redisOptions, workers: 1, tasksPerWorker: 10, pollIntervalMs: 100 }, - queue: { redis: redisOptions, masterQueueConsumersDisabled: true, processWorkerQueueDebounceMs: 50 }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, runLock: { redis: redisOptions }, machines: { defaultMachine: "small-1x", @@ -171,6 +178,6 @@ describe("RunEngine.createFailedTaskRun", () => { } finally { await engine.quit(); } - }, + } ); }); diff --git a/internal-packages/run-engine/src/engine/tests/debounce.test.ts b/internal-packages/run-engine/src/engine/tests/debounce.test.ts index e46f0de07cd..089756c5bcd 100644 --- a/internal-packages/run-engine/src/engine/tests/debounce.test.ts +++ b/internal-packages/run-engine/src/engine/tests/debounce.test.ts @@ -96,117 +96,114 @@ describe("RunEngine debounce", () => { } }); - containerTest( - "Debounce: multiple triggers return same run", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Debounce: multiple triggers return same run", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // First trigger creates run - const run1 = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, + // First trigger creates run + const run1 = await engine.trigger( + { + number: 1, + friendlyId: "run_deb1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "first"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "user-123", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Second trigger should return same run - const run2 = await engine.trigger( - { - number: 2, - friendlyId: "run_deb2", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "second"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, + // Second trigger should return same run + const run2 = await engine.trigger( + { + number: 2, + friendlyId: "run_deb2", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "second"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "user-123", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Both should return the same run (first run wins) - expect(run2.id).toBe(run1.id); - expect(run2.friendlyId).toBe(run1.friendlyId); + // Both should return the same run (first run wins) + expect(run2.id).toBe(run1.id); + expect(run2.friendlyId).toBe(run1.friendlyId); - // Only one run should exist in DB - const runs = await prisma.taskRun.findMany({ - where: { - taskIdentifier, - runtimeEnvironmentId: authenticatedEnvironment.id, - }, - }); - expect(runs.length).toBe(1); - } finally { - await engine.quit(); - } + // Only one run should exist in DB + const runs = await prisma.taskRun.findMany({ + where: { + taskIdentifier, + runtimeEnvironmentId: authenticatedEnvironment.id, + }, + }); + expect(runs.length).toBe(1); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: delay extension on subsequent triggers", @@ -441,91 +438,88 @@ describe("RunEngine debounce", () => { } ); - containerTest( - "Debounce: run executes after final delay", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Debounce: run executes after final delay", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // First trigger with 1s delay - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 1000), - debounce: { - key: "user-123", - delay: "1s", - }, + // First trigger with 1s delay + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_deb1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: '{"data": "first"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: "task/test-task", + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 1000), + debounce: { + key: "user-123", + delay: "1s", }, - prisma - ); + }, + prisma + ); - // Verify it's in DELAYED status - let executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.snapshot.executionStatus).toBe("DELAYED"); + // Verify it's in DELAYED status + let executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.snapshot.executionStatus).toBe("DELAYED"); - // Wait for delay to pass - await setTimeout(1500); + // Wait for delay to pass + await setTimeout(1500); - // Should now be QUEUED - executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.snapshot.executionStatus).toBe("QUEUED"); - } finally { - await engine.quit(); - } + // Should now be QUEUED + executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.snapshot.executionStatus).toBe("QUEUED"); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: no longer works after run is enqueued", @@ -620,122 +614,16 @@ describe("RunEngine debounce", () => { queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "user-123", - delay: "5s", - }, - }, - prisma - ); - - // Should be a different run - expect(run2.id).not.toBe(run1.id); - } finally { - await engine.quit(); - } - } - ); - - containerTest( - "Debounce: max duration exceeded creates new run", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - - // Set a very short max debounce duration - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, - machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, - }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 500, // Very short max duration - }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - try { - const taskIdentifier = "test-task"; - - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - - // First trigger - const run1 = await engine.trigger( - { - number: 1, - friendlyId: "run_deb1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "first"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 2000), - debounce: { - key: "user-123", - delay: "2s", - }, - }, - prisma - ); - - // Wait for max duration to be exceeded - await setTimeout(700); - - // Second trigger should create a new run because max duration exceeded - const run2 = await engine.trigger( - { - number: 2, - friendlyId: "run_deb2", - environment: authenticatedEnvironment, - taskIdentifier, - payload: '{"data": "second"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: "task/test-task", - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 2000), + delayUntil: new Date(Date.now() + 5000), debounce: { key: "user-123", - delay: "2s", + delay: "5s", }, }, prisma ); - // Should be a different run because max duration exceeded + // Should be a different run expect(run2.id).not.toBe(run1.id); } finally { await engine.quit(); @@ -744,10 +632,11 @@ describe("RunEngine debounce", () => { ); containerTest( - "Debounce keys are scoped to task identifier", + "Debounce: max duration exceeded creates new run", async ({ prisma, redisOptions }) => { const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + // Set a very short max debounce duration const engine = new RunEngine({ prisma, worker: { @@ -775,80 +664,182 @@ describe("RunEngine debounce", () => { baseCostInCents: 0.0001, }, debounce: { - maxDebounceDurationMs: 60_000, + maxDebounceDurationMs: 500, // Very short max duration }, tracer: trace.getTracer("test", "0.0.0"), }); try { - const taskIdentifier1 = "test-task-1"; - const taskIdentifier2 = "test-task-2"; + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier1); - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier2); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger task 1 with debounce key + // First trigger const run1 = await engine.trigger( { number: 1, - friendlyId: "run_task1", + friendlyId: "run_deb1", environment: authenticatedEnvironment, - taskIdentifier: taskIdentifier1, - payload: '{"data": "task1"}', + taskIdentifier, + payload: '{"data": "first"}', payloadType: "application/json", context: {}, traceContext: {}, traceId: "t12345", spanId: "s12345", workerQueue: "main", - queue: `task/${taskIdentifier1}`, + queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), + delayUntil: new Date(Date.now() + 2000), debounce: { - key: "shared-key", - delay: "5s", + key: "user-123", + delay: "2s", }, }, prisma ); - // Trigger task 2 with same debounce key - should create separate run + // Wait for max duration to be exceeded + await setTimeout(700); + + // Second trigger should create a new run because max duration exceeded const run2 = await engine.trigger( { number: 2, - friendlyId: "run_task2", + friendlyId: "run_deb2", environment: authenticatedEnvironment, - taskIdentifier: taskIdentifier2, - payload: '{"data": "task2"}', + taskIdentifier, + payload: '{"data": "second"}', payloadType: "application/json", context: {}, traceContext: {}, traceId: "t12346", spanId: "s12346", workerQueue: "main", - queue: `task/${taskIdentifier2}`, + queue: "task/test-task", isTest: false, tags: [], - delayUntil: new Date(Date.now() + 5000), + delayUntil: new Date(Date.now() + 2000), debounce: { - key: "shared-key", - delay: "5s", + key: "user-123", + delay: "2s", }, }, prisma ); - // Should be different runs (debounce scoped to task) + // Should be a different run because max duration exceeded expect(run2.id).not.toBe(run1.id); - expect(run1.taskIdentifier).toBe(taskIdentifier1); - expect(run2.taskIdentifier).toBe(taskIdentifier2); } finally { await engine.quit(); } } ); + containerTest("Debounce keys are scoped to task identifier", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", + machines: { + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, + }, + }, + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); + + try { + const taskIdentifier1 = "test-task-1"; + const taskIdentifier2 = "test-task-2"; + + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier1); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier2); + + // Trigger task 1 with debounce key + const run1 = await engine.trigger( + { + number: 1, + friendlyId: "run_task1", + environment: authenticatedEnvironment, + taskIdentifier: taskIdentifier1, + payload: '{"data": "task1"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier1}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "shared-key", + delay: "5s", + }, + }, + prisma + ); + + // Trigger task 2 with same debounce key - should create separate run + const run2 = await engine.trigger( + { + number: 2, + friendlyId: "run_task2", + environment: authenticatedEnvironment, + taskIdentifier: taskIdentifier2, + payload: '{"data": "task2"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: `task/${taskIdentifier2}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "shared-key", + delay: "5s", + }, + }, + prisma + ); + + // Should be different runs (debounce scoped to task) + expect(run2.id).not.toBe(run1.id); + expect(run1.taskIdentifier).toBe(taskIdentifier1); + expect(run2.taskIdentifier).toBe(taskIdentifier2); + } finally { + await engine.quit(); + } + }); + containerTest( "Debounce with triggerAndWait: parent blocked by debounced child run", async ({ prisma, redisOptions }) => { @@ -1219,130 +1210,127 @@ describe("RunEngine debounce", () => { } ); - containerTest( - "Debounce: keys scoped to environment", - async ({ prisma, redisOptions }) => { - // Create production environment (also creates org and project) - const prodEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - - // Create a second environment (development) within the same org/project - const devEnvironment = await prisma.runtimeEnvironment.create({ - data: { - type: "DEVELOPMENT", - slug: "dev-slug", - projectId: prodEnvironment.projectId, - organizationId: prodEnvironment.organizationId, - apiKey: "dev_api_key", - pkApiKey: "dev_pk_api_key", - shortcode: "dev_short", - maximumConcurrencyLimit: 10, - }, - include: { - project: true, - organization: true, - orgMember: true, - }, - }); + containerTest("Debounce: keys scoped to environment", async ({ prisma, redisOptions }) => { + // Create production environment (also creates org and project) + const prodEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + + // Create a second environment (development) within the same org/project + const devEnvironment = await prisma.runtimeEnvironment.create({ + data: { + type: "DEVELOPMENT", + slug: "dev-slug", + projectId: prodEnvironment.projectId, + organizationId: prodEnvironment.organizationId, + apiKey: "dev_api_key", + pkApiKey: "dev_pk_api_key", + shortcode: "dev_short", + maximumConcurrencyLimit: 10, + }, + include: { + project: true, + organization: true, + orgMember: true, + }, + }); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, - }, - debounce: { - maxDebounceDurationMs: 60_000, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + debounce: { + maxDebounceDurationMs: 60_000, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, prodEnvironment, taskIdentifier); - await setupBackgroundWorker(engine, devEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, prodEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, devEnvironment, taskIdentifier); - // Trigger in production environment - const runProd = await engine.trigger( - { - number: 1, - friendlyId: "run_prod1", - environment: prodEnvironment, - taskIdentifier, - payload: '{"env": "prod"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "same-key", - delay: "5s", - }, + // Trigger in production environment + const runProd = await engine.trigger( + { + number: 1, + friendlyId: "run_prod1", + environment: prodEnvironment, + taskIdentifier, + payload: '{"env": "prod"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "same-key", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Trigger in development environment with same key - should create separate run - const runDev = await engine.trigger( - { - number: 2, - friendlyId: "run_dev1", - environment: devEnvironment, - taskIdentifier, - payload: '{"env": "dev"}', - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12346", - spanId: "s12346", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - delayUntil: new Date(Date.now() + 5000), - debounce: { - key: "same-key", - delay: "5s", - }, + // Trigger in development environment with same key - should create separate run + const runDev = await engine.trigger( + { + number: 2, + friendlyId: "run_dev1", + environment: devEnvironment, + taskIdentifier, + payload: '{"env": "dev"}', + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12346", + spanId: "s12346", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + delayUntil: new Date(Date.now() + 5000), + debounce: { + key: "same-key", + delay: "5s", }, - prisma - ); + }, + prisma + ); - // Should be different runs (debounce scoped to environment) - expect(runDev.id).not.toBe(runProd.id); - expect(runProd.runtimeEnvironmentId).toBe(prodEnvironment.id); - expect(runDev.runtimeEnvironmentId).toBe(devEnvironment.id); - } finally { - await engine.quit(); - } + // Should be different runs (debounce scoped to environment) + expect(runDev.id).not.toBe(runProd.id); + expect(runProd.runtimeEnvironmentId).toBe(prodEnvironment.id); + expect(runDev.runtimeEnvironmentId).toBe(devEnvironment.id); + } finally { + await engine.quit(); } - ); + }); containerTest( "Debounce: concurrent triggers only create one run (distributed race protection)", @@ -2957,13 +2945,7 @@ describe("RunEngine debounce", () => { assertNonNullable(originalDelayUntil); try { - const blockResult = await blockingRedis.set( - run1.id, - "test-blocker", - "PX", - 30_000, - "NX" - ); + const blockResult = await blockingRedis.set(run1.id, "test-blocker", "PX", 30_000, "NX"); expect(blockResult).toBe("OK"); const run2 = await engine.trigger( @@ -3196,4 +3178,3 @@ describe("RunEngine debounce", () => { ); } }); - diff --git a/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts b/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts index 5871bee832b..fd67ff2cf35 100644 --- a/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts +++ b/internal-packages/run-engine/src/engine/tests/dequeuing.test.ts @@ -160,9 +160,8 @@ describe("RunEngine dequeuing", () => { expect(executionDataBefore.snapshot.executionStatus).toBe("PENDING_EXECUTING"); // Verify run is in concurrency - const envConcurrencyBefore = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyBefore = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyBefore).toBe(1); // Simulate DB failure fallback: call nackMessage directly via Redis @@ -174,9 +173,8 @@ describe("RunEngine dequeuing", () => { // Verify concurrency is cleared - this is the key fix! // Without this fix, the run would stay in concurrency sets forever - const envConcurrencyAfter = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyAfter = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyAfter).toBe(0); // Verify the message is back in the queue diff --git a/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts b/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts index 15831d10839..f48c7c37c91 100644 --- a/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts +++ b/internal-packages/run-engine/src/engine/tests/getSnapshotsSince.test.ts @@ -13,10 +13,7 @@ import { setupTestScenario, generateLargeOutput, } from "./helpers/snapshotTestHelpers.js"; -import { - copySnapshotsToReplica, - createTestMetricsMeter, -} from "./helpers/replicaTestHelpers.js"; +import { copySnapshotsToReplica, createTestMetricsMeter } from "./helpers/replicaTestHelpers.js"; import { generateFriendlyId } from "@trigger.dev/core/v3/isomorphic"; vi.setConfig({ testTimeout: 120_000 }); diff --git a/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts b/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts index f981f35145f..a4084a8c199 100644 --- a/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts +++ b/internal-packages/run-engine/src/engine/tests/helpers/snapshotTestHelpers.ts @@ -294,7 +294,9 @@ export async function setupTestScenario( } // Get the waitpoint IDs that should be "completed" at this snapshot - const completedWaitpointIds = waitpoints.slice(0, config.completedWaitpointCount).map((w) => w.id); + const completedWaitpointIds = waitpoints + .slice(0, config.completedWaitpointCount) + .map((w) => w.id); const snapshot = await createTestSnapshot(prisma, { runId: run.id, diff --git a/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts b/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts index dedcff5b5b6..d45e3bcd6cb 100644 --- a/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts +++ b/internal-packages/run-engine/src/engine/tests/lazyWaitpoint.test.ts @@ -196,298 +196,289 @@ describe("RunEngine lazy waitpoint creation", () => { } }); - containerTest( - "Completion without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Completion without waitpoint succeeds", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_complete1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - }, - prisma - ); + // Trigger a standalone run (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_complete1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + }, + prisma + ); - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); - // Dequeue and start the run - await setTimeout(500); - const dequeued = await engine.dequeueFromWorkerQueue({ - consumerId: "test_12345", - workerQueue: "main", - }); - const attemptResult = await engine.startRunAttempt({ - runId: run.id, - snapshotId: dequeued[0].snapshot.id, - }); + // Dequeue and start the run + await setTimeout(500); + const dequeued = await engine.dequeueFromWorkerQueue({ + consumerId: "test_12345", + workerQueue: "main", + }); + const attemptResult = await engine.startRunAttempt({ + runId: run.id, + snapshotId: dequeued[0].snapshot.id, + }); - // Complete the run - should NOT throw even without waitpoint - const completeResult = await engine.completeRunAttempt({ - runId: run.id, - snapshotId: attemptResult.snapshot.id, - completion: { - id: run.id, - ok: true, - output: '{"result":"success"}', - outputType: "application/json", - }, - }); + // Complete the run - should NOT throw even without waitpoint + const completeResult = await engine.completeRunAttempt({ + runId: run.id, + snapshotId: attemptResult.snapshot.id, + completion: { + id: run.id, + ok: true, + output: '{"result":"success"}', + outputType: "application/json", + }, + }); - // Verify run completed successfully - expect(completeResult.attemptStatus).toBe("RUN_FINISHED"); - const executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.run.status).toBe("COMPLETED_SUCCESSFULLY"); - expect(executionData.snapshot.executionStatus).toBe("FINISHED"); - } finally { - await engine.quit(); - } + // Verify run completed successfully + expect(completeResult.attemptStatus).toBe("RUN_FINISHED"); + const executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.run.status).toBe("COMPLETED_SUCCESSFULLY"); + expect(executionData.snapshot.executionStatus).toBe("FINISHED"); + } finally { + await engine.quit(); } - ); + }); - containerTest( - "Cancellation without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("Cancellation without waitpoint succeeds", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, - pollIntervalMs: 100, - }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - }, - runLock: { - redis: redisOptions, - }, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, }, - tracer: trace.getTracer("test", "0.0.0"), - }); + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - try { - const taskIdentifier = "test-task"; + try { + const taskIdentifier = "test-task"; - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_cancel1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - }, - prisma - ); + // Trigger a standalone run (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_cancel1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + }, + prisma + ); - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); - // Cancel the run - should NOT throw even without waitpoint - const cancelResult = await engine.cancelRun({ - runId: run.id, - reason: "Test cancellation", - }); + // Cancel the run - should NOT throw even without waitpoint + const cancelResult = await engine.cancelRun({ + runId: run.id, + reason: "Test cancellation", + }); - // Verify run was cancelled - expect(cancelResult.alreadyFinished).toBe(false); - const executionData = await engine.getRunExecutionData({ runId: run.id }); - assertNonNullable(executionData); - expect(executionData.run.status).toBe("CANCELED"); - expect(executionData.snapshot.executionStatus).toBe("FINISHED"); - } finally { - await engine.quit(); - } + // Verify run was cancelled + expect(cancelResult.alreadyFinished).toBe(false); + const executionData = await engine.getRunExecutionData({ runId: run.id }); + assertNonNullable(executionData); + expect(executionData.run.status).toBe("CANCELED"); + expect(executionData.snapshot.executionStatus).toBe("FINISHED"); + } finally { + await engine.quit(); } - ); + }); - containerTest( - "TTL expiration without waitpoint succeeds", - async ({ prisma, redisOptions }) => { - const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); + containerTest("TTL expiration without waitpoint succeeds", async ({ prisma, redisOptions }) => { + const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION"); - const engine = new RunEngine({ - prisma, - worker: { - redis: redisOptions, - workers: 1, - tasksPerWorker: 10, + const engine = new RunEngine({ + prisma, + worker: { + redis: redisOptions, + workers: 1, + tasksPerWorker: 10, + pollIntervalMs: 100, + }, + queue: { + redis: redisOptions, + masterQueueConsumersDisabled: true, + processWorkerQueueDebounceMs: 50, + ttlSystem: { pollIntervalMs: 100, + batchSize: 10, + batchMaxWaitMs: 100, }, - queue: { - redis: redisOptions, - masterQueueConsumersDisabled: true, - processWorkerQueueDebounceMs: 50, - ttlSystem: { - pollIntervalMs: 100, - batchSize: 10, - batchMaxWaitMs: 100, - }, - }, - runLock: { - redis: redisOptions, - }, + }, + runLock: { + redis: redisOptions, + }, + machines: { + defaultMachine: "small-1x", machines: { - defaultMachine: "small-1x", - machines: { - "small-1x": { - name: "small-1x" as const, - cpu: 0.5, - memory: 0.5, - centsPerMs: 0.0001, - }, + "small-1x": { + name: "small-1x" as const, + cpu: 0.5, + memory: 0.5, + centsPerMs: 0.0001, }, - baseCostInCents: 0.0001, }, - tracer: trace.getTracer("test", "0.0.0"), - }); - - try { - const taskIdentifier = "test-task"; + baseCostInCents: 0.0001, + }, + tracer: trace.getTracer("test", "0.0.0"), + }); - await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); + try { + const taskIdentifier = "test-task"; - // TTL only expires runs still queued waiting on a concurrency slot. - await engine.runQueue.updateEnvConcurrencyLimits({ - ...authenticatedEnvironment, - maximumConcurrencyLimit: 0, - }); + await setupBackgroundWorker(engine, authenticatedEnvironment, taskIdentifier); - // Trigger a standalone run with TTL (no waitpoint) - const run = await engine.trigger( - { - number: 1, - friendlyId: "run_ttl1", - environment: authenticatedEnvironment, - taskIdentifier, - payload: "{}", - payloadType: "application/json", - context: {}, - traceContext: {}, - traceId: "t12345", - spanId: "s12345", - workerQueue: "main", - queue: `task/${taskIdentifier}`, - isTest: false, - tags: [], - ttl: "1s", - }, - prisma - ); - - // Verify no waitpoint - const dbRun = await prisma.taskRun.findFirst({ - where: { id: run.id }, - include: { associatedWaitpoint: true }, - }); - assertNonNullable(dbRun); - expect(dbRun.associatedWaitpoint).toBeNull(); + // TTL only expires runs still queued waiting on a concurrency slot. + await engine.runQueue.updateEnvConcurrencyLimits({ + ...authenticatedEnvironment, + maximumConcurrencyLimit: 0, + }); - // Wait for TTL to expire - await setTimeout(1_500); + // Trigger a standalone run with TTL (no waitpoint) + const run = await engine.trigger( + { + number: 1, + friendlyId: "run_ttl1", + environment: authenticatedEnvironment, + taskIdentifier, + payload: "{}", + payloadType: "application/json", + context: {}, + traceContext: {}, + traceId: "t12345", + spanId: "s12345", + workerQueue: "main", + queue: `task/${taskIdentifier}`, + isTest: false, + tags: [], + ttl: "1s", + }, + prisma + ); - // Verify run expired successfully (no throw). - // The batch TTL path does not create execution snapshots, so check - // the status directly from the database rather than via - // getRunExecutionData. - const expiredRun = await prisma.taskRun.findUnique({ - where: { id: run.id }, - select: { status: true }, - }); - expect(expiredRun?.status).toBe("EXPIRED"); - } finally { - await engine.quit(); - } + // Verify no waitpoint + const dbRun = await prisma.taskRun.findFirst({ + where: { id: run.id }, + include: { associatedWaitpoint: true }, + }); + assertNonNullable(dbRun); + expect(dbRun.associatedWaitpoint).toBeNull(); + + // Wait for TTL to expire + await setTimeout(1_500); + + // Verify run expired successfully (no throw). + // The batch TTL path does not create execution snapshots, so check + // the status directly from the database rather than via + // getRunExecutionData. + const expiredRun = await prisma.taskRun.findUnique({ + where: { id: run.id }, + select: { status: true }, + }); + expect(expiredRun?.status).toBe("EXPIRED"); + } finally { + await engine.quit(); } - ); + }); containerTest( "getOrCreateRunWaitpoint: returns existing waitpoint", diff --git a/internal-packages/run-engine/src/engine/tests/trigger.test.ts b/internal-packages/run-engine/src/engine/tests/trigger.test.ts index a4f500e72aa..3bd0c833d1b 100644 --- a/internal-packages/run-engine/src/engine/tests/trigger.test.ts +++ b/internal-packages/run-engine/src/engine/tests/trigger.test.ts @@ -103,9 +103,8 @@ describe("RunEngine trigger()", () => { expect(queueLength).toBe(1); //concurrency before - const envConcurrencyBefore = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyBefore = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyBefore).toBe(0); await setTimeout(500); @@ -119,9 +118,8 @@ describe("RunEngine trigger()", () => { expect(dequeued[0].run.id).toBe(run.id); expect(dequeued[0].run.attemptNumber).toBe(1); - const envConcurrencyAfter = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyAfter = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyAfter).toBe(1); let attemptEvent: EventBusEventArgs<"runAttemptStarted">[0] | undefined = undefined; @@ -186,9 +184,8 @@ describe("RunEngine trigger()", () => { expect(completedEvent.run.outputType).toBe("application/json"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); //standalone triggers don't create waitpoints, so none should exist @@ -312,9 +309,8 @@ describe("RunEngine trigger()", () => { expect(executionData3.run.status).toBe("COMPLETED_WITH_ERRORS"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); //standalone triggers don't create waitpoints, so none should exist @@ -526,5 +522,4 @@ describe("RunEngine trigger()", () => { } } ); - }); diff --git a/internal-packages/run-engine/src/engine/tests/ttl.test.ts b/internal-packages/run-engine/src/engine/tests/ttl.test.ts index 13d4c55b669..52b26452070 100644 --- a/internal-packages/run-engine/src/engine/tests/ttl.test.ts +++ b/internal-packages/run-engine/src/engine/tests/ttl.test.ts @@ -115,9 +115,8 @@ describe("RunEngine ttl", () => { expect(expiredRun?.status).toBe("EXPIRED"); //concurrency should have been released - const envConcurrencyCompleted = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrencyCompleted = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrencyCompleted).toBe(0); // Queue sorted set should be empty (run removed from queue) @@ -634,9 +633,8 @@ describe("RunEngine ttl", () => { } // Concurrency should be released for all - const envConcurrency = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrency = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrency).toBe(0); // Queue sorted set should be empty (all runs removed from queue) @@ -986,9 +984,8 @@ describe("RunEngine ttl", () => { expect(expiredRunData?.status).toBe("EXPIRED"); // Concurrency should be released - const envConcurrency = await engine.runQueue.currentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const envConcurrency = + await engine.runQueue.currentConcurrencyOfEnvironment(authenticatedEnvironment); expect(envConcurrency).toBe(0); } finally { await engine.quit(); @@ -1070,9 +1067,8 @@ describe("RunEngine ttl", () => { await engine.runQueue.redis.sadd(envConcurrencyKey, run.id); await engine.runQueue.redis.sadd(envDequeuedKey, run.id); - const concurrencyBefore = await engine.runQueue.getCurrentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const concurrencyBefore = + await engine.runQueue.getCurrentConcurrencyOfEnvironment(authenticatedEnvironment); expect(concurrencyBefore).toContain(run.id); await setTimeout(1_500); @@ -1090,9 +1086,8 @@ describe("RunEngine ttl", () => { { timeout: 15_000, interval: 200 } ); - const concurrencyAfter = await engine.runQueue.getCurrentConcurrencyOfEnvironment( - authenticatedEnvironment - ); + const concurrencyAfter = + await engine.runQueue.getCurrentConcurrencyOfEnvironment(authenticatedEnvironment); expect(concurrencyAfter).not.toContain(run.id); const stillInDequeued = await engine.runQueue.redis.sismember(envDequeuedKey, run.id); diff --git a/internal-packages/run-engine/src/run-queue/index.ts b/internal-packages/run-engine/src/run-queue/index.ts index 9808f96f41a..c1003357579 100644 --- a/internal-packages/run-engine/src/run-queue/index.ts +++ b/internal-packages/run-engine/src/run-queue/index.ts @@ -387,7 +387,7 @@ export class RunQueue { const burstFactor = result ? Number(result) - : this.options.defaultEnvConcurrencyBurstFactor ?? 1; + : (this.options.defaultEnvConcurrencyBurstFactor ?? 1); const limit = await this.getEnvConcurrencyLimit(env); @@ -399,7 +399,7 @@ export class RunQueue { const burstFactor = result ? Number(result) - : this.options.defaultEnvConcurrencyBurstFactor ?? 1; + : (this.options.defaultEnvConcurrencyBurstFactor ?? 1); return burstFactor; } @@ -1389,9 +1389,7 @@ export class RunQueue { })) { const now = Date.now(); - const [error, expiredRuns] = await tryCatch( - this.#expireTtlRuns(shard, now, batchSize) - ); + const [error, expiredRuns] = await tryCatch(this.#expireTtlRuns(shard, now, batchSize)); if (error) { this.logger.error(`Failed to expire TTL runs for shard ${shard}`, { @@ -1858,8 +1856,9 @@ export class RunQueue { const workerQueueKey = this.keys.workerQueueKey(message.workerQueue); const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(message.queue); const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(message.queue); - const envConcurrencyLimitBurstFactorKey = - this.keys.envConcurrencyLimitBurstFactorKeyFromQueue(message.queue); + const envConcurrencyLimitBurstFactorKey = this.keys.envConcurrencyLimitBurstFactorKeyFromQueue( + message.queue + ); // The value stored in the worker queue list — used to look up the message payload on dequeue const messageKeyValue = messageKey; @@ -2064,9 +2063,7 @@ export class RunQueue { this.keys.envIdFromQueue(messageQueue), ttlShardCount ); - const ttlQueueKey = this.options.ttlSystem - ? this.keys.ttlQueueKeyForShard(ttlShard) - : ""; + const ttlQueueKey = this.options.ttlSystem ? this.keys.ttlQueueKeyForShard(ttlShard) : ""; this.logger.debug("#callDequeueMessagesFromQueue", { messageQueue, @@ -2172,13 +2169,11 @@ export class RunQueue { }); const ckIndexKey = this.keys.ckIndexKeyFromQueue(ckWildcardQueue); - const queueConcurrencyLimitKey = - this.keys.queueConcurrencyLimitKeyFromQueue(ckWildcardQueue); + const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(ckWildcardQueue); const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(ckWildcardQueue); const envConcurrencyLimitBurstFactorKey = this.keys.envConcurrencyLimitBurstFactorKeyFromQueue(ckWildcardQueue); - const envCurrentConcurrencyKey = - this.keys.envCurrentConcurrencyKeyFromQueue(ckWildcardQueue); + const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(ckWildcardQueue); const messageKeyPrefix = this.keys.messageKeyPrefixFromQueue(ckWildcardQueue); const envQueueKey = this.keys.envQueueKeyFromQueue(ckWildcardQueue); const masterQueueKey = this.keys.masterQueueKeyForShard(shard); @@ -2189,9 +2184,7 @@ export class RunQueue { this.keys.envIdFromQueue(ckWildcardQueue), ttlShardCount ); - const ttlQueueKey = this.options.ttlSystem - ? this.keys.ttlQueueKeyForShard(ttlShard) - : ""; + const ttlQueueKey = this.options.ttlSystem ? this.keys.ttlQueueKeyForShard(ttlShard) : ""; this.logger.debug("#callDequeueMessagesFromCkQueue", { ckWildcardQueue, diff --git a/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts b/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts index 23213bfe15c..c08b3a515e7 100644 --- a/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/ckCounters.test.ts @@ -74,184 +74,168 @@ function makeMessage(overrides: Partial = {}): InputPayload { vi.setConfig({ testTimeout: 60_000 }); describe("CK base-queue counters", () => { - redisTest( - "lengthOfQueue returns aggregate across CK variants", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const messages = [ - makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), - makeMessage({ runId: "r2", concurrencyKey: "ck-a", timestamp: now + 1 }), - makeMessage({ runId: "r3", concurrencyKey: "ck-b", timestamp: now + 2 }), - makeMessage({ runId: "r4", concurrencyKey: "ck-c", timestamp: now + 3 }), - ]; - - for (const msg of messages) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } + redisTest("lengthOfQueue returns aggregate across CK variants", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const messages = [ + makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), + makeMessage({ runId: "r2", concurrencyKey: "ck-a", timestamp: now + 1 }), + makeMessage({ runId: "r3", concurrencyKey: "ck-b", timestamp: now + 2 }), + makeMessage({ runId: "r4", concurrencyKey: "ck-c", timestamp: now + 3 }), + ]; + + for (const msg of messages) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } - // Aggregate (no CK arg) should sum all variants - expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue)).toBe(4); + // Aggregate (no CK arg) should sum all variants + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue)).toBe(4); - // Per-variant still works - expect( - await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-a") - ).toBe(2); - expect( - await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-b") - ).toBe(1); + // Per-variant still works + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-a")).toBe(2); + expect(await queue.lengthOfQueue(authenticatedEnvDev, messages[0].queue, "ck-b")).toBe(1); - // Plural lengthOfQueues should also see the aggregate - const lengths = await queue.lengthOfQueues(authenticatedEnvDev, [messages[0].queue]); - expect(lengths[messages[0].queue]).toBe(4); - } finally { - await queue.quit(); - } + // Plural lengthOfQueues should also see the aggregate + const lengths = await queue.lengthOfQueues(authenticatedEnvDev, [messages[0].queue]); + expect(lengths[messages[0].queue]).toBe(4); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "lazy init from pre-existing CK backlog", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const baseMsg = makeMessage({ runId: "seed", concurrencyKey: "ck-a", timestamp: now }); - - // Pre-populate two variants via direct ZADD to simulate pre-deploy backlog - // (no counter touched). ioredis auto-prefixes keys with `runqueue:test:`, - // so we pass un-prefixed keys. - const variantA = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-a"); - const variantB = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-b"); - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(variantA); - for (let i = 0; i < 10; i++) { - await queue.redis.zadd(variantA, now + i, `old-a-${i}`); - } - for (let i = 0; i < 5; i++) { - await queue.redis.zadd(variantB, now + i, `old-b-${i}`); - } - await queue.redis.zadd(ckIndexKey, now, variantA); - await queue.redis.zadd(ckIndexKey, now, variantB); + redisTest("lazy init from pre-existing CK backlog", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const baseMsg = makeMessage({ runId: "seed", concurrencyKey: "ck-a", timestamp: now }); + + // Pre-populate two variants via direct ZADD to simulate pre-deploy backlog + // (no counter touched). ioredis auto-prefixes keys with `runqueue:test:`, + // so we pass un-prefixed keys. + const variantA = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-a"); + const variantB = testOptions.keys.queueKey(authenticatedEnvDev, baseMsg.queue, "ck-b"); + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(variantA); + for (let i = 0; i < 10; i++) { + await queue.redis.zadd(variantA, now + i, `old-a-${i}`); + } + for (let i = 0; i < 5; i++) { + await queue.redis.zadd(variantB, now + i, `old-b-${i}`); + } + await queue.redis.zadd(ckIndexKey, now, variantA); + await queue.redis.zadd(ckIndexKey, now, variantB); + + // Counter should not yet exist + const counterKey = testOptions.keys.queueLengthCounterKeyFromQueue(variantA); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // First CK enqueue: lazy init should compute 15 (pre-state), then INCR to 16 + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "new-a", concurrencyKey: "ck-a", timestamp: now + 100 }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); - // Counter should not yet exist - const counterKey = testOptions.keys.queueLengthCounterKeyFromQueue(variantA); - expect(await queue.redis.exists(counterKey)).toBe(0); + const counterVal = await queue.redis.get(counterKey); + expect(Number(counterVal)).toBe(16); - // First CK enqueue: lazy init should compute 15 (pre-state), then INCR to 16 + // lengthOfQueue should also reflect 16 + expect(await queue.lengthOfQueue(authenticatedEnvDev, baseMsg.queue)).toBe(16); + } finally { + await queue.quit(); + } + }); + + redisTest("non-CK queue regression: counter never created", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + for (let i = 0; i < 5; i++) { await queue.enqueueMessage({ env: authenticatedEnvDev, - message: makeMessage({ runId: "new-a", concurrencyKey: "ck-a", timestamp: now + 100 }), + message: makeMessage({ runId: `r${i}`, timestamp: Date.now() + i }), workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - const counterVal = await queue.redis.get(counterKey); - expect(Number(counterVal)).toBe(16); - - // lengthOfQueue should also reflect 16 - expect(await queue.lengthOfQueue(authenticatedEnvDev, baseMsg.queue)).toBe(16); - } finally { - await queue.quit(); } - } - ); - redisTest( - "non-CK queue regression: counter never created", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - for (let i = 0; i < 5; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `r${i}`, timestamp: Date.now() + i }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Counter key should not exist for a pure non-CK queue - const counterKey = testOptions.keys.queueLengthCounterKey(authenticatedEnvDev, "task/my-task"); - expect(await queue.redis.exists(counterKey)).toBe(0); - - // But lengthOfQueue still returns 5 via base ZCARD - expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); - } finally { - await queue.quit(); - } + // Counter key should not exist for a pure non-CK queue + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // But lengthOfQueue still returns 5 via base ZCARD + expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); + } finally { + await queue.quit(); } - ); - - redisTest( - "mixed CK + non-CK on same base queue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - // 3 non-CK + 2 CK on same base queue name - for (let i = 0; i < 3; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `nonck-${i}`, timestamp: Date.now() + i }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - for (let i = 0; i < 2; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ - runId: `ck-${i}`, - concurrencyKey: "ck-a", - timestamp: Date.now() + 100 + i, - }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } + }); - expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); - } finally { - await queue.quit(); + redisTest("mixed CK + non-CK on same base queue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + // 3 non-CK + 2 CK on same base queue name + for (let i = 0; i < 3; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: `nonck-${i}`, timestamp: Date.now() + i }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); } + for (let i = 0; i < 2; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ + runId: `ck-${i}`, + concurrencyKey: "ck-a", + timestamp: Date.now() + 100 + i, + }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } + + expect(await queue.lengthOfQueue(authenticatedEnvDev, "task/my-task")).toBe(5); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "length counter decrements on dequeue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msgs = [ - makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), - makeMessage({ runId: "r2", concurrencyKey: "ck-b", timestamp: now + 1 }), - ]; - for (const msg of msgs) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(2); + redisTest("length counter decrements on dequeue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msgs = [ + makeMessage({ runId: "r1", concurrencyKey: "ck-a", timestamp: now }), + makeMessage({ runId: "r2", concurrencyKey: "ck-b", timestamp: now + 1 }), + ]; + for (const msg of msgs) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + } + expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(2); - const shard = testOptions.keys.masterQueueShardForEnvironment(msgs[0].environmentId, 2); - await queue.testDequeueFromMasterQueue(shard, msgs[0].environmentId, 10); + const shard = testOptions.keys.masterQueueShardForEnvironment(msgs[0].environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msgs[0].environmentId, 10); - // Both dequeued → counter should be 0 - expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(0); - } finally { - await queue.quit(); - } + // Both dequeued → counter should be 0 + expect(await queue.lengthOfQueue(authenticatedEnvDev, msgs[0].queue)).toBe(0); + } finally { + await queue.quit(); } - ); + }); redisTest( "running counter bumps when dequeueMessageFromKey is called for a CK message", @@ -264,8 +248,7 @@ describe("CK base-queue counters", () => { const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); const queueKey = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); const messageKey = testOptions.keys.messageKey(msg.orgId, msg.runId); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); await queue.redis.set( messageKey, @@ -292,9 +275,7 @@ describe("CK base-queue counters", () => { expect(Number(await queue.redis.get(runningCounterKey))).toBe(3); - const running = await queue.currentConcurrencyOfQueues(authenticatedEnvDev, [ - msg.queue, - ]); + const running = await queue.currentConcurrencyOfQueues(authenticatedEnvDev, [msg.queue]); expect(running[msg.queue]).toBe(3); } finally { await queue.quit(); @@ -302,64 +283,54 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "floor-at-zero protects against spurious decrements", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const variantA = testOptions.keys.queueKey( - authenticatedEnvDev, - "task/my-task", - "ck-a" - ); - const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variantA); - await queue.redis.set(runningCounterKey, "0"); - - // Call the Tracked release directly with un-prefixed keys (ioredis prepends the prefix) - await queue.redis.releaseConcurrencyTracked( - testOptions.keys.queueCurrentConcurrencyKeyFromQueue(variantA), - testOptions.keys.envCurrentConcurrencyKey(authenticatedEnvDev), - testOptions.keys.queueCurrentDequeuedKeyFromQueue(variantA), - testOptions.keys.envCurrentDequeuedKey(authenticatedEnvDev), - runningCounterKey, - testOptions.keys.ckIndexKeyFromQueue(variantA), - "phantom-message", - "runqueue:test:", - DEFAULT_COUNTER_TTL - ); - - expect(Number(await queue.redis.get(runningCounterKey))).toBe(0); - } finally { - await queue.quit(); - } + redisTest("floor-at-zero protects against spurious decrements", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const variantA = testOptions.keys.queueKey(authenticatedEnvDev, "task/my-task", "ck-a"); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variantA); + await queue.redis.set(runningCounterKey, "0"); + + // Call the Tracked release directly with un-prefixed keys (ioredis prepends the prefix) + await queue.redis.releaseConcurrencyTracked( + testOptions.keys.queueCurrentConcurrencyKeyFromQueue(variantA), + testOptions.keys.envCurrentConcurrencyKey(authenticatedEnvDev), + testOptions.keys.queueCurrentDequeuedKeyFromQueue(variantA), + testOptions.keys.envCurrentDequeuedKey(authenticatedEnvDev), + runningCounterKey, + testOptions.keys.ckIndexKeyFromQueue(variantA), + "phantom-message", + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); + + expect(Number(await queue.redis.get(runningCounterKey))).toBe(0); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "lengthCounter has 24h TTL after lazy-init", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); + redisTest("lengthCounter has 24h TTL after lazy-init", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - const ttl = await queue.redis.ttl(counterKey); - // Expect roughly 86400; allow only a few seconds of slack for test scheduling. - expect(ttl).toBeGreaterThanOrEqual(86390); - expect(ttl).toBeLessThanOrEqual(86400); - } finally { - await queue.quit(); - } + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + const ttl = await queue.redis.ttl(counterKey); + // Expect roughly 86400; allow only a few seconds of slack for test scheduling. + expect(ttl).toBeGreaterThanOrEqual(86390); + expect(ttl).toBeLessThanOrEqual(86400); + } finally { + await queue.quit(); } - ); + }); redisTest( "duplicate CK enqueue (same runId) does not inflate lengthCounter", @@ -438,8 +409,7 @@ describe("CK base-queue counters", () => { const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); const queueKey = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); const messageKey = testOptions.keys.messageKey(msg.orgId, msg.runId); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(queueKey); await queue.redis.set( messageKey, @@ -447,11 +417,19 @@ describe("CK base-queue counters", () => { ); // First call: SADD returns 1, runningCounter goes 0 -> 1. - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); expect(Number(await queue.redis.get(runningCounterKey))).toBe(1); // Second call on the same messageKey: SADD returns 0, runningCounter must stay at 1. - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); expect(Number(await queue.redis.get(runningCounterKey))).toBe(1); } finally { await queue.quit(); @@ -459,50 +437,47 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "nack lazy-inits lengthCounter when it expired", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); - // Seed three messages on the CK variant so the lazy-init has a non-trivial floor. - for (let i = 0; i < 3; i++) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: `seed-${i}`, concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Simulate counter expiry (the 24h TTL kicked in). - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - await queue.redis.del(counterKey); - expect(await queue.redis.exists(counterKey)).toBe(0); - - // Dequeue one to currentConcurrency so we have something to nack back. - const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); - await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); - - // Nack a CK message. nackMessageCkTracked should lazy-init the counter - // (find 2 already in zset + 1 we're re-queuing) rather than starting from 1. - await queue.nackMessage({ - orgId: msg.orgId, - messageId: "seed-0", + redisTest("nack lazy-inits lengthCounter when it expired", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); + // Seed three messages on the CK variant so the lazy-init has a non-trivial floor. + for (let i = 0; i < 3; i++) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: `seed-${i}`, concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - // 3 originals, 1 was dequeued (still re-queued by nack), counter should now reflect all 3. - const observed = await queue.lengthOfQueue(authenticatedEnvDev, msg.queue); - expect(observed).toBe(3); - } finally { - await queue.quit(); } + + // Simulate counter expiry (the 24h TTL kicked in). + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + await queue.redis.del(counterKey); + expect(await queue.redis.exists(counterKey)).toBe(0); + + // Dequeue one to currentConcurrency so we have something to nack back. + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); + + // Nack a CK message. nackMessageCkTracked should lazy-init the counter + // (find 2 already in zset + 1 we're re-queuing) rather than starting from 1. + await queue.nackMessage({ + orgId: msg.orgId, + messageId: "seed-0", + skipDequeueProcessing: true, + }); + + // 3 originals, 1 was dequeued (still re-queued by nack), counter should now reflect all 3. + const observed = await queue.lengthOfQueue(authenticatedEnvDev, msg.queue); + expect(observed).toBe(3); + } finally { + await queue.quit(); } - ); + }); redisTest( "fast-path-variant dequeue seeds runningCounter without missing its own SCARD", @@ -521,14 +496,9 @@ describe("CK base-queue counters", () => { msg.queue, "ck-fastpath" ); - const otherVariant = testOptions.keys.queueKey( - authenticatedEnvDev, - msg.queue, - "ck-other" - ); + const otherVariant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-other"); const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue(fastVariant); - const runningCounterKey = - testOptions.keys.queueRunningCounterKeyFromQueue(fastVariant); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(fastVariant); // Seed 3 prior fast-path runs into the fast variant's currentDequeued (no zset, no ckIndex). for (let i = 0; i < 3; i++) { @@ -549,7 +519,11 @@ describe("CK base-queue counters", () => { messageKey, JSON.stringify({ ...msg, queue: fastVariant, version: "2", workerQueue: "wq" }) ); - await queue.redis.dequeueMessageFromKeyTracked(messageKey, "runqueue:test:", DEFAULT_COUNTER_TTL); + await queue.redis.dequeueMessageFromKeyTracked( + messageKey, + "runqueue:test:", + DEFAULT_COUNTER_TTL + ); // True running across all variants: 3 (fast prior) + 1 (fast new) + 1 (other) = 5. // Without the ownVariantSeen fix, the seed would miss the fast variant entirely @@ -562,82 +536,76 @@ describe("CK base-queue counters", () => { } ); - redisTest( - "release lazy-inits runningCounter when missing", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); - const variant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); - const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variant); - - // Enqueue so ckIndex picks up the variant. - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - // Seed two running messages directly into currentDequeued so release has something to remove. - await queue.redis.sadd(`${variant}:currentDequeued`, msg.runId); - await queue.redis.sadd(`${variant}:currentDequeued`, "r2"); + redisTest("release lazy-inits runningCounter when missing", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const msg = makeMessage({ runId: "r1", concurrencyKey: "ck-a" }); + const variant = testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, "ck-a"); + const runningCounterKey = testOptions.keys.queueRunningCounterKeyFromQueue(variant); + + // Enqueue so ckIndex picks up the variant. + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + // Seed two running messages directly into currentDequeued so release has something to remove. + await queue.redis.sadd(`${variant}:currentDequeued`, msg.runId); + await queue.redis.sadd(`${variant}:currentDequeued`, "r2"); - // Counter is missing — simulates post-TTL state without waiting. - expect(await queue.redis.exists(runningCounterKey)).toBe(0); + // Counter is missing — simulates post-TTL state without waiting. + expect(await queue.redis.exists(runningCounterKey)).toBe(0); - // releaseAllConcurrency calls releaseConcurrencyTracked under the hood for CK queues. - await queue.releaseAllConcurrency(msg.orgId, msg.runId); + // releaseAllConcurrency calls releaseConcurrencyTracked under the hood for CK queues. + await queue.releaseAllConcurrency(msg.orgId, msg.runId); - // Pre-release truth was 2 in flight; after release one remains. Counter should be 1. - const counterVal = Number(await queue.redis.get(runningCounterKey)); - expect(counterVal).toBe(1); - } finally { - await queue.quit(); - } + // Pre-release truth was 2 in flight; after release one remains. Counter should be 1. + const counterVal = Number(await queue.redis.get(runningCounterKey)); + expect(counterVal).toBe(1); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "counterTtlSeconds option is honored on lazy-init", - async ({ redisContainer }) => { - // Build a queue with a short TTL and verify the counter is SET with it. - const queue = new RunQueue({ - ...testOptions, - counterTtlSeconds: 60, - queueSelectionStrategy: new FairQueueSelectionStrategy({ - redis: { - keyPrefix: "runqueue:test:", - host: redisContainer.getHost(), - port: redisContainer.getPort(), - }, - keys: testOptions.keys, - }), + redisTest("counterTtlSeconds option is honored on lazy-init", async ({ redisContainer }) => { + // Build a queue with a short TTL and verify the counter is SET with it. + const queue = new RunQueue({ + ...testOptions, + counterTtlSeconds: 60, + queueSelectionStrategy: new FairQueueSelectionStrategy({ redis: { keyPrefix: "runqueue:test:", host: redisContainer.getHost(), port: redisContainer.getPort(), }, + keys: testOptions.keys, + }), + redis: { + keyPrefix: "runqueue:test:", + host: redisContainer.getHost(), + port: redisContainer.getPort(), + }, + }); + try { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - try { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: makeMessage({ runId: "r1", concurrencyKey: "ck-a" }), - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - const counterKey = testOptions.keys.queueLengthCounterKey( - authenticatedEnvDev, - "task/my-task" - ); - const ttl = await queue.redis.ttl(counterKey); - // Generous lower bound — CI workers can occasionally stall multiple - // seconds between the lazy-init SET and the TTL read. - expect(ttl).toBeGreaterThanOrEqual(50); - expect(ttl).toBeLessThanOrEqual(60); - } finally { - await queue.quit(); - } + const counterKey = testOptions.keys.queueLengthCounterKey( + authenticatedEnvDev, + "task/my-task" + ); + const ttl = await queue.redis.ttl(counterKey); + // Generous lower bound — CI workers can occasionally stall multiple + // seconds between the lazy-init SET and the TTL read. + expect(ttl).toBeGreaterThanOrEqual(50); + expect(ttl).toBeLessThanOrEqual(60); + } finally { + await queue.quit(); } - ); + }); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts b/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts index 3945b455a07..5d468bf0eb1 100644 --- a/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/ckIndex.test.ts @@ -97,12 +97,7 @@ describe("CK Index", () => { const masterQueueKey = testOptions.keys.masterQueueKeyForShard( testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) ); - const masterMembers = await queue.redis.zrange( - masterQueueKey, - 0, - -1, - "WITHSCORES" - ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1, "WITHSCORES"); // Should have exactly one member ending with :ck:* const ckWildcardMembers = masterMembers.filter( (m, i) => i % 2 === 0 && m.endsWith(":ck:*") @@ -127,264 +122,232 @@ describe("CK Index", () => { } ); - redisTest( - "multiple CKs result in single master queue entry", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now(); - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 100, - }); - const msg3 = makeMessage({ - runId: "r3", - concurrencyKey: "ck-c", - timestamp: now + 200, + redisTest("multiple CKs result in single master queue entry", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now(); + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 100, + }); + const msg3 = makeMessage({ + runId: "r3", + concurrencyKey: "ck-c", + timestamp: now + 200, + }); + + for (const msg of [msg1, msg2, msg3]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msg1, msg2, msg3]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Master queue should have exactly ONE entry (the :ck:* wildcard) - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2) - ); - const masterMembers = await queue.redis.zrange( - masterQueueKey, - 0, - -1 - ); - // Filter to only members for our queue - const ourMembers = masterMembers.filter((m) => - m.includes("queue:task/my-task") - ); - expect(ourMembers.length).toBe(1); - expect(ourMembers[0]).toContain(":ck:*"); - - // CK index should have 3 entries - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) - ); - const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(3); - } finally { - await queue.quit(); } - } - ); - - redisTest( - "dequeue from CK queue distributes across sub-queues", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; // In the past so they're ready - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 1, - }); - const msg3 = makeMessage({ - runId: "r3", - concurrencyKey: "ck-a", - timestamp: now + 2, - }); - - for (const msg of [msg1, msg2, msg3]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - // Dequeue via the master queue consumer - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); - const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); - - // Should dequeue messages from both CK sub-queues - expect(messages).toBeDefined(); - // We should get at least 2 messages (one from each CK) - // The exact order depends on CK index scoring - expect(messages!.length).toBeGreaterThanOrEqual(2); - - const dequeuedRunIds = messages!.map((m: any) => m.messageId); - // r1 (ck-a, oldest) and r2 (ck-b) should be dequeued - expect(dequeuedRunIds).toContain("r1"); - expect(dequeuedRunIds).toContain("r2"); - } finally { - await queue.quit(); - } + // Master queue should have exactly ONE entry (the :ck:* wildcard) + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2) + ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + // Filter to only members for our queue + const ourMembers = masterMembers.filter((m) => m.includes("queue:task/my-task")); + expect(ourMembers.length).toBe(1); + expect(ourMembers[0]).toContain(":ck:*"); + + // CK index should have 3 entries + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) + ); + const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(3); + } finally { + await queue.quit(); } - ); + }); - redisTest( - "empty CK sub-queue is removed from CK index", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg1 = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - const msg2 = makeMessage({ - runId: "r2", - concurrencyKey: "ck-b", - timestamp: now + 1, + redisTest("dequeue from CK queue distributes across sub-queues", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; // In the past so they're ready + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 1, + }); + const msg3 = makeMessage({ + runId: "r3", + concurrencyKey: "ck-a", + timestamp: now + 2, + }); + + for (const msg of [msg1, msg2, msg3]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msg1, msg2]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // CK index should have 2 entries initially - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) - ); - let ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(2); - - // Dequeue both messages - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); - await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); - - // CK index should be empty (both sub-queues drained) - ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(0); - } finally { - await queue.quit(); } - } - ); - redisTest( - "empty CK index removes :ck:* from master queue", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); + // Dequeue via the master queue consumer + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); + const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); + + // Should dequeue messages from both CK sub-queues + expect(messages).toBeDefined(); + // We should get at least 2 messages (one from each CK) + // The exact order depends on CK index scoring + expect(messages!.length).toBeGreaterThanOrEqual(2); + + const dequeuedRunIds = messages!.map((m: any) => m.messageId); + // r1 (ck-a, oldest) and r2 (ck-b) should be dequeued + expect(dequeuedRunIds).toContain("r1"); + expect(dequeuedRunIds).toContain("r2"); + } finally { + await queue.quit(); + } + }); + redisTest("empty CK sub-queue is removed from CK index", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg1 = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + const msg2 = makeMessage({ + runId: "r2", + concurrencyKey: "ck-b", + timestamp: now + 1, + }); + + for (const msg of [msg1, msg2]) { await queue.enqueueMessage({ env: authenticatedEnvDev, message: msg, workerQueue: authenticatedEnvDev.id, skipDequeueProcessing: true, }); - - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) - ); - - // Master queue should have :ck:* entry - let masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(1); - - // Dequeue the message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg.environmentId, - 2 - ); - await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 10); - - // Master queue should be empty - masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(0); - } finally { - await queue.quit(); } - } - ); - redisTest( - "mixed CK and non-CK queues in same shard", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; + // CK index should have 2 entries initially + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg1.queue, msg1.concurrencyKey) + ); + let ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(2); + + // Dequeue both messages + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 10); + + // CK index should be empty (both sub-queues drained) + ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(0); + } finally { + await queue.quit(); + } + }); - // Non-CK message - const msgNoCk = makeMessage({ - runId: "r-no-ck", - timestamp: now, - }); + redisTest("empty CK index removes :ck:* from master queue", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2) + ); + + // Master queue should have :ck:* entry + let masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(1); + + // Dequeue the message + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 10); + + // Master queue should be empty + masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(0); + } finally { + await queue.quit(); + } + }); - // CK messages - const msgCk1 = makeMessage({ - runId: "r-ck-1", - concurrencyKey: "ck-a", - timestamp: now + 1, - }); - const msgCk2 = makeMessage({ - runId: "r-ck-2", - concurrencyKey: "ck-b", - timestamp: now + 2, + redisTest("mixed CK and non-CK queues in same shard", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + + // Non-CK message + const msgNoCk = makeMessage({ + runId: "r-no-ck", + timestamp: now, + }); + + // CK messages + const msgCk1 = makeMessage({ + runId: "r-ck-1", + concurrencyKey: "ck-a", + timestamp: now + 1, + }); + const msgCk2 = makeMessage({ + runId: "r-ck-2", + concurrencyKey: "ck-b", + timestamp: now + 2, + }); + + for (const msg of [msgNoCk, msgCk1, msgCk2]) { + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, }); - - for (const msg of [msgNoCk, msgCk1, msgCk2]) { - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - } - - // Master queue should have 2 entries: one non-CK queue and one :ck:* - const masterQueueKey = testOptions.keys.masterQueueKeyForShard( - testOptions.keys.masterQueueShardForEnvironment(msgNoCk.environmentId, 2) - ); - const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(2); - - // One should be the non-CK queue, one should be :ck:* - const ckWildcard = masterMembers.filter((m) => m.endsWith(":ck:*")); - const nonCk = masterMembers.filter( - (m) => !m.includes(":ck:") - ); - expect(ckWildcard.length).toBe(1); - expect(nonCk.length).toBe(1); - } finally { - await queue.quit(); } + + // Master queue should have 2 entries: one non-CK queue and one :ck:* + const masterQueueKey = testOptions.keys.masterQueueKeyForShard( + testOptions.keys.masterQueueShardForEnvironment(msgNoCk.environmentId, 2) + ); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(2); + + // One should be the non-CK queue, one should be :ck:* + const ckWildcard = masterMembers.filter((m) => m.endsWith(":ck:*")); + const nonCk = masterMembers.filter((m) => !m.includes(":ck:")); + expect(ckWildcard.length).toBe(1); + expect(nonCk.length).toBe(1); + } finally { + await queue.quit(); } - ); + }); redisTest( "acknowledge CK message rebalances CK index and master queue", @@ -413,10 +376,7 @@ describe("CK Index", () => { } // Dequeue one message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg1.environmentId, - 2 - ); + const shard = testOptions.keys.masterQueueShardForEnvironment(msg1.environmentId, 2); const messages = await queue.testDequeueFromMasterQueue(shard, msg1.environmentId, 1); expect(messages!.length).toBe(1); expect(messages![0].messageId).toBe("r1"); @@ -459,63 +419,57 @@ describe("CK Index", () => { } ); - redisTest( - "nack CK message rebalances CK index", - async ({ redisContainer }) => { - const queue = createQueue(redisContainer); - try { - const now = Date.now() - 1000; - const msg = makeMessage({ - runId: "r1", - concurrencyKey: "ck-a", - timestamp: now, - }); - - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: msg, - workerQueue: authenticatedEnvDev.id, - skipDequeueProcessing: true, - }); - - // Dequeue the message - const shard = testOptions.keys.masterQueueShardForEnvironment( - msg.environmentId, - 2 - ); - const messages = await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); - expect(messages!.length).toBe(1); - - // Nack the message (re-enqueue) - await queue.nackMessage({ - orgId: msg.orgId, - messageId: "r1", - retryAt: Date.now() + 5000, - incrementAttemptCount: false, - skipDequeueProcessing: true, - }); - - // CK index should have the ck-a entry (message re-enqueued) - const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( - testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, msg.concurrencyKey) - ); - const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); - expect(ckIndexMembers.length).toBe(1); - - // Master queue should have the :ck:* entry - const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); - const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); - expect(masterMembers.length).toBe(1); - expect(masterMembers[0]).toContain(":ck:*"); - - // No old-format entries - const oldFormatMembers = masterMembers.filter( - (m) => m.includes(":ck:") && !m.endsWith(":ck:*") - ); - expect(oldFormatMembers.length).toBe(0); - } finally { - await queue.quit(); - } + redisTest("nack CK message rebalances CK index", async ({ redisContainer }) => { + const queue = createQueue(redisContainer); + try { + const now = Date.now() - 1000; + const msg = makeMessage({ + runId: "r1", + concurrencyKey: "ck-a", + timestamp: now, + }); + + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: msg, + workerQueue: authenticatedEnvDev.id, + skipDequeueProcessing: true, + }); + + // Dequeue the message + const shard = testOptions.keys.masterQueueShardForEnvironment(msg.environmentId, 2); + const messages = await queue.testDequeueFromMasterQueue(shard, msg.environmentId, 1); + expect(messages!.length).toBe(1); + + // Nack the message (re-enqueue) + await queue.nackMessage({ + orgId: msg.orgId, + messageId: "r1", + retryAt: Date.now() + 5000, + incrementAttemptCount: false, + skipDequeueProcessing: true, + }); + + // CK index should have the ck-a entry (message re-enqueued) + const ckIndexKey = testOptions.keys.ckIndexKeyFromQueue( + testOptions.keys.queueKey(authenticatedEnvDev, msg.queue, msg.concurrencyKey) + ); + const ckIndexMembers = await queue.redis.zrange(ckIndexKey, 0, -1); + expect(ckIndexMembers.length).toBe(1); + + // Master queue should have the :ck:* entry + const masterQueueKey = testOptions.keys.masterQueueKeyForShard(shard); + const masterMembers = await queue.redis.zrange(masterQueueKey, 0, -1); + expect(masterMembers.length).toBe(1); + expect(masterMembers[0]).toContain(":ck:*"); + + // No old-format entries + const oldFormatMembers = masterMembers.filter( + (m) => m.includes(":ck:") && !m.endsWith(":ck:*") + ); + expect(oldFormatMembers.length).toBe(0); + } finally { + await queue.quit(); } - ); + }); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts b/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts index cc7485483aa..9138215bbe9 100644 --- a/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/dequeueMessageFromWorkerQueue.test.ts @@ -272,9 +272,8 @@ describe("RunQueue.dequeueMessageFromWorkerQueue", () => { const dequeued3 = await queue.dequeueMessageFromWorkerQueue("test_12345", "main"); expect(dequeued3).toBeUndefined(); - const envConcurrencyAfter = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envConcurrencyAfter = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envConcurrencyAfter).toBe(2); } finally { await queue.quit(); diff --git a/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts b/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts index 8ce2d68d5de..e45c99b25c6 100644 --- a/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.ts @@ -48,7 +48,10 @@ const messageDev: InputPayload = { vi.setConfig({ testTimeout: 60_000 }); -function createQueue(redisContainer: { getHost: () => string; getPort: () => number }, prefix = "runqueue:test:") { +function createQueue( + redisContainer: { getHost: () => string; getPort: () => number }, + prefix = "runqueue:test:" +) { return new RunQueue({ ...testOptions, queueSelectionStrategy: new FairQueueSelectionStrategy({ @@ -133,45 +136,48 @@ describe("RunQueue.enqueueMessage", () => { }); describe("RunQueue.enqueueMessage fast path", () => { - redisTest("should fast-path to worker queue when queue is empty and concurrency available", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp1:"); - - try { - // Set concurrency limits - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue with fast path enabled - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: messageDev, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // Queue sorted set should be empty (fast path skips it) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(0); - - // Queue concurrency should be claimed (operational concurrency) - const queueConcurrency = await queue.currentConcurrencyOfQueue( - authenticatedEnvDev, - messageDev.queue - ); - expect(queueConcurrency).toBe(1); - - // Message should be directly in worker queue - dequeue it - const dequeued = await queue.dequeueMessageFromWorkerQueue( - "test_12345", - authenticatedEnvDev.id, - { blockingPop: false } - ); - assertNonNullable(dequeued); - expect(dequeued.messageId).toEqual(messageDev.runId); - expect(dequeued.message.version).toEqual("2"); - } finally { - await queue.quit(); + redisTest( + "should fast-path to worker queue when queue is empty and concurrency available", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp1:"); + + try { + // Set concurrency limits + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue with fast path enabled + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: messageDev, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // Queue sorted set should be empty (fast path skips it) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(0); + + // Queue concurrency should be claimed (operational concurrency) + const queueConcurrency = await queue.currentConcurrencyOfQueue( + authenticatedEnvDev, + messageDev.queue + ); + expect(queueConcurrency).toBe(1); + + // Message should be directly in worker queue - dequeue it + const dequeued = await queue.dequeueMessageFromWorkerQueue( + "test_12345", + authenticatedEnvDev.id, + { blockingPop: false } + ); + assertNonNullable(dequeued); + expect(dequeued.messageId).toEqual(messageDev.runId); + expect(dequeued.message.version).toEqual("2"); + } finally { + await queue.quit(); + } } - }); + ); redisTest("should take slow path when enableFastPath is false", async ({ redisContainer }) => { const queue = createQueue(redisContainer, "runqueue:fp2:"); @@ -201,105 +207,111 @@ describe("RunQueue.enqueueMessage fast path", () => { } }); - redisTest("should take slow path when queue has available messages", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp3:"); - - try { - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue a first message (slow path to populate the queue) - const message1: InputPayload = { - ...messageDev, - runId: "r1111", - timestamp: Date.now() - 1000, // in the past, so it's "available" - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: message1, - workerQueue: authenticatedEnvDev.id, - enableFastPath: false, - }); - - // Now enqueue a second message with fast path - const message2: InputPayload = { - ...messageDev, - runId: "r2222", - timestamp: Date.now(), - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: message2, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // Both messages should be in the queue sorted set (slow path for both) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(2); - } finally { - await queue.quit(); + redisTest( + "should take slow path when queue has available messages", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp3:"); + + try { + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue a first message (slow path to populate the queue) + const message1: InputPayload = { + ...messageDev, + runId: "r1111", + timestamp: Date.now() - 1000, // in the past, so it's "available" + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: message1, + workerQueue: authenticatedEnvDev.id, + enableFastPath: false, + }); + + // Now enqueue a second message with fast path + const message2: InputPayload = { + ...messageDev, + runId: "r2222", + timestamp: Date.now(), + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: message2, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // Both messages should be in the queue sorted set (slow path for both) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(2); + } finally { + await queue.quit(); + } } - }); - - redisTest("should fast-path when queue only has future-scored messages", async ({ redisContainer }) => { - const queue = createQueue(redisContainer, "runqueue:fp4:"); - - try { - await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); - - // Enqueue a message with a future timestamp (simulating a nacked retry) - const futureMessage: InputPayload = { - ...messageDev, - runId: "r_future", - timestamp: Date.now() + 60_000, // 60 seconds in the future - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: futureMessage, - workerQueue: authenticatedEnvDev.id, - enableFastPath: false, - }); - - // Queue has 1 message but it's not available (future score) - const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength).toBe(1); - - // Now enqueue a new message with fast path - const newMessage: InputPayload = { - ...messageDev, - runId: "r_new", - timestamp: Date.now(), - }; - await queue.enqueueMessage({ - env: authenticatedEnvDev, - message: newMessage, - workerQueue: authenticatedEnvDev.id, - enableFastPath: true, - }); - - // The future message stays in queue, new message went to worker queue - const queueLength2 = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); - expect(queueLength2).toBe(1); // Only the future message - - // Queue concurrency claimed for the fast-pathed message - const queueConcurrency = await queue.currentConcurrencyOfQueue( - authenticatedEnvDev, - messageDev.queue - ); - expect(queueConcurrency).toBe(1); - - // Can dequeue the fast-pathed message from worker queue - const dequeued = await queue.dequeueMessageFromWorkerQueue( - "test_12345", - authenticatedEnvDev.id, - { blockingPop: false } - ); - assertNonNullable(dequeued); - expect(dequeued.messageId).toEqual("r_new"); - } finally { - await queue.quit(); + ); + + redisTest( + "should fast-path when queue only has future-scored messages", + async ({ redisContainer }) => { + const queue = createQueue(redisContainer, "runqueue:fp4:"); + + try { + await queue.updateEnvConcurrencyLimits(authenticatedEnvDev); + + // Enqueue a message with a future timestamp (simulating a nacked retry) + const futureMessage: InputPayload = { + ...messageDev, + runId: "r_future", + timestamp: Date.now() + 60_000, // 60 seconds in the future + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: futureMessage, + workerQueue: authenticatedEnvDev.id, + enableFastPath: false, + }); + + // Queue has 1 message but it's not available (future score) + const queueLength = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength).toBe(1); + + // Now enqueue a new message with fast path + const newMessage: InputPayload = { + ...messageDev, + runId: "r_new", + timestamp: Date.now(), + }; + await queue.enqueueMessage({ + env: authenticatedEnvDev, + message: newMessage, + workerQueue: authenticatedEnvDev.id, + enableFastPath: true, + }); + + // The future message stays in queue, new message went to worker queue + const queueLength2 = await queue.lengthOfQueue(authenticatedEnvDev, messageDev.queue); + expect(queueLength2).toBe(1); // Only the future message + + // Queue concurrency claimed for the fast-pathed message + const queueConcurrency = await queue.currentConcurrencyOfQueue( + authenticatedEnvDev, + messageDev.queue + ); + expect(queueConcurrency).toBe(1); + + // Can dequeue the fast-pathed message from worker queue + const dequeued = await queue.dequeueMessageFromWorkerQueue( + "test_12345", + authenticatedEnvDev.id, + { blockingPop: false } + ); + assertNonNullable(dequeued); + expect(dequeued.messageId).toEqual("r_new"); + } finally { + await queue.quit(); + } } - }); + ); redisTest("should take slow path when env concurrency is full", async ({ redisContainer }) => { // Use a low concurrency limit diff --git a/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts b/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts index 0f65b020885..3e31085d678 100644 --- a/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/keyProducer.test.ts @@ -417,8 +417,12 @@ describe("KeyProducer", () => { it("isCkWildcard", () => { const keyProducer = new RunQueueFullKeyProducer(); - expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:*")).toBe(true); - expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:bar")).toBe(false); + expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:*")).toBe( + true + ); + expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo:ck:bar")).toBe( + false + ); expect(keyProducer.isCkWildcard("{org:o1234}:proj:p1234:env:e1234:queue:task/foo")).toBe(false); }); diff --git a/internal-packages/run-engine/src/run-queue/tests/nack.test.ts b/internal-packages/run-engine/src/run-queue/tests/nack.test.ts index 8bfd8b3a76f..a1cf2740176 100644 --- a/internal-packages/run-engine/src/run-queue/tests/nack.test.ts +++ b/internal-packages/run-engine/src/run-queue/tests/nack.test.ts @@ -89,9 +89,8 @@ describe("RunQueue.nackMessage", () => { ); expect(queueCurrentConcurrency).toBe(1); - const envCurrentConcurrency = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envCurrentConcurrency = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envCurrentConcurrency).toBe(1); // Nack the message @@ -107,9 +106,8 @@ describe("RunQueue.nackMessage", () => { ); expect(queueCurrentConcurrencyAfterNack).toBe(0); - const envCurrentConcurrencyAfterNack = await queue.currentConcurrencyOfEnvironment( - authenticatedEnvDev - ); + const envCurrentConcurrencyAfterNack = + await queue.currentConcurrencyOfEnvironment(authenticatedEnvDev); expect(envCurrentConcurrencyAfterNack).toBe(0); const envQueueLength = await queue.lengthOfEnvQueue(authenticatedEnvDev); @@ -196,9 +194,8 @@ describe("RunQueue.nackMessage", () => { const envQueueLengthDequeue = await queue.lengthOfEnvQueue(authenticatedEnvDev); expect(envQueueLengthDequeue).toBe(0); - const deadLetterQueueLengthBefore = await queue.lengthOfDeadLetterQueue( - authenticatedEnvDev - ); + const deadLetterQueueLengthBefore = + await queue.lengthOfDeadLetterQueue(authenticatedEnvDev); expect(deadLetterQueueLengthBefore).toBe(0); await queue.nackMessage({ @@ -209,9 +206,8 @@ describe("RunQueue.nackMessage", () => { const envQueueLengthAfterNack = await queue.lengthOfEnvQueue(authenticatedEnvDev); expect(envQueueLengthAfterNack).toBe(0); - const deadLetterQueueLengthAfterNack = await queue.lengthOfDeadLetterQueue( - authenticatedEnvDev - ); + const deadLetterQueueLengthAfterNack = + await queue.lengthOfDeadLetterQueue(authenticatedEnvDev); expect(deadLetterQueueLengthAfterNack).toBe(1); } finally { await queue.quit(); diff --git a/internal-packages/run-store/src/NoopRunStore.ts b/internal-packages/run-store/src/NoopRunStore.ts index e27080c9af6..067aa3de096 100644 --- a/internal-packages/run-store/src/NoopRunStore.ts +++ b/internal-packages/run-store/src/NoopRunStore.ts @@ -5,31 +5,85 @@ export class NoopRunStore implements RunStore { private fail(method: string): never { throw new Error(`NoopRunStore.${method} called`); } - createRun(): never { return this.fail("createRun"); } - createCancelledRun(): never { return this.fail("createCancelledRun"); } - createFailedRun(): never { return this.fail("createFailedRun"); } - startAttempt(): never { return this.fail("startAttempt"); } - completeAttemptSuccess(): never { return this.fail("completeAttemptSuccess"); } - recordRetryOutcome(): never { return this.fail("recordRetryOutcome"); } - requeueRun(): never { return this.fail("requeueRun"); } - recordBulkActionMembership(): never { return this.fail("recordBulkActionMembership"); } - cancelRun(): never { return this.fail("cancelRun"); } - failRunPermanently(): never { return this.fail("failRunPermanently"); } - expireRun(): never { return this.fail("expireRun"); } - expireRunsBatch(): never { return this.fail("expireRunsBatch"); } - lockRunToWorker(): never { return this.fail("lockRunToWorker"); } - parkPendingVersion(): never { return this.fail("parkPendingVersion"); } - promotePendingVersionRuns(): never { return this.fail("promotePendingVersionRuns"); } - suspendForCheckpoint(): never { return this.fail("suspendForCheckpoint"); } - resumeFromCheckpoint(): never { return this.fail("resumeFromCheckpoint"); } - rescheduleRun(): never { return this.fail("rescheduleRun"); } - enqueueDelayedRun(): never { return this.fail("enqueueDelayedRun"); } - rewriteDebouncedRun(): never { return this.fail("rewriteDebouncedRun"); } - updateMetadata(): never { return this.fail("updateMetadata"); } - clearIdempotencyKey(): never { return this.fail("clearIdempotencyKey"); } - pushTags(): never { return this.fail("pushTags"); } - pushRealtimeStream(): never { return this.fail("pushRealtimeStream"); } - findRun(): never { return this.fail("findRun"); } - findRunOrThrow(): never { return this.fail("findRunOrThrow"); } - findRuns(): never { return this.fail("findRuns"); } + createRun(): never { + return this.fail("createRun"); + } + createCancelledRun(): never { + return this.fail("createCancelledRun"); + } + createFailedRun(): never { + return this.fail("createFailedRun"); + } + startAttempt(): never { + return this.fail("startAttempt"); + } + completeAttemptSuccess(): never { + return this.fail("completeAttemptSuccess"); + } + recordRetryOutcome(): never { + return this.fail("recordRetryOutcome"); + } + requeueRun(): never { + return this.fail("requeueRun"); + } + recordBulkActionMembership(): never { + return this.fail("recordBulkActionMembership"); + } + cancelRun(): never { + return this.fail("cancelRun"); + } + failRunPermanently(): never { + return this.fail("failRunPermanently"); + } + expireRun(): never { + return this.fail("expireRun"); + } + expireRunsBatch(): never { + return this.fail("expireRunsBatch"); + } + lockRunToWorker(): never { + return this.fail("lockRunToWorker"); + } + parkPendingVersion(): never { + return this.fail("parkPendingVersion"); + } + promotePendingVersionRuns(): never { + return this.fail("promotePendingVersionRuns"); + } + suspendForCheckpoint(): never { + return this.fail("suspendForCheckpoint"); + } + resumeFromCheckpoint(): never { + return this.fail("resumeFromCheckpoint"); + } + rescheduleRun(): never { + return this.fail("rescheduleRun"); + } + enqueueDelayedRun(): never { + return this.fail("enqueueDelayedRun"); + } + rewriteDebouncedRun(): never { + return this.fail("rewriteDebouncedRun"); + } + updateMetadata(): never { + return this.fail("updateMetadata"); + } + clearIdempotencyKey(): never { + return this.fail("clearIdempotencyKey"); + } + pushTags(): never { + return this.fail("pushTags"); + } + pushRealtimeStream(): never { + return this.fail("pushRealtimeStream"); + } + findRun(): never { + return this.fail("findRun"); + } + findRunOrThrow(): never { + return this.fail("findRunOrThrow"); + } + findRuns(): never { + return this.fail("findRuns"); + } } diff --git a/internal-packages/run-store/src/PostgresRunStore.test.ts b/internal-packages/run-store/src/PostgresRunStore.test.ts index 47876b70c8d..49fcbfe4503 100644 --- a/internal-packages/run-store/src/PostgresRunStore.test.ts +++ b/internal-packages/run-store/src/PostgresRunStore.test.ts @@ -77,38 +77,41 @@ function buildCreateRunInput(params: { } describe("PostgresRunStore", () => { - postgresTest("createRun creates the run with one snapshot and no waitpoint", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "createRun creates the run with one snapshot and no waitpoint", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ - prisma, - // The read-only client just needs to be a PrismaClient for these tests. - readOnlyPrisma: prisma, - }); + const store = new PostgresRunStore({ + prisma, + // The read-only client just needs to be a PrismaClient for these tests. + readOnlyPrisma: prisma, + }); - const runId = "run_test_1"; + const runId = "run_test_1"; - const run = await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + const run = await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("PENDING"); - expect(run.associatedWaitpoint).toBeNull(); + expect(run.id).toBe(runId); + expect(run.status).toBe("PENDING"); + expect(run.associatedWaitpoint).toBeNull(); - const snapshots = await prisma.taskRunExecutionSnapshot.findMany({ - where: { runId }, - }); + const snapshots = await prisma.taskRunExecutionSnapshot.findMany({ + where: { runId }, + }); - expect(snapshots).toHaveLength(1); - expect(snapshots[0]?.executionStatus).toBe("RUN_CREATED"); - expect(snapshots[0]?.runStatus).toBe("PENDING"); - }); + expect(snapshots).toHaveLength(1); + expect(snapshots[0]?.executionStatus).toBe("RUN_CREATED"); + expect(snapshots[0]?.runStatus).toBe("PENDING"); + } + ); postgresTest( "createCancelledRun creates a CANCELED run with one FINISHED/CANCELED execution snapshot", @@ -233,35 +236,46 @@ describe("PostgresRunStore", () => { } ); - postgresTest("startAttempt sets status to EXECUTING and records attempt fields", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "startAttempt sets status to EXECUTING and records attempt fields", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_start_attempt_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_start_attempt_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const executedAt = new Date("2026-03-01T10:00:00.000Z"); + const executedAt = new Date("2026-03-01T10:00:00.000Z"); - const run = await store.startAttempt( - runId, - { attemptNumber: 1, executedAt, isWarmStart: true }, - { select: { id: true, status: true, attemptNumber: true, executedAt: true, isWarmStart: true } } - ); + const run = await store.startAttempt( + runId, + { attemptNumber: 1, executedAt, isWarmStart: true }, + { + select: { + id: true, + status: true, + attemptNumber: true, + executedAt: true, + isWarmStart: true, + }, + } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("EXECUTING"); - expect(run.attemptNumber).toBe(1); - expect(run.executedAt).toEqual(executedAt); - expect(run.isWarmStart).toBe(true); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("EXECUTING"); + expect(run.attemptNumber).toBe(1); + expect(run.executedAt).toEqual(executedAt); + expect(run.isWarmStart).toBe(true); + } + ); postgresTest( "completeAttemptSuccess sets status to COMPLETED_SUCCESSFULLY and creates a FINISHED snapshot", @@ -330,36 +344,43 @@ describe("PostgresRunStore", () => { } ); - postgresTest("recordRetryOutcome updates machine/usage/cost but leaves status unchanged", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "recordRetryOutcome updates machine/usage/cost but leaves status unchanged", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_retry_outcome_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_retry_outcome_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - // Set status to EXECUTING first so we know what to verify against - await store.startAttempt(runId, { attemptNumber: 1, isWarmStart: false }, { select: { id: true } }); + // Set status to EXECUTING first so we know what to verify against + await store.startAttempt( + runId, + { attemptNumber: 1, isWarmStart: false }, + { select: { id: true } } + ); - const run = await store.recordRetryOutcome( - runId, - { machinePreset: "large-1x", usageDurationMs: 200, costInCents: 5 }, - { include: { runtimeEnvironment: true } } - ); + const run = await store.recordRetryOutcome( + runId, + { machinePreset: "large-1x", usageDurationMs: 200, costInCents: 5 }, + { include: { runtimeEnvironment: true } } + ); - // Status must be unchanged (EXECUTING — not PENDING, not CANCELED) - expect(run.status).toBe("EXECUTING"); - expect(run.machinePreset).toBe("large-1x"); - expect(run.usageDurationMs).toBe(200); - expect(run.costInCents).toBe(5); - }); + // Status must be unchanged (EXECUTING — not PENDING, not CANCELED) + expect(run.status).toBe("EXECUTING"); + expect(run.machinePreset).toBe("large-1x"); + expect(run.usageDurationMs).toBe(200); + expect(run.costInCents).toBe(5); + } + ); postgresTest("requeueRun sets status to PENDING", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); @@ -376,7 +397,11 @@ describe("PostgresRunStore", () => { }) ); - await store.startAttempt(runId, { attemptNumber: 1, isWarmStart: false }, { select: { id: true } }); + await store.startAttempt( + runId, + { attemptNumber: 1, isWarmStart: false }, + { select: { id: true } } + ); const run = await store.requeueRun(runId, { select: { id: true, status: true } }); @@ -384,48 +409,51 @@ describe("PostgresRunStore", () => { expect(run.status).toBe("PENDING"); }); - postgresTest("recordBulkActionMembership appends bulkActionId to existing array", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "recordBulkActionMembership appends bulkActionId to existing array", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_bulk_action_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_bulk_action_1"; - // Seed a run with an existing bulk action id - await prisma.taskRun.create({ - data: { - id: runId, - engine: "V2", - status: "CANCELED", - friendlyId: "run_bulk_action_friendly_1", - runtimeEnvironmentId: environment.id, - environmentType: "DEVELOPMENT", - organizationId: organization.id, - projectId: project.id, - taskIdentifier: "my-task", - payload: "{}", - payloadType: "application/json", - traceContext: {}, - traceId: "trace_b1", - spanId: "span_b1", - queue: "task/my-task", - isTest: false, - taskEventStore: "taskEvent", - depth: 0, - bulkActionGroupIds: ["existing-bulk-id"], - }, - }); + // Seed a run with an existing bulk action id + await prisma.taskRun.create({ + data: { + id: runId, + engine: "V2", + status: "CANCELED", + friendlyId: "run_bulk_action_friendly_1", + runtimeEnvironmentId: environment.id, + environmentType: "DEVELOPMENT", + organizationId: organization.id, + projectId: project.id, + taskIdentifier: "my-task", + payload: "{}", + payloadType: "application/json", + traceContext: {}, + traceId: "trace_b1", + spanId: "span_b1", + queue: "task/my-task", + isTest: false, + taskEventStore: "taskEvent", + depth: 0, + bulkActionGroupIds: ["existing-bulk-id"], + }, + }); - await store.recordBulkActionMembership(runId, "new-bulk-id"); + await store.recordBulkActionMembership(runId, "new-bulk-id"); - const updated = await prisma.taskRun.findUnique({ - where: { id: runId }, - select: { bulkActionGroupIds: true }, - }); + const updated = await prisma.taskRun.findUnique({ + where: { id: runId }, + select: { bulkActionGroupIds: true }, + }); - expect(updated?.bulkActionGroupIds).toContain("existing-bulk-id"); - expect(updated?.bulkActionGroupIds).toContain("new-bulk-id"); - expect(updated?.bulkActionGroupIds).toHaveLength(2); - }); + expect(updated?.bulkActionGroupIds).toContain("existing-bulk-id"); + expect(updated?.bulkActionGroupIds).toContain("new-bulk-id"); + expect(updated?.bulkActionGroupIds).toHaveLength(2); + } + ); postgresTest( "cancelRun sets status to CANCELED; without bulkActionId/usage those fields are untouched", @@ -466,7 +494,16 @@ describe("PostgresRunStore", () => { const run = await store.cancelRun( runId, { completedAt: cancelledAt, error }, - { select: { id: true, status: true, completedAt: true, bulkActionGroupIds: true, usageDurationMs: true, costInCents: true } } + { + select: { + id: true, + status: true, + completedAt: true, + bulkActionGroupIds: true, + usageDurationMs: true, + costInCents: true, + }, + } ); expect(run.id).toBe(runId); @@ -502,8 +539,22 @@ describe("PostgresRunStore", () => { const run = await store.cancelRun( runId, - { completedAt: cancelledAt, error, bulkActionId: "bulk-abc", usageDurationMs: 300, costInCents: 7 }, - { select: { id: true, status: true, bulkActionGroupIds: true, usageDurationMs: true, costInCents: true } } + { + completedAt: cancelledAt, + error, + bulkActionId: "bulk-abc", + usageDurationMs: 300, + costInCents: 7, + }, + { + select: { + id: true, + status: true, + bulkActionGroupIds: true, + usageDurationMs: true, + costInCents: true, + }, + } ); expect(run.status).toBe("CANCELED"); @@ -513,44 +564,47 @@ describe("PostgresRunStore", () => { } ); - postgresTest("failRunPermanently sets the passed status with completedAt/error/usage/cost", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "failRunPermanently sets the passed status with completedAt/error/usage/cost", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_fail_permanently_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_fail_permanently_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const completedAt = new Date("2026-05-01T00:00:00.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "permanent failure" }; + const completedAt = new Date("2026-05-01T00:00:00.000Z"); + const error = { type: "STRING_ERROR" as const, raw: "permanent failure" }; - const run = await store.failRunPermanently( - runId, - { status: "SYSTEM_FAILURE", completedAt, error, usageDurationMs: 150, costInCents: 3 }, - { - select: { - id: true, - status: true, - completedAt: true, - usageDurationMs: true, - costInCents: true, - }, - } - ); + const run = await store.failRunPermanently( + runId, + { status: "SYSTEM_FAILURE", completedAt, error, usageDurationMs: 150, costInCents: 3 }, + { + select: { + id: true, + status: true, + completedAt: true, + usageDurationMs: true, + costInCents: true, + }, + } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("SYSTEM_FAILURE"); - expect(run.completedAt).toEqual(completedAt); - expect(run.usageDurationMs).toBe(150); - expect(run.costInCents).toBe(3); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("SYSTEM_FAILURE"); + expect(run.completedAt).toEqual(completedAt); + expect(run.usageDurationMs).toBe(150); + expect(run.costInCents).toBe(3); + } + ); postgresTest( "expireRun sets status to EXPIRED with distinct completedAt/expiredAt, error set, and one FINISHED/EXPIRED snapshot", @@ -571,7 +625,10 @@ describe("PostgresRunStore", () => { const completedAt = new Date("2026-06-01T10:00:00.000Z"); const expiredAt = new Date("2026-06-01T10:00:01.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "Run expired because the TTL was reached" }; + const error = { + type: "STRING_ERROR" as const, + raw: "Run expired because the TTL was reached", + }; const run = await store.expireRun( runId, @@ -651,7 +708,10 @@ describe("PostgresRunStore", () => { } const now = new Date("2026-06-01T12:00:00.000Z"); - const error = { type: "STRING_ERROR" as const, raw: "Run expired because the TTL was reached" }; + const error = { + type: "STRING_ERROR" as const, + raw: "Run expired because the TTL was reached", + }; const count = await store.expireRunsBatch([runId1, runId2], { error, now }); @@ -827,31 +887,34 @@ describe("PostgresRunStore", () => { } ); - postgresTest("parkPendingVersion sets status to PENDING_VERSION and stores statusReason", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "parkPendingVersion sets status to PENDING_VERSION and stores statusReason", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_park_1"; + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_park_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const run = await store.parkPendingVersion( - runId, - { statusReason: "No background worker found" }, - { select: { id: true, status: true, statusReason: true } } - ); + const run = await store.parkPendingVersion( + runId, + { statusReason: "No background worker found" }, + { select: { id: true, status: true, statusReason: true } } + ); - expect(run.id).toBe(runId); - expect(run.status).toBe("PENDING_VERSION"); - expect(run.statusReason).toBe("No background worker found"); - }); + expect(run.id).toBe(runId); + expect(run.status).toBe("PENDING_VERSION"); + expect(run.statusReason).toBe("No background worker found"); + } + ); postgresTest( "promotePendingVersionRuns flips PENDING_VERSION to PENDING and returns count 1; run in another status returns count 0 and is unchanged", @@ -889,7 +952,10 @@ describe("PostgresRunStore", () => { expect(result.count).toBe(1); - const promoted = await prisma.taskRun.findUniqueOrThrow({ where: { id: pendingVersionId }, select: { status: true } }); + const promoted = await prisma.taskRun.findUniqueOrThrow({ + where: { id: pendingVersionId }, + select: { status: true }, + }); expect(promoted.status).toBe("PENDING"); // Seed a run NOT in PENDING_VERSION (e.g. EXECUTING) @@ -921,7 +987,10 @@ describe("PostgresRunStore", () => { expect(result2.count).toBe(0); - const unchanged = await prisma.taskRun.findUniqueOrThrow({ where: { id: executingId }, select: { status: true } }); + const unchanged = await prisma.taskRun.findUniqueOrThrow({ + where: { id: executingId }, + select: { status: true }, + }); expect(unchanged.status).toBe("EXECUTING"); } ); @@ -1585,7 +1654,9 @@ describe("PostgresRunStore — read", () => { const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - await expect(store.findRunOrThrow({ id: "missing" }, { select: { id: true } })).rejects.toThrow(); + await expect( + store.findRunOrThrow({ id: "missing" }, { select: { id: true } }) + ).rejects.toThrow(); }); postgresTest("findRun with include hydrates the relation", async ({ prisma }) => { @@ -1610,79 +1681,89 @@ describe("PostgresRunStore — read", () => { expect(run?.runtimeEnvironment.id).toBe(environment.id); }); - postgresTest("findRuns applies where/orderBy/take and returns ordered, limited rows", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "findRuns applies where/orderBy/take and returns ordered, limited rows", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const earliest = new Date("2026-06-01T00:00:00.000Z"); - const middle = new Date("2026-06-02T00:00:00.000Z"); - const latest = new Date("2026-06-03T00:00:00.000Z"); + const earliest = new Date("2026-06-01T00:00:00.000Z"); + const middle = new Date("2026-06-02T00:00:00.000Z"); + const latest = new Date("2026-06-03T00:00:00.000Z"); - const rows: Array<{ id: string; createdAt: Date }> = [ - { id: "run_find_many_earliest", createdAt: earliest }, - { id: "run_find_many_middle", createdAt: middle }, - { id: "run_find_many_latest", createdAt: latest }, - ]; + const rows: Array<{ id: string; createdAt: Date }> = [ + { id: "run_find_many_earliest", createdAt: earliest }, + { id: "run_find_many_middle", createdAt: middle }, + { id: "run_find_many_latest", createdAt: latest }, + ]; - for (const row of rows) { - await prisma.taskRun.create({ - data: { - id: row.id, - engine: "V2", - status: "PENDING", - friendlyId: `${row.id}_friendly`, - runtimeEnvironmentId: environment.id, - environmentType: "DEVELOPMENT", - organizationId: organization.id, - projectId: project.id, - taskIdentifier: "my-task", - payload: "{}", - payloadType: "application/json", - traceContext: {}, - traceId: `trace_${row.id}`, - spanId: `span_${row.id}`, - queue: "task/my-task", - isTest: false, - taskEventStore: "taskEvent", - depth: 0, - createdAt: row.createdAt, - }, - }); - } + for (const row of rows) { + await prisma.taskRun.create({ + data: { + id: row.id, + engine: "V2", + status: "PENDING", + friendlyId: `${row.id}_friendly`, + runtimeEnvironmentId: environment.id, + environmentType: "DEVELOPMENT", + organizationId: organization.id, + projectId: project.id, + taskIdentifier: "my-task", + payload: "{}", + payloadType: "application/json", + traceContext: {}, + traceId: `trace_${row.id}`, + spanId: `span_${row.id}`, + queue: "task/my-task", + isTest: false, + taskEventStore: "taskEvent", + depth: 0, + createdAt: row.createdAt, + }, + }); + } - const found = await store.findRuns({ - where: { projectId: project.id }, - select: { id: true }, - orderBy: { createdAt: "desc" }, - take: 2, - }); + const found = await store.findRuns({ + where: { projectId: project.id }, + select: { id: true }, + orderBy: { createdAt: "desc" }, + take: 2, + }); - expect(found).toEqual([{ id: "run_find_many_latest" }, { id: "run_find_many_middle" }]); - }); + expect(found).toEqual([{ id: "run_find_many_latest" }, { id: "run_find_many_middle" }]); + } + ); - postgresTest("findRun reads a just-written row when passed the writer client", async ({ prisma }) => { - const { organization, project, environment } = await seedEnvironment(prisma); + postgresTest( + "findRun reads a just-written row when passed the writer client", + async ({ prisma }) => { + const { organization, project, environment } = await seedEnvironment(prisma); - // Use a NoopRunStore-style read replica that must NOT be hit: pass the writer - // (prisma) explicitly so reads go through it for read-after-write consistency. - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - const runId = "run_find_read_after_write_1"; + // Use a NoopRunStore-style read replica that must NOT be hit: pass the writer + // (prisma) explicitly so reads go through it for read-after-write consistency. + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const runId = "run_find_read_after_write_1"; - await store.createRun( - buildCreateRunInput({ - runId, - organizationId: organization.id, - projectId: project.id, - runtimeEnvironmentId: environment.id, - }) - ); + await store.createRun( + buildCreateRunInput({ + runId, + organizationId: organization.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + }) + ); - const run = await store.findRun({ id: runId }, { select: { id: true, status: true } }, prisma); + const run = await store.findRun( + { id: runId }, + { select: { id: true, status: true } }, + prisma + ); - expect(run?.id).toBe(runId); - expect(run?.status).toBe("PENDING"); - }); + expect(run?.id).toBe(runId); + expect(run?.status).toBe("PENDING"); + } + ); postgresTest("findRun by id with no projection returns the whole row", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); @@ -1710,13 +1791,16 @@ describe("PostgresRunStore — read", () => { expect(run?.payloadType).toBe("application/json"); }); - postgresTest("findRunOrThrow with no projection throws when no row matches", async ({ prisma }) => { - await seedEnvironment(prisma); + postgresTest( + "findRunOrThrow with no projection throws when no row matches", + async ({ prisma }) => { + await seedEnvironment(prisma); - const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); + const store = new PostgresRunStore({ prisma, readOnlyPrisma: prisma }); - await expect(store.findRunOrThrow({ id: "missing" })).rejects.toThrow(); - }); + await expect(store.findRunOrThrow({ id: "missing" })).rejects.toThrow(); + } + ); postgresTest("findRuns with no projection returns whole rows", async ({ prisma }) => { const { organization, project, environment } = await seedEnvironment(prisma); diff --git a/internal-packages/run-store/src/PostgresRunStore.ts b/internal-packages/run-store/src/PostgresRunStore.ts index 2caa5ca85b4..79c85099d50 100644 --- a/internal-packages/run-store/src/PostgresRunStore.ts +++ b/internal-packages/run-store/src/PostgresRunStore.ts @@ -299,7 +299,12 @@ export class PostgresRunStore implements RunStore { async expireRun( runId: string, - data: { error: TaskRunError; completedAt: Date; expiredAt: Date; snapshot: ExpireSnapshotInput }, + data: { + error: TaskRunError; + completedAt: Date; + expiredAt: Date; + snapshot: ExpireSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise> { @@ -629,10 +634,7 @@ export class PostgresRunStore implements RunStore { args: { include: I }, client?: ReadClient ): Promise | null>; - findRun( - where: Prisma.TaskRunWhereInput, - client?: ReadClient - ): Promise; + findRun(where: Prisma.TaskRunWhereInput, client?: ReadClient): Promise; async findRun( where: Prisma.TaskRunWhereInput, argsOrClient?: { select?: Prisma.TaskRunSelect; include?: Prisma.TaskRunInclude } | ReadClient, @@ -656,10 +658,7 @@ export class PostgresRunStore implements RunStore { args: { include: I }, client?: ReadClient ): Promise>; - findRunOrThrow( - where: Prisma.TaskRunWhereInput, - client?: ReadClient - ): Promise; + findRunOrThrow(where: Prisma.TaskRunWhereInput, client?: ReadClient): Promise; async findRunOrThrow( where: Prisma.TaskRunWhereInput, argsOrClient?: { select?: Prisma.TaskRunSelect; include?: Prisma.TaskRunInclude } | ReadClient, diff --git a/internal-packages/run-store/src/types.ts b/internal-packages/run-store/src/types.ts index 319ef187814..98eb3bb9e1a 100644 --- a/internal-packages/run-store/src/types.ts +++ b/internal-packages/run-store/src/types.ts @@ -159,7 +159,12 @@ export type CreateRunInput = { }; export type CreateCancelledRunInput = { - data: CreateRunData & { error: Prisma.InputJsonValue; completedAt: Date; updatedAt: Date; attemptNumber: 0 }; + data: CreateRunData & { + error: Prisma.InputJsonValue; + completedAt: Date; + updatedAt: Date; + attemptNumber: 0; + }; snapshot: CreateRunSnapshotInput; }; @@ -227,7 +232,11 @@ export type RewriteDebouncedRunData = { export type ClearIdempotencyKeyInput = | { byId: { runId: string; idempotencyKey: string }; byPredicate?: never; byFriendlyIds?: never } - | { byPredicate: { idempotencyKey: string; taskIdentifier: string; runtimeEnvironmentId: string }; byId?: never; byFriendlyIds?: never } + | { + byPredicate: { idempotencyKey: string; taskIdentifier: string; runtimeEnvironmentId: string }; + byId?: never; + byFriendlyIds?: never; + } | { byFriendlyIds: string[]; byId?: never; byPredicate?: never }; export type TaskRunWithWaitpoint = TaskRun & { associatedWaitpoint: Waitpoint | null }; @@ -235,8 +244,14 @@ export type TaskRunWithWaitpoint = TaskRun & { associatedWaitpoint: Waitpoint | export interface RunStore { // Create createRun(params: CreateRunInput, tx?: PrismaClientOrTransaction): Promise; - createCancelledRun(params: CreateCancelledRunInput, tx?: PrismaClientOrTransaction): Promise; - createFailedRun(params: CreateFailedRunInput, tx?: PrismaClientOrTransaction): Promise; + createCancelledRun( + params: CreateCancelledRunInput, + tx?: PrismaClientOrTransaction + ): Promise; + createFailedRun( + params: CreateFailedRunInput, + tx?: PrismaClientOrTransaction + ): Promise; // Attempt lifecycle startAttempt( @@ -247,7 +262,14 @@ export interface RunStore { ): Promise>; completeAttemptSuccess( runId: string, - data: { completedAt: Date; output?: string; outputType: string; usageDurationMs: number; costInCents: number; snapshot: CompletionSnapshotInput }, + data: { + completedAt: Date; + output?: string; + outputType: string; + usageDurationMs: number; + costInCents: number; + snapshot: CompletionSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; @@ -262,16 +284,32 @@ export interface RunStore { args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - recordBulkActionMembership(runId: string, bulkActionId: string, tx?: PrismaClientOrTransaction): Promise; + recordBulkActionMembership( + runId: string, + bulkActionId: string, + tx?: PrismaClientOrTransaction + ): Promise; cancelRun( runId: string, - data: { completedAt?: Date; error: TaskRunError; bulkActionId?: string; usageDurationMs?: number; costInCents?: number }, + data: { + completedAt?: Date; + error: TaskRunError; + bulkActionId?: string; + usageDurationMs?: number; + costInCents?: number; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; failRunPermanently( runId: string, - data: { status: TaskRunStatus; completedAt: Date; error: TaskRunError; usageDurationMs: number; costInCents: number }, + data: { + status: TaskRunStatus; + completedAt: Date; + error: TaskRunError; + usageDurationMs: number; + costInCents: number; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; @@ -279,11 +317,20 @@ export interface RunStore { // Expiry expireRun( runId: string, - data: { error: TaskRunError; completedAt: Date; expiredAt: Date; snapshot: ExpireSnapshotInput }, + data: { + error: TaskRunError; + completedAt: Date; + expiredAt: Date; + snapshot: ExpireSnapshotInput; + }, args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - expireRunsBatch(runIds: string[], data: { error: TaskRunError; now: Date }, tx?: PrismaClientOrTransaction): Promise; + expireRunsBatch( + runIds: string[], + data: { error: TaskRunError; now: Date }, + tx?: PrismaClientOrTransaction + ): Promise; // Dequeue / version / checkpoint lockRunToWorker( @@ -297,7 +344,10 @@ export interface RunStore { args: { select: S }, tx?: PrismaClientOrTransaction ): Promise>; - promotePendingVersionRuns(runId: string, tx?: PrismaClientOrTransaction): Promise<{ count: number }>; + promotePendingVersionRuns( + runId: string, + tx?: PrismaClientOrTransaction + ): Promise<{ count: number }>; suspendForCheckpoint( runId: string, args: { include: I }, @@ -315,19 +365,44 @@ export interface RunStore { data: { delayUntil: Date; queueTimestamp?: Date; snapshot?: RescheduleSnapshotInput }, tx?: PrismaClientOrTransaction ): Promise; - enqueueDelayedRun(runId: string, data: { queuedAt: Date }, tx?: PrismaClientOrTransaction): Promise; - rewriteDebouncedRun(runId: string, data: RewriteDebouncedRunData, tx?: PrismaClientOrTransaction): Promise; + enqueueDelayedRun( + runId: string, + data: { queuedAt: Date }, + tx?: PrismaClientOrTransaction + ): Promise; + rewriteDebouncedRun( + runId: string, + data: RewriteDebouncedRunData, + tx?: PrismaClientOrTransaction + ): Promise; // Field touches updateMetadata( runId: string, - data: { metadata: string | null; metadataType?: string; metadataVersion: { increment: number }; updatedAt: Date }, + data: { + metadata: string | null; + metadataType?: string; + metadataVersion: { increment: number }; + updatedAt: Date; + }, options: { expectedMetadataVersion?: number }, tx?: PrismaClientOrTransaction ): Promise<{ count: number }>; - clearIdempotencyKey(params: ClearIdempotencyKeyInput, tx?: PrismaClientOrTransaction): Promise<{ count: number }>; - pushTags(runId: string, tags: string[], where: { runtimeEnvironmentId: string }, tx?: PrismaClientOrTransaction): Promise<{ updatedAt: Date }>; - pushRealtimeStream(runId: string, streamId: string, tx?: PrismaClientOrTransaction): Promise; + clearIdempotencyKey( + params: ClearIdempotencyKeyInput, + tx?: PrismaClientOrTransaction + ): Promise<{ count: number }>; + pushTags( + runId: string, + tags: string[], + where: { runtimeEnvironmentId: string }, + tx?: PrismaClientOrTransaction + ): Promise<{ updatedAt: Date }>; + pushRealtimeStream( + runId: string, + streamId: string, + tx?: PrismaClientOrTransaction + ): Promise; // Read findRun( diff --git a/internal-packages/schedule-engine/package.json b/internal-packages/schedule-engine/package.json index 86929a39341..52a27a11e27 100644 --- a/internal-packages/schedule-engine/package.json +++ b/internal-packages/schedule-engine/package.json @@ -36,4 +36,4 @@ "build": "pnpm run clean && tsc -p tsconfig.build.json", "dev": "tsc --watch -p tsconfig.build.json" } -} \ No newline at end of file +} diff --git a/internal-packages/schedule-engine/src/engine/index.ts b/internal-packages/schedule-engine/src/engine/index.ts index 2c78beccbc8..0d82d2b5f13 100644 --- a/internal-packages/schedule-engine/src/engine/index.ts +++ b/internal-packages/schedule-engine/src/engine/index.ts @@ -576,7 +576,7 @@ export class ScheduleEngine { // last-fire timestamp with a series of skipped slots. const carriedLastScheduleTime = shouldTrigger ? scheduleTimestamp - : params.lastScheduleTime ?? instance.lastScheduledTimestamp ?? undefined; + : (params.lastScheduleTime ?? instance.lastScheduledTimestamp ?? undefined); const [nextRunError] = await tryCatch( this.registerNextTaskScheduleInstance({ diff --git a/internal-packages/schedule-engine/test/scheduleRecovery.test.ts b/internal-packages/schedule-engine/test/scheduleRecovery.test.ts index 40ce4b1bba6..b791d936547 100644 --- a/internal-packages/schedule-engine/test/scheduleRecovery.test.ts +++ b/internal-packages/schedule-engine/test/scheduleRecovery.test.ts @@ -565,7 +565,8 @@ describe("Schedule Recovery", () => { const job = await engine.getJob(`scheduled-task-instance:${scheduleInstance.id}`); expect(job).not.toBeNull(); - const enqueuedLastScheduleTime = (job?.item as { lastScheduleTime?: Date }).lastScheduleTime; + const enqueuedLastScheduleTime = (job?.item as { lastScheduleTime?: Date }) + .lastScheduleTime; // Brand-new schedule: cron's previous slot predates instance.createdAt, // so the function leaves lastScheduleTime undefined — the first fire // will report `payload.lastTimestamp: undefined` and customer first-run diff --git a/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts b/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts index 6894606fd0a..87283db3aa9 100644 --- a/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts +++ b/internal-packages/sdk-compat-tests/src/fixtures/deno/test.ts @@ -6,7 +6,18 @@ */ // Use bare specifier - resolved via node_modules when nodeModulesDir is enabled -import { task, logger, schedules, runs, configure, queue, retry, wait, metadata, tags } from "@trigger.dev/sdk"; +import { + task, + logger, + schedules, + runs, + configure, + queue, + retry, + wait, + metadata, + tags, +} from "@trigger.dev/sdk"; // Validate exports exist const checks: [string, boolean][] = [ diff --git a/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs b/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs index 70b055aaa2f..a90aaaa1d88 100644 --- a/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs +++ b/internal-packages/sdk-compat-tests/src/fixtures/esm-import/test.mjs @@ -6,7 +6,18 @@ */ // Test main export -import { task, logger, schedules, runs, configure, queue, retry, wait, metadata, tags } from "@trigger.dev/sdk"; +import { + task, + logger, + schedules, + runs, + configure, + queue, + retry, + wait, + metadata, + tags, +} from "@trigger.dev/sdk"; // Test /v3 subpath (legacy, but should still work) import { task as taskV3 } from "@trigger.dev/sdk/v3"; diff --git a/internal-packages/sso/src/fallback.ts b/internal-packages/sso/src/fallback.ts index 564eb3391b5..6d98e7c6573 100644 --- a/internal-packages/sso/src/fallback.ts +++ b/internal-packages/sso/src/fallback.ts @@ -102,10 +102,7 @@ class SsoFallbackController implements SsoController { return errAsync("feature_disabled" as const); } - completeAuthorization(_params: { - code: string; - state: string; - }): ResultAsync< + completeAuthorization(_params: { code: string; state: string }): ResultAsync< { profile: SsoProfile; redirectTo: string; diff --git a/internal-packages/sso/src/index.ts b/internal-packages/sso/src/index.ts index aec22b81ce4..5f65ae6f972 100644 --- a/internal-packages/sso/src/index.ts +++ b/internal-packages/sso/src/index.ts @@ -20,9 +20,7 @@ import { ResultAsync } from "neverthrow"; import { SsoFallback } from "./fallback.js"; export type { SsoController } from "@trigger.dev/plugins"; -export type SsoPrismaInput = - | PrismaClient - | { primary: PrismaClient; replica: PrismaClient }; +export type SsoPrismaInput = PrismaClient | { primary: PrismaClient; replica: PrismaClient }; export type SsoCreateOptions = { // When true, skip loading the plugin. Useful for tests and for @@ -44,17 +42,13 @@ export class LazyController implements SsoController { this._init = this.load(prisma, options); } - private async load( - prisma: SsoPrismaInput, - options?: SsoCreateOptions - ): Promise { + private async load(prisma: SsoPrismaInput, options?: SsoCreateOptions): Promise { if (options?.forceFallback) { return new SsoFallback(prisma).create(); } const moduleName = "@triggerdotdev/plugins/sso"; const importer = - options?.importer ?? - ((m: string) => import(m) as Promise<{ default: SsoPlugin }>); + options?.importer ?? ((m: string) => import(m) as Promise<{ default: SsoPlugin }>); try { const module = await importer(moduleName); const plugin: SsoPlugin = module.default; @@ -75,10 +69,8 @@ export class LazyController implements SsoController { // plugin's own module name. const code = (err as NodeJS.ErrnoException | undefined)?.code; const message = err instanceof Error ? err.message : String(err); - const isModuleNotFound = - code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; - const isPluginItselfMissing = - isModuleNotFound && message.includes(moduleName); + const isModuleNotFound = code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND"; + const isPluginItselfMissing = isModuleNotFound && message.includes(moduleName); if (!isPluginItselfMissing) { console.error( diff --git a/internal-packages/sso/src/loader.test.ts b/internal-packages/sso/src/loader.test.ts index fa05f2947eb..de2c6731e1e 100644 --- a/internal-packages/sso/src/loader.test.ts +++ b/internal-packages/sso/src/loader.test.ts @@ -122,10 +122,7 @@ describe("SSO LazyController", () => { const controller = new LazyController(fakePrisma, { importer }); await controller.isUsingPlugin(); const fallbackLogs = logSpy.mock.calls.filter((args) => - args.some( - (a) => - typeof a === "string" && a.includes("no plugin installed") - ) + args.some((a) => typeof a === "string" && a.includes("no plugin installed")) ); expect(fallbackLogs.length).toBe(0); expect(errorSpy).not.toHaveBeenCalled(); @@ -152,9 +149,7 @@ describe("SSO LazyController", () => { const controller = new LazyController(fakePrisma, { importer }); await controller.isUsingPlugin(); const fallbackLogs = logSpy.mock.calls.filter((args) => - args.some( - (a) => typeof a === "string" && a.includes("no plugin installed") - ) + args.some((a) => typeof a === "string" && a.includes("no plugin installed")) ); expect(fallbackLogs.length).toBe(1); } finally { @@ -170,10 +165,9 @@ describe("SSO LazyController", () => { const importer = vi.fn(async () => { // Module-not-found from a *transitive* dep, not the plugin // itself — its `message` won't contain the plugin's moduleName. - const err = Object.assign( - new Error(`Cannot find module 'some-transitive-dep'`), - { code: "ERR_MODULE_NOT_FOUND" } - ); + const err = Object.assign(new Error(`Cannot find module 'some-transitive-dep'`), { + code: "ERR_MODULE_NOT_FOUND", + }); throw err; }); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); diff --git a/internal-packages/testcontainers/src/webapp.ts b/internal-packages/testcontainers/src/webapp.ts index 0c24ec546a1..02f36dd9c49 100644 --- a/internal-packages/testcontainers/src/webapp.ts +++ b/internal-packages/testcontainers/src/webapp.ts @@ -112,14 +112,14 @@ export async function startWebapp( REDIS_TLS_DISABLED: "true", // all *_REDIS_TLS_DISABLED vars default to this; test Redis has no TLS // Disable all background workers. Each worker has its own env var and its own // check idiom ("0" vs "false" vs boolean), so we set all of them explicitly. - WORKER_ENABLED: "false", // disables workerQueue.initialize() (checked === "true") - RUN_ENGINE_WORKER_ENABLED: "0", // disables run engine workers (checked === "0", default "1") - SCHEDULE_WORKER_ENABLED: "0", // disables schedule engine worker (checked === "0") + WORKER_ENABLED: "false", // disables workerQueue.initialize() (checked === "true") + RUN_ENGINE_WORKER_ENABLED: "0", // disables run engine workers (checked === "0", default "1") + SCHEDULE_WORKER_ENABLED: "0", // disables schedule engine worker (checked === "0") BATCH_QUEUE_WORKER_ENABLED: "false", // disables batch queue consumers (BoolEnv) LEGACY_RUN_ENGINE_WORKER_ENABLED: "0", // disables legacy run engine worker - COMMON_WORKER_ENABLED: "0", // disables common worker - RUN_ENGINE_TTL_SYSTEM_DISABLED: "true", // disables TTL expiry system (BoolEnv) - RUN_ENGINE_TTL_CONSUMERS_DISABLED: "true", // disables TTL consumers (BoolEnv) + COMMON_WORKER_ENABLED: "0", // disables common worker + RUN_ENGINE_TTL_SYSTEM_DISABLED: "true", // disables TTL expiry system (BoolEnv) + RUN_ENGINE_TTL_CONSUMERS_DISABLED: "true", // disables TTL consumers (BoolEnv) RUN_REPLICATION_ENABLED: "0", // Force the RBAC loader to use the default fallback in e2e tests // so auth behaviour is deterministic regardless of whether a @@ -208,9 +208,7 @@ export interface TestServer { } /** Convenience helper: starts a postgres + redis container + webapp and returns both for testing. */ -export async function startTestServer( - options: StartWebappOptions = {} -): Promise { +export async function startTestServer(options: StartWebappOptions = {}): Promise { const network = await new Network().start(); // Track each resource as we acquire it so we can tear it down if a later step fails. @@ -231,11 +229,7 @@ export async function startTestServer( prisma = new PrismaClient({ datasources: { db: { url: pg.url } } }); await prisma.$connect(); // pre-warm pool; surface connection failures before tests start - const started = await startWebapp( - pg.url, - { host: rc.getHost(), port: rc.getPort() }, - options - ); + const started = await startWebapp(pg.url, { host: rc.getHost(), port: rc.getPort() }, options); webapp = started.instance; stopWebapp = started.stop; } catch (err) { diff --git a/internal-packages/tracing/package.json b/internal-packages/tracing/package.json index 7474ec249ec..a3aa10bb152 100644 --- a/internal-packages/tracing/package.json +++ b/internal-packages/tracing/package.json @@ -14,4 +14,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/internal-packages/tsql/src/index.test.ts b/internal-packages/tsql/src/index.test.ts index a93fb0fd4b0..3c958635e8d 100644 --- a/internal-packages/tsql/src/index.test.ts +++ b/internal-packages/tsql/src/index.test.ts @@ -113,9 +113,7 @@ describe("isColumnReferencedInExpression", () => { }); it("should detect column in qualified reference (table.column)", () => { - const ast = parseTSQLSelect( - "SELECT * FROM task_runs WHERE task_runs.time > '2024-01-01'" - ); + const ast = parseTSQLSelect("SELECT * FROM task_runs WHERE task_runs.time > '2024-01-01'"); if (ast.expression_type === "select_query") { expect(isColumnReferencedInExpression(ast.where, "time")).toBe(true); } @@ -408,15 +406,12 @@ describe("compileTSQL with whereClauseFallback", () => { }); it("should NOT apply fallback when column is in qualified reference", () => { - const { sql } = compileTSQL( - "SELECT * FROM task_runs WHERE task_runs.time > '2024-06-01'", - { - ...baseOptions, - whereClauseFallback: { - time: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT * FROM task_runs WHERE task_runs.time > '2024-06-01'", { + ...baseOptions, + whereClauseFallback: { + time: { op: "gte", value: "2024-01-01" }, + }, + }); // Fallback should not be applied const timeGreaterMatches = sql.match(/greater.*time/g) || []; @@ -588,16 +583,13 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should apply enforced condition even when user filters on same field", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + }); // Should have BOTH the user's condition AND the enforced condition // User's condition: greater(triggered_at, '2025-01-01') @@ -681,19 +673,16 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should apply enforced but not fallback when user filters on fallback column", () => { - const { sql, params } = compileTSQL( - "SELECT id FROM task_runs WHERE status = 'failed'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - whereClauseFallback: { - status: { op: "eq", value: "completed" }, - }, - } - ); + const { sql, params } = compileTSQL("SELECT id FROM task_runs WHERE status = 'failed'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + whereClauseFallback: { + status: { op: "eq", value: "completed" }, + }, + }); // Enforced triggered_at should be applied expect(sql).toContain("triggered_at"); @@ -722,19 +711,16 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should skip fallback but keep enforced when user filters on same field", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-06-01" }, // Enforced: always applied - }, - whereClauseFallback: { - triggered_at: { op: "gte", value: "2024-01-01" }, // Fallback: skipped since user filtered - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE triggered_at > '2025-01-01'", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-06-01" }, // Enforced: always applied + }, + whereClauseFallback: { + triggered_at: { op: "gte", value: "2024-01-01" }, // Fallback: skipped since user filtered + }, + }); // User's condition + enforced should be present // Fallback should NOT be applied since user filtered on triggered_at @@ -767,16 +753,13 @@ describe("compileTSQL with enforcedWhereClause", () => { }); it("should NOT be bypassable via OR clause", () => { - const { sql } = compileTSQL( - "SELECT id FROM task_runs WHERE status = 'completed' OR 1=1", - { - tableSchema: [taskRunsSchema], - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_123" }, - triggered_at: { op: "gte", value: "2024-01-01" }, - }, - } - ); + const { sql } = compileTSQL("SELECT id FROM task_runs WHERE status = 'completed' OR 1=1", { + tableSchema: [taskRunsSchema], + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_123" }, + triggered_at: { op: "gte", value: "2024-01-01" }, + }, + }); // The enforced conditions should be ANDed with the entire user WHERE clause // So the structure should be: (enforced AND enforced AND ...) AND (user_where) @@ -825,4 +808,3 @@ describe("compileTSQL with enforcedWhereClause", () => { }); }); }); - diff --git a/internal-packages/tsql/src/query/database.ts b/internal-packages/tsql/src/query/database.ts index e91e0132c53..7961ed4d6b5 100644 --- a/internal-packages/tsql/src/query/database.ts +++ b/internal-packages/tsql/src/query/database.ts @@ -452,8 +452,8 @@ function constantTypeToSerializedFieldType( return printed === "String" ? DatabaseSerializedFieldType.STRING : printed === "JSON" - ? DatabaseSerializedFieldType.JSON - : DatabaseSerializedFieldType.ARRAY; + ? DatabaseSerializedFieldType.JSON + : DatabaseSerializedFieldType.ARRAY; } if (printed === "Boolean") return DatabaseSerializedFieldType.BOOLEAN; if (printed === "Date") return DatabaseSerializedFieldType.DATE; diff --git a/internal-packages/tsql/src/query/functions.ts b/internal-packages/tsql/src/query/functions.ts index f184ed8f382..2f2b9278454 100644 --- a/internal-packages/tsql/src/query/functions.ts +++ b/internal-packages/tsql/src/query/functions.ts @@ -136,7 +136,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { substr: { clickhouseName: "substring", minArgs: 2, maxArgs: 3 }, mid: { clickhouseName: "substring", minArgs: 2, maxArgs: 3 }, substringUTF8: { clickhouseName: "substringUTF8", minArgs: 2, maxArgs: 3 }, - appendTrailingCharIfAbsent: { clickhouseName: "appendTrailingCharIfAbsent", minArgs: 2, maxArgs: 2 }, + appendTrailingCharIfAbsent: { + clickhouseName: "appendTrailingCharIfAbsent", + minArgs: 2, + maxArgs: 2, + }, convertCharset: { clickhouseName: "convertCharset", minArgs: 3, maxArgs: 3 }, base58Encode: { clickhouseName: "base58Encode", minArgs: 1, maxArgs: 1 }, base58Decode: { clickhouseName: "base58Decode", minArgs: 1, maxArgs: 1 }, @@ -166,7 +170,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { position: { clickhouseName: "position", minArgs: 2, maxArgs: 2 }, positionCaseInsensitive: { clickhouseName: "positionCaseInsensitive", minArgs: 2, maxArgs: 2 }, positionUTF8: { clickhouseName: "positionUTF8", minArgs: 2, maxArgs: 2 }, - positionCaseInsensitiveUTF8: { clickhouseName: "positionCaseInsensitiveUTF8", minArgs: 2, maxArgs: 2 }, + positionCaseInsensitiveUTF8: { + clickhouseName: "positionCaseInsensitiveUTF8", + minArgs: 2, + maxArgs: 2, + }, locate: { clickhouseName: "locate", minArgs: 2, maxArgs: 2 }, match: { clickhouseName: "match", minArgs: 2, maxArgs: 2 }, multiMatchAny: { clickhouseName: "multiMatchAny", minArgs: 2, maxArgs: 2 }, @@ -177,7 +185,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { multiSearchAny: { clickhouseName: "multiSearchAny", minArgs: 2, maxArgs: 2 }, extract: { clickhouseName: "extract", minArgs: 2, maxArgs: 2 }, extractAll: { clickhouseName: "extractAll", minArgs: 2, maxArgs: 2 }, - extractAllGroupsHorizontal: { clickhouseName: "extractAllGroupsHorizontal", minArgs: 2, maxArgs: 2 }, + extractAllGroupsHorizontal: { + clickhouseName: "extractAllGroupsHorizontal", + minArgs: 2, + maxArgs: 2, + }, extractAllGroupsVertical: { clickhouseName: "extractAllGroupsVertical", minArgs: 2, maxArgs: 2 }, like: { clickhouseName: "like", minArgs: 2, maxArgs: 2 }, ilike: { clickhouseName: "ilike", minArgs: 2, maxArgs: 2 }, @@ -294,12 +306,42 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { toTimeZone: { clickhouseName: "toTimeZone", minArgs: 2, maxArgs: 2 }, formatDateTime: { clickhouseName: "formatDateTime", minArgs: 2, maxArgs: 3 }, parseDateTime: { clickhouseName: "parseDateTime", minArgs: 2, maxArgs: 3 }, - parseDateTimeBestEffort: { clickhouseName: "parseDateTimeBestEffort", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTimeBestEffortOrNull: { clickhouseName: "parseDateTimeBestEffortOrNull", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTimeBestEffortOrZero: { clickhouseName: "parseDateTimeBestEffortOrZero", minArgs: 1, maxArgs: 2, tzAware: true }, - parseDateTime64BestEffort: { clickhouseName: "parseDateTime64BestEffort", minArgs: 1, maxArgs: 3, tzAware: true }, - parseDateTime64BestEffortOrNull: { clickhouseName: "parseDateTime64BestEffortOrNull", minArgs: 1, maxArgs: 3, tzAware: true }, - parseDateTime64BestEffortOrZero: { clickhouseName: "parseDateTime64BestEffortOrZero", minArgs: 1, maxArgs: 3, tzAware: true }, + parseDateTimeBestEffort: { + clickhouseName: "parseDateTimeBestEffort", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTimeBestEffortOrNull: { + clickhouseName: "parseDateTimeBestEffortOrNull", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTimeBestEffortOrZero: { + clickhouseName: "parseDateTimeBestEffortOrZero", + minArgs: 1, + maxArgs: 2, + tzAware: true, + }, + parseDateTime64BestEffort: { + clickhouseName: "parseDateTime64BestEffort", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, + parseDateTime64BestEffortOrNull: { + clickhouseName: "parseDateTime64BestEffortOrNull", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, + parseDateTime64BestEffortOrZero: { + clickhouseName: "parseDateTime64BestEffortOrZero", + minArgs: 1, + maxArgs: 3, + tzAware: true, + }, // Interval functions toIntervalSecond: { clickhouseName: "toIntervalSecond", minArgs: 1, maxArgs: 1 }, @@ -413,9 +455,21 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { domain: { clickhouseName: "domain", minArgs: 1, maxArgs: 1 }, domainWithoutWWW: { clickhouseName: "domainWithoutWWW", minArgs: 1, maxArgs: 1 }, topLevelDomain: { clickhouseName: "topLevelDomain", minArgs: 1, maxArgs: 1 }, - firstSignificantSubdomain: { clickhouseName: "firstSignificantSubdomain", minArgs: 1, maxArgs: 1 }, - cutToFirstSignificantSubdomain: { clickhouseName: "cutToFirstSignificantSubdomain", minArgs: 1, maxArgs: 1 }, - cutToFirstSignificantSubdomainWithWWW: { clickhouseName: "cutToFirstSignificantSubdomainWithWWW", minArgs: 1, maxArgs: 1 }, + firstSignificantSubdomain: { + clickhouseName: "firstSignificantSubdomain", + minArgs: 1, + maxArgs: 1, + }, + cutToFirstSignificantSubdomain: { + clickhouseName: "cutToFirstSignificantSubdomain", + minArgs: 1, + maxArgs: 1, + }, + cutToFirstSignificantSubdomainWithWWW: { + clickhouseName: "cutToFirstSignificantSubdomainWithWWW", + minArgs: 1, + maxArgs: 1, + }, port: { clickhouseName: "port", minArgs: 1, maxArgs: 2 }, path: { clickhouseName: "path", minArgs: 1, maxArgs: 1 }, pathFull: { clickhouseName: "pathFull", minArgs: 1, maxArgs: 1 }, @@ -438,7 +492,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { isNaN: { clickhouseName: "isNaN", minArgs: 1, maxArgs: 1 }, bar: { clickhouseName: "bar", minArgs: 4, maxArgs: 4 }, transform: { clickhouseName: "transform", minArgs: 3, maxArgs: 4 }, - formatReadableDecimalSize: { clickhouseName: "formatReadableDecimalSize", minArgs: 1, maxArgs: 1 }, + formatReadableDecimalSize: { + clickhouseName: "formatReadableDecimalSize", + minArgs: 1, + maxArgs: 1, + }, formatReadableSize: { clickhouseName: "formatReadableSize", minArgs: 1, maxArgs: 1 }, formatReadableQuantity: { clickhouseName: "formatReadableQuantity", minArgs: 1, maxArgs: 1 }, formatReadableTimeDelta: { clickhouseName: "formatReadableTimeDelta", minArgs: 1, maxArgs: 2 }, @@ -447,7 +505,11 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record = { min2: { clickhouseName: "min2", minArgs: 2, maxArgs: 2 }, max2: { clickhouseName: "max2", minArgs: 2, maxArgs: 2 }, runningDifference: { clickhouseName: "runningDifference", minArgs: 1, maxArgs: 1 }, - runningDifferenceStartingWithFirstValue: { clickhouseName: "runningDifferenceStartingWithFirstValue", minArgs: 1, maxArgs: 1 }, + runningDifferenceStartingWithFirstValue: { + clickhouseName: "runningDifferenceStartingWithFirstValue", + minArgs: 1, + maxArgs: 1, + }, neighbor: { clickhouseName: "neighbor", minArgs: 2, maxArgs: 3 }, // Window functions @@ -504,10 +566,32 @@ export const TSQL_AGGREGATIONS: Record = { groupArrayIf: { clickhouseName: "groupArrayIf", minArgs: 2, maxArgs: 2, aggregate: true }, groupUniqArray: { clickhouseName: "groupUniqArray", minArgs: 1, maxArgs: 1, aggregate: true }, groupUniqArrayIf: { clickhouseName: "groupUniqArrayIf", minArgs: 2, maxArgs: 2, aggregate: true }, - groupArrayInsertAt: { clickhouseName: "groupArrayInsertAt", minArgs: 2, maxArgs: 2, aggregate: true }, - groupArrayMovingAvg: { clickhouseName: "groupArrayMovingAvg", minArgs: 1, maxArgs: 1, aggregate: true }, - groupArrayMovingSum: { clickhouseName: "groupArrayMovingSum", minArgs: 1, maxArgs: 1, aggregate: true }, - groupArraySample: { clickhouseName: "groupArraySample", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 2, aggregate: true }, + groupArrayInsertAt: { + clickhouseName: "groupArrayInsertAt", + minArgs: 2, + maxArgs: 2, + aggregate: true, + }, + groupArrayMovingAvg: { + clickhouseName: "groupArrayMovingAvg", + minArgs: 1, + maxArgs: 1, + aggregate: true, + }, + groupArrayMovingSum: { + clickhouseName: "groupArrayMovingSum", + minArgs: 1, + maxArgs: 1, + aggregate: true, + }, + groupArraySample: { + clickhouseName: "groupArraySample", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 2, + aggregate: true, + }, array_agg: { clickhouseName: "groupArray", minArgs: 1, maxArgs: 1, aggregate: true }, // Bitmap aggregations @@ -528,12 +612,39 @@ export const TSQL_AGGREGATIONS: Record = { median: { clickhouseName: "median", minArgs: 1, maxArgs: 1, aggregate: true }, medianIf: { clickhouseName: "medianIf", minArgs: 2, maxArgs: 2, aggregate: true }, medianExact: { clickhouseName: "medianExact", minArgs: 1, maxArgs: 1, aggregate: true }, - quantile: { clickhouseName: "quantile", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, - quantileIf: { clickhouseName: "quantileIf", minArgs: 2, maxArgs: 2, minParams: 1, maxParams: 1, aggregate: true }, + quantile: { + clickhouseName: "quantile", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, + quantileIf: { + clickhouseName: "quantileIf", + minArgs: 2, + maxArgs: 2, + minParams: 1, + maxParams: 1, + aggregate: true, + }, quantiles: { clickhouseName: "quantiles", minArgs: 1, aggregate: true }, // -Merge combinators for AggregatingMergeTree tables - quantilesMerge: { clickhouseName: "quantilesMerge", minArgs: 1, maxArgs: 1, minParams: 1, aggregate: true }, - quantileMerge: { clickhouseName: "quantileMerge", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, + quantilesMerge: { + clickhouseName: "quantilesMerge", + minArgs: 1, + maxArgs: 1, + minParams: 1, + aggregate: true, + }, + quantileMerge: { + clickhouseName: "quantileMerge", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, sumMerge: { clickhouseName: "sumMerge", minArgs: 1, maxArgs: 1, aggregate: true }, avgMerge: { clickhouseName: "avgMerge", minArgs: 1, maxArgs: 1, aggregate: true }, countMerge: { clickhouseName: "countMerge", minArgs: 1, maxArgs: 1, aggregate: true }, @@ -541,7 +652,12 @@ export const TSQL_AGGREGATIONS: Record = { maxMerge: { clickhouseName: "maxMerge", minArgs: 1, maxArgs: 1, aggregate: true }, // Statistical functions - simpleLinearRegression: { clickhouseName: "simpleLinearRegression", minArgs: 2, maxArgs: 2, aggregate: true }, + simpleLinearRegression: { + clickhouseName: "simpleLinearRegression", + minArgs: 2, + maxArgs: 2, + aggregate: true, + }, contingency: { clickhouseName: "contingency", minArgs: 2, maxArgs: 2, aggregate: true }, cramersV: { clickhouseName: "cramersV", minArgs: 2, maxArgs: 2, aggregate: true }, theilsU: { clickhouseName: "theilsU", minArgs: 2, maxArgs: 2, aggregate: true }, @@ -552,7 +668,14 @@ export const TSQL_AGGREGATIONS: Record = { maxMap: { clickhouseName: "maxMap", minArgs: 1, maxArgs: 2, aggregate: true }, // TopK - topK: { clickhouseName: "topK", minArgs: 1, maxArgs: 1, minParams: 1, maxParams: 1, aggregate: true }, + topK: { + clickhouseName: "topK", + minArgs: 1, + maxArgs: 1, + minParams: 1, + maxParams: 1, + aggregate: true, + }, // Funnel windowFunnel: { clickhouseName: "windowFunnel", minArgs: 1, maxArgs: 99, aggregate: true }, @@ -623,7 +746,9 @@ export function findTSQLFunction(name: string): TSQLFunctionMeta | undefined { * Get all exposed function names (for autocomplete, suggestions, etc.) */ export function getAllExposedFunctionNames(): string[] { - const functionNames = Object.keys(TSQL_CLICKHOUSE_FUNCTIONS).filter((name) => !name.startsWith("_")); + const functionNames = Object.keys(TSQL_CLICKHOUSE_FUNCTIONS).filter( + (name) => !name.startsWith("_") + ); const aggregationNames = Object.keys(TSQL_AGGREGATIONS).filter((name) => !name.startsWith("_")); return [...functionNames, ...aggregationNames]; } @@ -662,4 +787,3 @@ export function validateFunctionArgs( ); } } - diff --git a/internal-packages/tsql/src/query/parse_string.ts b/internal-packages/tsql/src/query/parse_string.ts index be38f752dd2..996c4e5494e 100644 --- a/internal-packages/tsql/src/query/parse_string.ts +++ b/internal-packages/tsql/src/query/parse_string.ts @@ -1,65 +1,69 @@ // TypeScript translation of posthog/hogql/parse_string.py // Keep this file in sync with the Python version -import { SyntaxError } from './errors'; +import { SyntaxError } from "./errors"; function replaceCommonEscapeCharacters(text: string): string { - // copied from clickhouse_driver/util/escape.py - // Note: \a (bell) and \v (vertical tab) are not directly supported in JavaScript strings - // but we handle them as escape sequences that get replaced - text = text.replace(/\\b/g, '\b'); - text = text.replace(/\\f/g, '\f'); - text = text.replace(/\\r/g, '\r'); - text = text.replace(/\\n/g, '\n'); - text = text.replace(/\\t/g, '\t'); - text = text.replace(/\\0/g, ''); // NUL characters are ignored - text = text.replace(/\\a/g, '\x07'); // Bell character (ASCII 7) - text = text.replace(/\\v/g, '\x0B'); // Vertical tab (ASCII 11) - text = text.replace(/\\\\/g, '\\'); - return text; + // copied from clickhouse_driver/util/escape.py + // Note: \a (bell) and \v (vertical tab) are not directly supported in JavaScript strings + // but we handle them as escape sequences that get replaced + text = text.replace(/\\b/g, "\b"); + text = text.replace(/\\f/g, "\f"); + text = text.replace(/\\r/g, "\r"); + text = text.replace(/\\n/g, "\n"); + text = text.replace(/\\t/g, "\t"); + text = text.replace(/\\0/g, ""); // NUL characters are ignored + text = text.replace(/\\a/g, "\x07"); // Bell character (ASCII 7) + text = text.replace(/\\v/g, "\x0B"); // Vertical tab (ASCII 11) + text = text.replace(/\\\\/g, "\\"); + return text; } export function parseStringLiteralText(text: string): string { - /** Converts a string received from antlr via ctx.getText() into a JavaScript string */ - let result: string; - - if (text.startsWith("'") && text.endsWith("'")) { - result = text.slice(1, -1); - result = result.replace(/''/g, "'"); - result = result.replace(/\\'/g, "'"); - } else if (text.startsWith('"') && text.endsWith('"')) { - result = text.slice(1, -1); - result = result.replace(/""/g, '"'); - result = result.replace(/\\"/g, '"'); - } else if (text.startsWith('`') && text.endsWith('`')) { - result = text.slice(1, -1); - result = result.replace(/``/g, '`'); - result = result.replace(/\\`/g, '`'); - } else if (text.startsWith('{') && text.endsWith('}')) { - result = text.slice(1, -1); - result = result.replace(/{{/g, '{'); - result = result.replace(/\\{/g, '{'); - } else { - throw new SyntaxError(`Invalid string literal, must start and end with the same quote type: ${text}`); - } + /** Converts a string received from antlr via ctx.getText() into a JavaScript string */ + let result: string; - return replaceCommonEscapeCharacters(result); + if (text.startsWith("'") && text.endsWith("'")) { + result = text.slice(1, -1); + result = result.replace(/''/g, "'"); + result = result.replace(/\\'/g, "'"); + } else if (text.startsWith('"') && text.endsWith('"')) { + result = text.slice(1, -1); + result = result.replace(/""/g, '"'); + result = result.replace(/\\"/g, '"'); + } else if (text.startsWith("`") && text.endsWith("`")) { + result = text.slice(1, -1); + result = result.replace(/``/g, "`"); + result = result.replace(/\\`/g, "`"); + } else if (text.startsWith("{") && text.endsWith("}")) { + result = text.slice(1, -1); + result = result.replace(/{{/g, "{"); + result = result.replace(/\\{/g, "{"); + } else { + throw new SyntaxError( + `Invalid string literal, must start and end with the same quote type: ${text}` + ); + } + + return replaceCommonEscapeCharacters(result); } export function parseStringLiteralCtx(ctx: { getText(): string }): string { - /** Converts a STRING_LITERAL received from antlr via ctx.getText() into a JavaScript string */ - const text = ctx.getText(); - return parseStringLiteralText(text); + /** Converts a STRING_LITERAL received from antlr via ctx.getText() into a JavaScript string */ + const text = ctx.getText(); + return parseStringLiteralText(text); } -export function parseStringTextCtx(ctx: { getText(): string }, escapeQuotes: boolean = true): string { - /** Converts a STRING_TEXT received from antlr via ctx.getText() into a JavaScript string */ - let text = ctx.getText(); - if (escapeQuotes) { - text = text.replace(/''/g, "'"); - text = text.replace(/\\'/g, "'"); - } - text = text.replace(/\\{/g, '{'); - return replaceCommonEscapeCharacters(text); +export function parseStringTextCtx( + ctx: { getText(): string }, + escapeQuotes: boolean = true +): string { + /** Converts a STRING_TEXT received from antlr via ctx.getText() into a JavaScript string */ + let text = ctx.getText(); + if (escapeQuotes) { + text = text.replace(/''/g, "'"); + text = text.replace(/\\'/g, "'"); + } + text = text.replace(/\\{/g, "{"); + return replaceCommonEscapeCharacters(text); } - diff --git a/internal-packages/tsql/src/query/parser.ts b/internal-packages/tsql/src/query/parser.ts index 05b1df052ba..a4be2fbb657 100644 --- a/internal-packages/tsql/src/query/parser.ts +++ b/internal-packages/tsql/src/query/parser.ts @@ -1689,9 +1689,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor { visitTsqlxTagElementClosed(ctx: TSQLxTagElementContext): TSQLXTag { const kind = this.visitIdentifier(ctx.identifier()[0]); const attributes = ctx.tSQLxTagAttribute() - ? ctx - .tSQLxTagAttribute() - .map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) + ? ctx.tSQLxTagAttribute().map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) : []; return { expression_type: "tsqlx_tag", kind, attributes }; } @@ -1706,9 +1704,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor { } const attributes = ctx.tSQLxTagAttribute() - ? ctx - .tSQLxTagAttribute() - .map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) + ? ctx.tSQLxTagAttribute().map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a)) : []; // ── collect child nodes, discarding pure-indentation whitespace ── diff --git a/internal-packages/tsql/src/query/printer.test.ts b/internal-packages/tsql/src/query/printer.test.ts index 88ce7330354..0aa5b816064 100644 --- a/internal-packages/tsql/src/query/printer.test.ts +++ b/internal-packages/tsql/src/query/printer.test.ts @@ -2,12 +2,7 @@ import { describe, it, expect, beforeEach } from "vitest"; import { parseTSQLSelect, parseTSQLExpr, compileTSQL } from "../index.js"; import { ClickHousePrinter, printToClickHouse, type PrintResult } from "./printer.js"; import { createPrinterContext, PrinterContext } from "./printer_context.js"; -import { - createSchemaRegistry, - column, - type TableSchema, - type SchemaRegistry, -} from "./schema.js"; +import { createSchemaRegistry, column, type TableSchema, type SchemaRegistry } from "./schema.js"; import type { BucketThreshold } from "./time_buckets.js"; import { QueryError, SyntaxError } from "./errors.js"; @@ -253,17 +248,17 @@ describe("ClickHousePrinter", () => { describe("Table and column name mapping", () => { function createMappedContext() { const schema = createSchemaRegistry([runsSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - it("should map user-friendly table name to ClickHouse name", () => { + it("should map user-friendly table name to ClickHouse name", () => { const ctx = createMappedContext(); const { sql } = printQuery("SELECT * FROM runs", ctx); @@ -486,17 +481,17 @@ describe("ClickHousePrinter", () => { function createJsonContext() { const schema = createSchemaRegistry([jsonSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - it("should transform IS NULL to equals empty object for JSON columns with nullValue", () => { + it("should transform IS NULL to equals empty object for JSON columns with nullValue", () => { const ctx = createJsonContext(); const { sql } = printQuery("SELECT * FROM runs WHERE error IS NULL", ctx); @@ -616,10 +611,7 @@ describe("ClickHousePrinter", () => { it("should NOT add .:String type hint for JSON subfield in WHERE comparison", () => { const ctx = createJsonContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.data.name = 'test'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.data.name = 'test'", ctx); // WHERE clause should NOT have .:String type hint (it breaks the query) expect(sql).toContain("equals(error.data.name,"); @@ -628,10 +620,7 @@ describe("ClickHousePrinter", () => { it("should NOT add .:String for JSON subfield in WHERE with LIKE", () => { const ctx = createJsonContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.message LIKE '%error%'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.message LIKE '%error%'", ctx); // WHERE clause should NOT have .:String type hint expect(sql).toContain("like(error.message,"); @@ -702,18 +691,18 @@ describe("ClickHousePrinter", () => { function createTextColumnContext() { const schema = createSchemaRegistry([textColumnSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - describe("SELECT clause", () => { - it("should use text column when selecting bare JSON column", () => { + describe("SELECT clause", () => { + it("should use text column when selecting bare JSON column", () => { const ctx = createTextColumnContext(); const { sql } = printQuery("SELECT output FROM runs", ctx); @@ -791,10 +780,7 @@ describe("ClickHousePrinter", () => { it("should use JSON column for subfield comparison without .:String", () => { const ctx = createTextColumnContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE output.data.name = 'test'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE output.data.name = 'test'", ctx); // Should use the original JSON column, not the text column // And should NOT have .:String in WHERE (breaks the query) @@ -847,10 +833,7 @@ describe("ClickHousePrinter", () => { it("should use text column in both SELECT and WHERE for same query", () => { const ctx = createTextColumnContext(); - const { sql } = printQuery( - "SELECT output FROM runs WHERE output LIKE '%test%'", - ctx - ); + const { sql } = printQuery("SELECT output FROM runs WHERE output LIKE '%test%'", ctx); // SELECT should use text column expect(sql).toContain("output_text AS output"); @@ -991,18 +974,18 @@ describe("ClickHousePrinter", () => { function createDataPrefixContext() { const schema = createSchemaRegistry([dataPrefixSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); - } + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } - describe("SELECT clause", () => { - it("should inject dataPrefix into JSON subfield path", () => { + describe("SELECT clause", () => { + it("should inject dataPrefix into JSON subfield path", () => { const ctx = createDataPrefixContext(); const { sql } = printQuery("SELECT output.message FROM runs", ctx); @@ -1017,9 +1000,7 @@ describe("ClickHousePrinter", () => { // Alias should be output_message, not output_data_message expect(sql).toContain("AS output_message"); expect(sql).not.toContain("AS output_data_message"); - expect(columns).toContainEqual( - expect.objectContaining({ name: "output_message" }) - ); + expect(columns).toContainEqual(expect.objectContaining({ name: "output_message" })); }); it("should handle nested paths with dataPrefix", () => { @@ -1055,10 +1036,7 @@ describe("ClickHousePrinter", () => { describe("WHERE clause", () => { it("should inject dataPrefix into WHERE comparison", () => { const ctx = createDataPrefixContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE output.status = 'success'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE output.status = 'success'", ctx); // Should transform output.status to output.data.status expect(sql).toContain("output.data.status"); @@ -1066,10 +1044,7 @@ describe("ClickHousePrinter", () => { it("should inject dataPrefix into LIKE comparison", () => { const ctx = createDataPrefixContext(); - const { sql } = printQuery( - "SELECT id FROM runs WHERE error.message LIKE '%failed%'", - ctx - ); + const { sql } = printQuery("SELECT id FROM runs WHERE error.message LIKE '%failed%'", ctx); expect(sql).toContain("error.data.message"); }); @@ -1252,7 +1227,9 @@ describe("ClickHousePrinter", () => { describe("Date functions with interval units", () => { it("should output dateAdd with string interval as bare keyword", () => { - const { sql } = printQuery("SELECT dateAdd('day', 7, created_at) AS week_later FROM task_runs"); + const { sql } = printQuery( + "SELECT dateAdd('day', 7, created_at) AS week_later FROM task_runs" + ); expect(sql).toContain("dateAdd(day, 7, created_at)"); expect(sql).not.toContain("'day'"); @@ -1308,7 +1285,9 @@ describe("ClickHousePrinter", () => { }); it("should handle case-insensitive interval units", () => { - const { sql } = printQuery("SELECT dateAdd('DAY', 7, created_at) AS week_later FROM task_runs"); + const { sql } = printQuery( + "SELECT dateAdd('DAY', 7, created_at) AS week_later FROM task_runs" + ); expect(sql).toContain("dateAdd(day, 7, created_at)"); }); @@ -1659,17 +1638,17 @@ describe("Value mapping (valueMap)", () => { function createValueMapContext() { const schema = createSchemaRegistry([statusMappedSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -it("should transform user-friendly value to internal value in equality comparison", () => { + it("should transform user-friendly value to internal value in equality comparison", () => { const ctx = createValueMapContext(); const { sql, params } = printQuery("SELECT * FROM runs WHERE status = 'Completed'", ctx); @@ -1777,17 +1756,17 @@ describe("WHERE transform (whereTransform)", () => { function createPrefixedContext() { const schema = createSchemaRegistry([prefixedIdSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test123" }, - project_id: { op: "eq", value: "proj_test456" }, - environment_id: { op: "eq", value: "env_test789" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test123" }, + project_id: { op: "eq", value: "proj_test456" }, + environment_id: { op: "eq", value: "env_test789" }, + }, + }); + } -it("should strip prefix from value in equality comparison", () => { + it("should strip prefix from value in equality comparison", () => { const ctx = createPrefixedContext(); const { params } = printQuery("SELECT * FROM runs WHERE batch_id = 'batch_abc123'", ctx); @@ -2003,18 +1982,18 @@ describe("Virtual columns", () => { function createVirtualColumnContext() { const schema = createSchemaRegistry([virtualColumnSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("SELECT clause", () => { - it("should expand bare virtual column to expression with alias", () => { + describe("SELECT clause", () => { + it("should expand bare virtual column to expression with alias", () => { const ctx = createVirtualColumnContext(); const { sql } = printQuery("SELECT execution_duration FROM runs", ctx); @@ -2246,18 +2225,18 @@ describe("Expression columns with division (cost/invocation_cost pattern)", () = function createCostExpressionContext() { const schema = createSchemaRegistry([costExpressionSchema]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("WHERE clause with division expression columns", () => { - it("should expand invocation_cost > 100 to (base_cost_in_cents / 100.0) > 100", () => { + describe("WHERE clause with division expression columns", () => { + it("should expand invocation_cost > 100 to (base_cost_in_cents / 100.0) > 100", () => { const ctx = createCostExpressionContext(); const { sql } = printQuery("SELECT * FROM runs WHERE invocation_cost > 100", ctx); @@ -2392,18 +2371,18 @@ describe("Column metadata", () => { function createMetadataTestContext() { const schema = createSchemaRegistry([schemaWithRenderTypes]); - return createPrinterContext({ - schema, - enforcedWhereClause: { - organization_id: { op: "eq", value: "org_test" }, - project_id: { op: "eq", value: "proj_test" }, - environment_id: { op: "eq", value: "env_test" }, - }, - }); -} + return createPrinterContext({ + schema, + enforcedWhereClause: { + organization_id: { op: "eq", value: "org_test" }, + project_id: { op: "eq", value: "proj_test" }, + environment_id: { op: "eq", value: "env_test" }, + }, + }); + } -describe("Basic column metadata", () => { - it("should return column metadata for simple field references", () => { + describe("Basic column metadata", () => { + it("should return column metadata for simple field references", () => { const ctx = createMetadataTestContext(); const { columns } = printQuery("SELECT run_id, created_at FROM runs", ctx); @@ -2835,16 +2814,10 @@ describe("Basic column metadata", () => { it("should throw for invalid format type", () => { const ctx = createMetadataTestContext(); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", ctx); }).toThrow(QueryError); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 'invalid') FROM runs", ctx); }).toThrow(/Unknown format type/); }); @@ -2861,16 +2834,10 @@ describe("Basic column metadata", () => { it("should throw when second argument is not a string literal", () => { const ctx = createMetadataTestContext(); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 123) FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 123) FROM runs", ctx); }).toThrow(QueryError); expect(() => { - printQuery( - "SELECT prettyFormat(usage_duration_ms, 123) FROM runs", - ctx - ); + printQuery("SELECT prettyFormat(usage_duration_ms, 123) FROM runs", ctx); }).toThrow(/must be a string literal/); }); @@ -2890,10 +2857,7 @@ describe("Basic column metadata", () => { it("should auto-populate format from customRenderType when not explicitly set", () => { const ctx = createMetadataTestContext(); - const { columns } = printQuery( - "SELECT usage_duration_ms, cost_in_cents FROM runs", - ctx - ); + const { columns } = printQuery("SELECT usage_duration_ms, cost_in_cents FROM runs", ctx); expect(columns).toHaveLength(2); // customRenderType should auto-populate format @@ -3359,7 +3323,9 @@ describe("Internal-only column blocking", () => { }); it("should allow grouping by exposed tenant column", () => { - const { sql } = printQuery("SELECT organization_id, count(*) FROM task_runs GROUP BY organization_id"); + const { sql } = printQuery( + "SELECT organization_id, count(*) FROM task_runs GROUP BY organization_id" + ); expect(sql).toContain("GROUP BY organization_id"); }); }); @@ -3507,9 +3473,9 @@ describe("Required Filters", () => { `); // All should work without errors - expect(sql).toContain("friendly_id"); // run_id maps to friendly_id + expect(sql).toContain("friendly_id"); // run_id maps to friendly_id expect(sql).toContain("status"); - expect(sql).toContain("created_at"); // triggered_at maps to created_at + expect(sql).toContain("created_at"); // triggered_at maps to created_at expect(sql).toContain("cost_in_cents"); // total_cost is a virtual column }); }); @@ -3681,9 +3647,9 @@ describe("timeBucket()", () => { describe("error handling", () => { it("should throw when timeBucket() is called with arguments", () => { - expect(() => - printTimeBucketQuery("SELECT timeBucket(triggered_at) FROM runs") - ).toThrow("timeBucket() does not accept arguments"); + expect(() => printTimeBucketQuery("SELECT timeBucket(triggered_at) FROM runs")).toThrow( + "timeBucket() does not accept arguments" + ); }); it("should throw when table has no timeConstraint", () => { @@ -3828,9 +3794,7 @@ describe("timeBucket()", () => { timeRange: threeMinuteRange, }); - const ast = parseTSQLSelect( - "SELECT timeBucket(), count() FROM metrics GROUP BY timeBucket" - ); + const ast = parseTSQLSelect("SELECT timeBucket(), count() FROM metrics GROUP BY timeBucket"); const { sql } = printToClickHouse(ast, ctx); // Custom thresholds: under 10 min → 10 SECOND (not the global 5 SECOND) @@ -3855,9 +3819,7 @@ describe("timeBucket()", () => { timeRange: threeMinuteRange, }); - const ast = parseTSQLSelect( - "SELECT timeBucket(), count() FROM runs GROUP BY timeBucket" - ); + const ast = parseTSQLSelect("SELECT timeBucket(), count() FROM runs GROUP BY timeBucket"); const { sql } = printToClickHouse(ast, ctx); // Global default: under 5 min → 5 SECOND diff --git a/internal-packages/tsql/src/query/printer.ts b/internal-packages/tsql/src/query/printer.ts index d45002f6715..a4b50ca8540 100644 --- a/internal-packages/tsql/src/query/printer.ts +++ b/internal-packages/tsql/src/query/printer.ts @@ -741,7 +741,9 @@ export class ClickHousePrinter { } // Set format hint from prettyFormat() or auto-populate from customRenderType - const sourceWithFormat = sourceColumn as (Partial & { format?: ColumnFormatType }) | null; + const sourceWithFormat = sourceColumn as + | (Partial & { format?: ColumnFormatType }) + | null; if (sourceWithFormat?.format) { metadata.format = sourceWithFormat.format; } else if (sourceColumn?.customRenderType) { @@ -2990,10 +2992,7 @@ export class ClickHousePrinter { private visitCallArgs(functionName: string, args: Expression[]): string[] { const lowerName = functionName.toLowerCase(); - if ( - ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) && - args.length > 0 - ) { + if (ClickHousePrinter.DATE_FUNCTIONS_WITH_INTERVAL_UNIT.has(lowerName) && args.length > 0) { const firstArg = args[0]; const intervalUnit = this.extractIntervalUnit(firstArg); diff --git a/internal-packages/tsql/src/query/results.test.ts b/internal-packages/tsql/src/query/results.test.ts index 49216992cf1..d4670ee19f6 100644 --- a/internal-packages/tsql/src/query/results.test.ts +++ b/internal-packages/tsql/src/query/results.test.ts @@ -94,9 +94,7 @@ describe("transformResults", () => { }); it("should not modify columns without valueMap", () => { - const rows = [ - { id: "run_1", status: "COMPLETED_SUCCESSFULLY", task_identifier: "my-task" }, - ]; + const rows = [{ id: "run_1", status: "COMPLETED_SUCCESSFULLY", task_identifier: "my-task" }]; const transformed = transformResults(rows, [taskRunsSchema]); @@ -231,4 +229,3 @@ describe("createResultTransformer", () => { expect(transformed).toBe(rows); }); }); - diff --git a/internal-packages/tsql/src/query/results.ts b/internal-packages/tsql/src/query/results.ts index 8edeb8f54e4..c26702e5837 100644 --- a/internal-packages/tsql/src/query/results.ts +++ b/internal-packages/tsql/src/query/results.ts @@ -198,4 +198,3 @@ export function createResultTransformer( return rows.map((row) => transformRow(row, columnTransformMaps, options.fieldMappings)); }; } - diff --git a/internal-packages/tsql/src/query/schema.ts b/internal-packages/tsql/src/query/schema.ts index 615d112c2f1..9a1e2d2ddfe 100644 --- a/internal-packages/tsql/src/query/schema.ts +++ b/internal-packages/tsql/src/query/schema.ts @@ -869,7 +869,11 @@ export function sanitizeErrorMessage(message: string, schemas: TableSchema[]): s // Collect tenant column names to strip (global tables have no tenant columns) const tenantCols = table.tenantColumns; if (tenantCols) { - columnsToStrip.push(tenantCols.organizationId, tenantCols.projectId, tenantCols.environmentId); + columnsToStrip.push( + tenantCols.organizationId, + tenantCols.projectId, + tenantCols.environmentId + ); } // Collect required filter columns to strip diff --git a/internal-packages/tsql/src/query/security.test.ts b/internal-packages/tsql/src/query/security.test.ts index 2fcca477770..8c18981ee4c 100644 --- a/internal-packages/tsql/src/query/security.test.ts +++ b/internal-packages/tsql/src/query/security.test.ts @@ -614,11 +614,11 @@ describe("Multi-join Tenant Guard Qualification", () => { // The guards should be table-qualified to prevent binding to the wrong table // Look for pattern like: r.organization_id and e.organization_id (with table alias prefix) // The exact format in ClickHouse SQL is just "alias.column" after resolution - + // Count qualified organization_id references (should have table prefixes) // In the WHERE clause, we should see both r.organization_id and e.organization_id const whereClause = sql.substring(sql.indexOf("WHERE")); - + // Both tables should have their own qualified tenant guards // The pattern should be: table_alias.organization_id for each table expect(whereClause).toMatch(/\br\b[^,]*organization_id/); @@ -633,7 +633,7 @@ describe("Multi-join Tenant Guard Qualification", () => { `); const whereClause = sql.substring(sql.indexOf("WHERE")); - + // Both tables should have qualified guards expect(whereClause).toMatch(/\br\b[^,]*organization_id/); expect(whereClause).toMatch(/\be\b[^,]*organization_id/); @@ -648,7 +648,7 @@ describe("Multi-join Tenant Guard Qualification", () => { `); const whereClause = sql.substring(sql.indexOf("WHERE")); - + // All three table aliases should have qualified guards expect(whereClause).toMatch(/\br\b[^,]*organization_id/); expect(whereClause).toMatch(/\be1\b[^,]*organization_id/); @@ -667,13 +667,13 @@ describe("Multi-join Tenant Guard Qualification", () => { // This ensures each table gets its own guard, not shared/ambiguous references const orgIdPattern = /(\w+)\.organization_id/g; const matches = [...sql.matchAll(orgIdPattern)]; - const tableAliases = matches.map(m => m[1]); - + const tableAliases = matches.map((m) => m[1]); + // Should have at least 2 different table aliases for organization_id // (one for task_runs alias 'r' and one for task_events alias 'e') expect(tableAliases).toContain("r"); expect(tableAliases).toContain("e"); - + // Both should use the same tenant value (parameterized) expect(Object.values(params)).toContain("org_tenant1"); }); diff --git a/internal-packages/tsql/src/query/timings.ts b/internal-packages/tsql/src/query/timings.ts index d868fe06671..32384e7fbe0 100644 --- a/internal-packages/tsql/src/query/timings.ts +++ b/internal-packages/tsql/src/query/timings.ts @@ -8,22 +8,22 @@ * - Node.js <18 via perf_hooks module */ function getPerformanceNow(): number { - // Check for global performance (Node.js 18+ or browser) - if (typeof globalThis !== 'undefined' && 'performance' in globalThis) { - const perf = (globalThis as any).performance; - if (perf && typeof perf.now === 'function') { - return perf.now(); - } + // Check for global performance (Node.js 18+ or browser) + if (typeof globalThis !== "undefined" && "performance" in globalThis) { + const perf = (globalThis as any).performance; + if (perf && typeof perf.now === "function") { + return perf.now(); } - - // Fallback to Date.now() if performance API is not available - // Note: This is less precise but works everywhere - return Date.now(); + } + + // Fallback to Date.now() if performance API is not available + // Note: This is less precise but works everywhere + return Date.now(); } export interface QueryTiming { - key: string; // Key identifying the timing measurement - time: number; // Time in seconds + key: string; // Key identifying the timing measurement + time: number; // Time in seconds } const TIMING_DECIMAL_PLACES = 3; // round to milliseconds @@ -31,77 +31,79 @@ const TIMING_DECIMAL_PLACES = 3; // round to milliseconds // Not thread safe. // See trends_query_runner for an example of how to use for multithreaded queries export class TSQLTimings { - // Completed time in seconds for different parts of the TSQL query - timings: Record = {}; + // Completed time in seconds for different parts of the TSQL query + timings: Record = {}; - // Used for housekeeping - private _timingPointer: string; - private _timingStarts: Record = {}; + // Used for housekeeping + private _timingPointer: string; + private _timingStarts: Record = {}; - constructor(_timingPointer: string = '.') { - this._timingPointer = _timingPointer; - this._timingStarts[this._timingPointer] = this.perfCounter(); - } + constructor(_timingPointer: string = ".") { + this._timingPointer = _timingPointer; + this._timingStarts[this._timingPointer] = this.perfCounter(); + } - cloneForSubquery(seriesIndex: number): TSQLTimings { - return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`); - } + cloneForSubquery(seriesIndex: number): TSQLTimings { + return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`); + } - clearTimings(): void { - this.timings = {}; - } + clearTimings(): void { + this.timings = {}; + } - /** - * Measure execution time of a function. - * Usage: timings.measure('operation', () => { ... }); - */ - measure(key: string, fn: () => T): T { - const lastKey = this._timingPointer; - const fullKey = `${this._timingPointer}/${key}`; - this._timingPointer = fullKey; - this._timingStarts[fullKey] = this.perfCounter(); + /** + * Measure execution time of a function. + * Usage: timings.measure('operation', () => { ... }); + */ + measure(key: string, fn: () => T): T { + const lastKey = this._timingPointer; + const fullKey = `${this._timingPointer}/${key}`; + this._timingPointer = fullKey; + this._timingStarts[fullKey] = this.perfCounter(); - try { - return fn(); - } finally { - const duration = (this.perfCounter() - this._timingStarts[fullKey]) / 1000; // Convert to seconds - this.timings[fullKey] = (this.timings[fullKey] || 0.0) + duration; - delete this._timingStarts[fullKey]; - this._timingPointer = lastKey; - } + try { + return fn(); + } finally { + const duration = (this.perfCounter() - this._timingStarts[fullKey]) / 1000; // Convert to seconds + this.timings[fullKey] = (this.timings[fullKey] || 0.0) + duration; + delete this._timingStarts[fullKey]; + this._timingPointer = lastKey; } + } - /** - * Get performance counter in milliseconds (Node.js equivalent of perf_counter) - */ - private perfCounter(): number { - return getPerformanceNow(); - } + /** + * Get performance counter in milliseconds (Node.js equivalent of perf_counter) + */ + private perfCounter(): number { + return getPerformanceNow(); + } - toDict(): Record { - const timings = { ...this.timings }; - // Process in reverse order to handle nested timings correctly - const keys = Object.keys(this._timingStarts).reverse(); - for (const key of keys) { - const start = this._timingStarts[key]; - const elapsed = (this.perfCounter() - start) / 1000; // Convert to seconds - timings[key] = this.round((timings[key] || 0.0) + elapsed); - } - return timings; + toDict(): Record { + const timings = { ...this.timings }; + // Process in reverse order to handle nested timings correctly + const keys = Object.keys(this._timingStarts).reverse(); + for (const key of keys) { + const start = this._timingStarts[key]; + const elapsed = (this.perfCounter() - start) / 1000; // Convert to seconds + timings[key] = this.round((timings[key] || 0.0) + elapsed); } + return timings; + } - toList(backOutStack: boolean = true): QueryTiming[] { - const timingDict = backOutStack ? this.toDict() : this.timings; - return Object.entries(timingDict).map(([key, time]) => ({ - key: key, - time: this.round(time), - })); - } + toList(backOutStack: boolean = true): QueryTiming[] { + const timingDict = backOutStack ? this.toDict() : this.timings; + return Object.entries(timingDict).map(([key, time]) => ({ + key: key, + time: this.round(time), + })); + } - /** - * Round to specified decimal places (milliseconds precision) - */ - private round(value: number): number { - return Math.round(value * Math.pow(10, TIMING_DECIMAL_PLACES)) / Math.pow(10, TIMING_DECIMAL_PLACES); - } + /** + * Round to specified decimal places (milliseconds precision) + */ + private round(value: number): number { + return ( + Math.round(value * Math.pow(10, TIMING_DECIMAL_PLACES)) / Math.pow(10, TIMING_DECIMAL_PLACES) + ); + } } diff --git a/internal-packages/zod-worker/package.json b/internal-packages/zod-worker/package.json index 352fa945293..190dd1facd3 100644 --- a/internal-packages/zod-worker/package.json +++ b/internal-packages/zod-worker/package.json @@ -19,4 +19,4 @@ "scripts": { "typecheck": "tsc --noEmit" } -} \ No newline at end of file +} diff --git a/packages/build/src/extensions/core/neonSyncEnvVars.ts b/packages/build/src/extensions/core/neonSyncEnvVars.ts index 2e0dde31431..257eaed5277 100644 --- a/packages/build/src/extensions/core/neonSyncEnvVars.ts +++ b/packages/build/src/extensions/core/neonSyncEnvVars.ts @@ -85,8 +85,7 @@ export function syncNeonEnvVars(options?: { envVarPrefix?: string; }): BuildExtension { const sync = syncEnvVars(async (ctx) => { - const projectId = - options?.projectId ?? process.env.NEON_PROJECT_ID ?? ctx.env.NEON_PROJECT_ID; + const projectId = options?.projectId ?? process.env.NEON_PROJECT_ID ?? ctx.env.NEON_PROJECT_ID; const neonAccessToken = options?.neonAccessToken ?? process.env.NEON_ACCESS_TOKEN ?? ctx.env.NEON_ACCESS_TOKEN; const branch = options?.branch ?? ctx.branch; diff --git a/packages/build/src/extensions/core/syncSupabaseEnvVars.ts b/packages/build/src/extensions/core/syncSupabaseEnvVars.ts index f373c1e633f..d90d27a7634 100644 --- a/packages/build/src/extensions/core/syncSupabaseEnvVars.ts +++ b/packages/build/src/extensions/core/syncSupabaseEnvVars.ts @@ -133,9 +133,7 @@ export function syncSupabaseEnvVars(options?: { // Step 1: List branches const branchesUrl = `https://api.supabase.com/v1/projects/${projectId}/branches`; - const [branchesFetchError, branchesResponse] = await tryCatch( - fetch(branchesUrl, { headers }) - ); + const [branchesFetchError, branchesResponse] = await tryCatch(fetch(branchesUrl, { headers })); if (branchesFetchError) { throw new Error( @@ -170,9 +168,7 @@ export function syncSupabaseEnvVars(options?: { targetBranch = branches.find((b) => b.is_default); if (!targetBranch) { - throw new Error( - "syncSupabaseEnvVars: no default Supabase branch found for the project." - ); + throw new Error("syncSupabaseEnvVars: no default Supabase branch found for the project."); } } else { if (!branch) { @@ -218,9 +214,7 @@ export function syncSupabaseEnvVars(options?: { // Step 4: Get API keys for the branch project const apiKeysUrl = `https://api.supabase.com/v1/projects/${branchDetail.ref}/api-keys`; - const [apiKeysFetchError, apiKeysResponse] = await tryCatch( - fetch(apiKeysUrl, { headers }) - ); + const [apiKeysFetchError, apiKeysResponse] = await tryCatch(fetch(apiKeysUrl, { headers })); let anonKey: string | undefined; let serviceRoleKey: string | undefined; diff --git a/packages/build/src/extensions/core/vercelSyncEnvVars.ts b/packages/build/src/extensions/core/vercelSyncEnvVars.ts index ce79841e66d..a3ff02ef51f 100644 --- a/packages/build/src/extensions/core/vercelSyncEnvVars.ts +++ b/packages/build/src/extensions/core/vercelSyncEnvVars.ts @@ -36,7 +36,7 @@ export function syncVercelEnvVars(options?: { process.env.VERCEL_PREVIEW_BRANCH ?? ctx.env.VERCEL_PREVIEW_BRANCH ?? ctx.branch; - const isVercelEnv = !!(ctx.env.VERCEL); + const isVercelEnv = !!ctx.env.VERCEL; if (!projectId) { throw new Error( diff --git a/packages/build/src/internal/additionalFiles.ts b/packages/build/src/internal/additionalFiles.ts index 57a746c36b6..a9e59363cad 100644 --- a/packages/build/src/internal/additionalFiles.ts +++ b/packages/build/src/internal/additionalFiles.ts @@ -1,10 +1,6 @@ import { BuildManifest } from "@trigger.dev/core/v3"; import { BuildContext } from "@trigger.dev/core/v3/build"; -import { - copyMatcherResults, - findFilesByMatchers, - type MatcherResult, -} from "./copyFiles.js"; +import { copyMatcherResults, findFilesByMatchers, type MatcherResult } from "./copyFiles.js"; export type AdditionalFilesOptions = { files: string[]; diff --git a/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml b/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml index 8b757b29a17..3186f3f0795 100644 --- a/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml +++ b/packages/cli-v3/e2e/fixtures/monorepo-react-email/.yarnrc.yml @@ -1 +1 @@ -nodeLinker: node-modules \ No newline at end of file +nodeLinker: node-modules diff --git a/packages/cli-v3/e2e/utils.ts b/packages/cli-v3/e2e/utils.ts index 73530208c7e..077ac3cb77f 100644 --- a/packages/cli-v3/e2e/utils.ts +++ b/packages/cli-v3/e2e/utils.ts @@ -433,14 +433,14 @@ function parseAttributes(attributes: any): ExecuteTaskTraceEvent["attributes"] { acc[attribute.key] = isStringValue(attribute.value) ? attribute.value.stringValue : isIntValue(attribute.value) - ? Number(attribute.value.intValue) - : isDoubleValue(attribute.value) - ? attribute.value.doubleValue - : isBoolValue(attribute.value) - ? attribute.value.boolValue - : isBytesValue(attribute.value) - ? binaryToHex(attribute.value.bytesValue) - : undefined; + ? Number(attribute.value.intValue) + : isDoubleValue(attribute.value) + ? attribute.value.doubleValue + : isBoolValue(attribute.value) + ? attribute.value.boolValue + : isBytesValue(attribute.value) + ? binaryToHex(attribute.value.bytesValue) + : undefined; return acc; }, {}); diff --git a/packages/cli-v3/src/apiClient.ts b/packages/cli-v3/src/apiClient.ts index 7cf969a7bde..ac6c3ba9855 100644 --- a/packages/cli-v3/src/apiClient.ts +++ b/packages/cli-v3/src/apiClient.ts @@ -320,11 +320,7 @@ export class CliApiClient { ); } - async archiveBranch( - projectRef: string, - env: UpsertBranchRequestBody["env"], - branch: string - ) { + async archiveBranch(projectRef: string, env: UpsertBranchRequestBody["env"], branch: string) { if (!this.accessToken) { throw new Error("archiveBranch: No access token"); } diff --git a/packages/cli-v3/src/build/bundleSkills.ts b/packages/cli-v3/src/build/bundleSkills.ts index 8533d254c72..9ddcf81f273 100644 --- a/packages/cli-v3/src/build/bundleSkills.ts +++ b/packages/cli-v3/src/build/bundleSkills.ts @@ -39,9 +39,7 @@ export type CopySkillFoldersOptions = { * and indirectly by the deploy path (via `bundleSkills` which discovers * skills via its own indexer pass first, then delegates here). */ -export async function copySkillFolders( - options: CopySkillFoldersOptions -): Promise { +export async function copySkillFolders(options: CopySkillFoldersOptions): Promise { const { skills, destinationRoot, workingDir, logger } = options; if (skills.length === 0) { @@ -49,9 +47,7 @@ export async function copySkillFolders( } for (const skill of skills) { - const callerDir = skill.filePath - ? resolvePath(workingDir, skill.filePath, "..") - : workingDir; + const callerDir = skill.filePath ? resolvePath(workingDir, skill.filePath, "..") : workingDir; const sourcePath = isAbsolute(skill.sourcePath) ? skill.sourcePath : resolvePath(callerDir, skill.sourcePath); @@ -101,9 +97,7 @@ export async function copySkillFolders( * No `trigger.config.ts` changes required — discovery is side-effect * based, same mechanism as task/prompt registration. */ -export async function bundleSkills( - options: BundleSkillsOptions -): Promise { +export async function bundleSkills(options: BundleSkillsOptions): Promise { const { buildManifest, buildManifestPath, workingDir, env, logger } = options; let skills: SkillManifest[]; diff --git a/packages/cli-v3/src/build/manifests.ts b/packages/cli-v3/src/build/manifests.ts index f1188233a5f..5a1aa541a66 100644 --- a/packages/cli-v3/src/build/manifests.ts +++ b/packages/cli-v3/src/build/manifests.ts @@ -54,7 +54,10 @@ export async function copyManifestToDir( */ async function computeFileHash(filePath: string): Promise { const contents = await readFile(filePath); - return createHash("sha256").update(contents as Uint8Array).digest("hex").slice(0, 16); + return createHash("sha256") + .update(contents as Uint8Array) + .digest("hex") + .slice(0, 16); } /** diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 1ac161d3e4a..c0d5d4627dc 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -1214,10 +1214,10 @@ async function handleNativeBuildServerDeploy({ level === "error" ? chalk.bold(chalkError(message)) : level === "warn" - ? chalkWarning(message) - : level === "debug" - ? chalkGrey(message) - : message; + ? chalkWarning(message) + : level === "debug" + ? chalkGrey(message) + : message; // We use console.log here instead of clack's logger as the current version does not support changing the line spacing. // And the logs look verbose with the default spacing. diff --git a/packages/cli-v3/src/commands/dev.ts b/packages/cli-v3/src/commands/dev.ts index f1d4e30d88d..3915cadc209 100644 --- a/packages/cli-v3/src/commands/dev.ts +++ b/packages/cli-v3/src/commands/dev.ts @@ -7,7 +7,12 @@ import { ResolvedConfig } from "@trigger.dev/core/v3/build"; import { Command, Option as CommandOption } from "commander"; import { z } from "zod"; import { CliApiClient } from "../apiClient.js"; -import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js"; +import { + CommonCommandOptions, + commonOptions, + handleTelemetry, + wrapCommandAction, +} from "../cli/common.js"; import { watchConfig } from "../config.js"; import { DevSessionInstance, startDevSession } from "../dev/devSession.js"; import { createLockFile } from "../dev/lock.js"; @@ -214,7 +219,8 @@ export async function devCommand(options: DevCommandOptions) { ); } else { logger.log( - `${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${authorization.error + `${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${ + authorization.error }` ); } @@ -253,9 +259,9 @@ async function startDev(options: StartDevOptions) { const notificationPromise = options.skipPlatformNotifications ? undefined : fetchPlatformNotification({ - apiClient, - projectRef: options.projectRef, - }); + apiClient, + projectRef: options.projectRef, + }); await printStandloneInitialBanner(true, options.profile); @@ -276,7 +282,6 @@ async function startDev(options: StartDevOptions) { printDevBanner(displayedUpdateMessage); - if (envVars.TRIGGER_PROJECT_REF) { logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF }); } @@ -342,7 +347,7 @@ async function startDev(options: StartDevOptions) { devInstance = await bootDevSession(watcher.config); - const waitUntilExit = async () => { }; + const waitUntilExit = async () => {}; return { watcher, @@ -371,7 +376,6 @@ async function devArchiveCommand(dir: string, options: unknown) { ); } - async function archiveDevBranchCommand(dir: string, options: DevArchiveCommandOptions) { intro(`Archiving dev branch`); @@ -429,11 +433,7 @@ async function archiveDevBranchCommand(dir: string, options: DevArchiveCommandOp return result; } -async function archiveDevBranch( - authorization: LoginResultOk, - branch: string, - project: string -) { +async function archiveDevBranch(authorization: LoginResultOk, branch: string, project: string) { const apiClient = new CliApiClient(authorization.auth.apiUrl, authorization.auth.accessToken); const result = await apiClient.archiveBranch(project, "development", branch); diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index 8513c79854c..f13f88d23ab 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -107,7 +107,10 @@ Examples: "Additional arguments to pass to the package manager, accepts CSV for multiple args" ) .option("-y, --yes", "Skip all prompts and use defaults (requires --project-ref)") - .option("--no-browser", "Don't automatically open the browser during login; print the URL only") + .option( + "--no-browser", + "Don't automatically open the browser during login; print the URL only" + ) ) .addOption( new CommandOption( diff --git a/packages/cli-v3/src/commands/skills.ts b/packages/cli-v3/src/commands/skills.ts index b2d4613b02d..f2290f3f057 100644 --- a/packages/cli-v3/src/commands/skills.ts +++ b/packages/cli-v3/src/commands/skills.ts @@ -63,10 +63,7 @@ export function configureSkillsCommand(program: Command) { "Choose the target (or targets) to install the Trigger.dev skills into. Native install is supported for: " + targets.join(", ") ) - .option( - "-y, --yes", - "Install all available skills for the selected targets without prompting" - ) + .option("-y, --yes", "Install all available skills for the selected targets without prompting") .option( "-l, --log-level ", "The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.", @@ -85,13 +82,18 @@ export function configureSkillsCommand(program: Command) { } export async function installSkillsCommand(options: unknown) { - return await wrapCommandAction("installSkillsCommand", SkillsCommandOptions, options, async (opts) => { - if (opts.logLevel) { - logger.loggerLevel = opts.logLevel; - } + return await wrapCommandAction( + "installSkillsCommand", + SkillsCommandOptions, + options, + async (opts) => { + if (opts.logLevel) { + logger.loggerLevel = opts.logLevel; + } - return await _installSkillsCommand(opts); - }); + return await _installSkillsCommand(opts); + } + ); } /** @@ -107,9 +109,9 @@ export async function installSkillsCommand(options: unknown) { * that resolves correctly both when bundled (`/dist/esm`) and from source * (`/src`, run via tsx in dev/tests). */ -export async function resolveBundledPackageJSON(startDir: string = sourceDir): Promise< - string | null -> { +export async function resolveBundledPackageJSON( + startDir: string = sourceDir +): Promise { let searchDir = startDir; for (let i = 0; i < 10; i++) { @@ -340,7 +342,9 @@ async function installSkillsForTarget( if (targetName === "unsupported") { // This should not happen as unsupported targets are handled separately, // but if it does, provide helpful output. - log.message(`${chalk.yellow("⚠")} Skipping unsupported target - see manual configuration above`); + log.message( + `${chalk.yellow("⚠")} Skipping unsupported target - see manual configuration above` + ); return; } @@ -427,7 +431,9 @@ type SkillsPointer = { file: string; mode: "region" | "dedicated" }; * shared (a marked block is upserted so we never clobber other content); "dedicated" * files are ours to own and overwrite. */ -function resolveSkillsPointerForTarget(targetName: (typeof targets)[number]): SkillsPointer | undefined { +function resolveSkillsPointerForTarget( + targetName: (typeof targets)[number] +): SkillsPointer | undefined { switch (targetName) { case "claude-code": { return { file: "CLAUDE.md", mode: "region" }; diff --git a/packages/cli-v3/src/config.ts b/packages/cli-v3/src/config.ts index 1421cafe063..5b5dbfdfad7 100644 --- a/packages/cli-v3/src/config.ts +++ b/packages/cli-v3/src/config.ts @@ -118,8 +118,8 @@ export function configPlugin(resolvedConfig: ResolvedConfig): esbuild.Plugin | u ? $mod.exports.default.$args[0] : $mod.exports.default : $mod.exports.config?.$type === "function-call" - ? $mod.exports.config.$args[0] - : $mod.exports.config; + ? $mod.exports.config.$args[0] + : $mod.exports.config; options.build = {}; @@ -177,8 +177,8 @@ async function resolveConfig( const workingDir = result.configFile ? dirname(result.configFile) : packageJsonPath - ? dirname(packageJsonPath) - : cwd; + ? dirname(packageJsonPath) + : cwd; const config = "config" in result.config ? (result.config.config as TriggerConfig) : result.config; diff --git a/packages/cli-v3/src/dev/devOutput.ts b/packages/cli-v3/src/dev/devOutput.ts index 1ea302102f5..93c6beed5c7 100644 --- a/packages/cli-v3/src/dev/devOutput.ts +++ b/packages/cli-v3/src/dev/devOutput.ts @@ -92,7 +92,9 @@ export function startDevOutput(options: DevOutputOptions) { const runsLink = chalkLink(cliLink("View runs", runsUrl)); const runtime = chalkGrey(`[${worker.build.runtime}]`); - const workerStarted = chalkGrey(`Local worker ready on branch: ${branch ?? DEFAULT_DEV_BRANCH}`); + const workerStarted = chalkGrey( + `Local worker ready on branch: ${branch ?? DEFAULT_DEV_BRANCH}` + ); const workerVersion = chalkWorker(worker.serverWorker!.version); logParts.push(workerStarted, runtime, arrow, workerVersion); @@ -196,8 +198,8 @@ export function startDevOutput(options: DevOutputOptions) { !completion.ok && completion.skippedRetrying ? " (retrying skipped)" : !completion.ok && completion.retry !== undefined - ? ` (retrying in ${completion.retry.delay}ms)` - : "" + ? ` (retrying in ${completion.retry.delay}ms)` + : "" ); const resultText = !completion.ok @@ -211,8 +213,8 @@ export function startDevOutput(options: DevOutputOptions) { const errorText = !completion.ok ? formatErrorLog(completion.error) : "retry" in completion - ? `retry in ${completion.retry}ms` - : ""; + ? `retry in ${completion.retry}ms` + : ""; const elapsedText = chalkGrey( `(${formatDurationMilliseconds(durationMs, { style: "short" })})` diff --git a/packages/cli-v3/src/dev/devSupervisor.ts b/packages/cli-v3/src/dev/devSupervisor.ts index d2ca25928e0..9521af8e028 100644 --- a/packages/cli-v3/src/dev/devSupervisor.ts +++ b/packages/cli-v3/src/dev/devSupervisor.ts @@ -1,5 +1,12 @@ import { spawn, type ChildProcess } from "node:child_process"; -import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from "node:fs"; +import { + readFileSync, + writeFileSync, + renameSync, + unlinkSync, + existsSync, + mkdirSync, +} from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { setTimeout as awaitTimeout } from "node:timers/promises"; @@ -83,7 +90,7 @@ class DevSupervisor implements WorkerRuntime { private activeRunsPath?: string; private watchdogPidPath?: string; - constructor(public readonly options: WorkerRuntimeOptions) { } + constructor(public readonly options: WorkerRuntimeOptions) {} async init(): Promise { logger.debug("[DevSupervisor] initialized worker runtime", { options: this.options }); @@ -124,10 +131,10 @@ class DevSupervisor implements WorkerRuntime { : false; const maxPoolSize = - typeof processKeepAlive === "object" ? processKeepAlive.devMaxPoolSize ?? 25 : 25; + typeof processKeepAlive === "object" ? (processKeepAlive.devMaxPoolSize ?? 25) : 25; const maxExecutionsPerProcess = - typeof processKeepAlive === "object" ? processKeepAlive.maxExecutionsPerProcess ?? 50 : 50; + typeof processKeepAlive === "object" ? (processKeepAlive.maxExecutionsPerProcess ?? 50) : 50; if (enableProcessReuse) { logger.debug("[DevSupervisor] Enabling process reuse", { @@ -288,10 +295,10 @@ class DevSupervisor implements WorkerRuntime { // Clean up files try { if (this.activeRunsPath) unlinkSync(this.activeRunsPath); - } catch { } + } catch {} try { if (this.watchdogPidPath) unlinkSync(this.watchdogPidPath); - } catch { } + } catch {} } #updateActiveRunsFile() { @@ -535,7 +542,6 @@ class DevSupervisor implements WorkerRuntime { taskRunProcessPool: this.taskRunProcessPool, cwd, onFinished: () => { - logger.debug("[DevSupervisor] Run finished", { runId: message.run.friendlyId }); //stop the run controller, and remove it @@ -910,4 +916,3 @@ function generateValidationIssueMessage( } } } - diff --git a/packages/cli-v3/src/entryPoints/dev-run-controller.ts b/packages/cli-v3/src/entryPoints/dev-run-controller.ts index ffa4228cbcd..cf4038c1265 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-controller.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-controller.ts @@ -132,7 +132,6 @@ export class DevRunController { }); } - // This should only be used when we're already executing a run. Attempt number changes are not allowed. private updateRunPhase(run: Run, snapshot: Snapshot) { if (this.state.phase !== "RUN") { diff --git a/packages/cli-v3/src/entryPoints/managed-index-worker.ts b/packages/cli-v3/src/entryPoints/managed-index-worker.ts index 4923beb3af9..52e24cdd876 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-worker.ts @@ -204,8 +204,8 @@ await sendMessageInCatalog( typeof processKeepAlive === "object" ? processKeepAlive : typeof processKeepAlive === "boolean" - ? { enabled: processKeepAlive } - : undefined, + ? { enabled: processKeepAlive } + : undefined, timings, }, importErrors, diff --git a/packages/cli-v3/src/executions/taskRunProcess.ts b/packages/cli-v3/src/executions/taskRunProcess.ts index 6816b2e24f2..5db616ba859 100644 --- a/packages/cli-v3/src/executions/taskRunProcess.ts +++ b/packages/cli-v3/src/executions/taskRunProcess.ts @@ -213,9 +213,7 @@ export class TaskRunProcess { // expires — surfacing as TIMED_OUT/MAX_DURATION_EXCEEDED with empty attempts. Reject // any pending attempts now and gracefully terminate the worker so OTEL gets a flush // window before SIGKILL. - this.#rejectPendingAttempts( - new UncaughtExceptionError(message.error, message.origin) - ); + this.#rejectPendingAttempts(new UncaughtExceptionError(message.error, message.origin)); await this.#gracefullyTerminate(this.options.gracefulTerminationTimeoutInMs); }, @@ -317,11 +315,7 @@ export class TaskRunProcess { // @ts-expect-error - rejecter is assigned in the promise constructor above rejecter( - new UnexpectedExitError( - -1, - null, - "Child process is not connected, cannot execute task run" - ) + new UnexpectedExitError(-1, null, "Child process is not connected, cannot execute task run") ); } diff --git a/packages/cli-v3/src/mcp/config.ts b/packages/cli-v3/src/mcp/config.ts index 3e53137b9b9..31b5c6ec4dd 100644 --- a/packages/cli-v3/src/mcp/config.ts +++ b/packages/cli-v3/src/mcp/config.ts @@ -139,14 +139,12 @@ export const toolsMetadata = { whoami: { name: "whoami", title: "Who Am I", - description: - "Show the current authenticated user, active CLI profile, email, and API URL.", + description: "Show the current authenticated user, active CLI profile, email, and API URL.", }, list_profiles: { name: "list_profiles", title: "List Profiles", - description: - "List all configured CLI profiles. Shows which profile is currently active.", + description: "List all configured CLI profiles. Shows which profile is currently active.", }, switch_profile: { name: "switch_profile", diff --git a/packages/cli-v3/src/mcp/formatters.ts b/packages/cli-v3/src/mcp/formatters.ts index 131124cc9a4..539762257d2 100644 --- a/packages/cli-v3/src/mcp/formatters.ts +++ b/packages/cli-v3/src/mcp/formatters.ts @@ -473,10 +473,10 @@ export function formatSpanDetail(span: RetrieveSpanDetailResponseBody): string { const statusIndicator = span.isCancelled ? "[CANCELLED]" : span.isError - ? "[ERROR]" - : span.isPartial - ? "[IN PROGRESS]" - : "[COMPLETED]"; + ? "[ERROR]" + : span.isPartial + ? "[IN PROGRESS]" + : "[COMPLETED]"; lines.push(`## Span: ${span.message} ${statusIndicator}`); lines.push(`Span ID: ${span.spanId}`); diff --git a/packages/cli-v3/src/mcp/schemas.ts b/packages/cli-v3/src/mcp/schemas.ts index c69d11ab3ed..01aa9577cb3 100644 --- a/packages/cli-v3/src/mcp/schemas.ts +++ b/packages/cli-v3/src/mcp/schemas.ts @@ -242,17 +242,9 @@ export const QueryInput = CommonProjectsInput.extend({ period: z .string() .optional() - .describe( - "Time period shorthand, e.g. '1h', '7d', '30d'. Mutually exclusive with from/to." - ), - from: z - .string() - .optional() - .describe("Start of time range (ISO 8601). Must be paired with 'to'."), - to: z - .string() - .optional() - .describe("End of time range (ISO 8601). Must be paired with 'from'."), + .describe("Time period shorthand, e.g. '1h', '7d', '30d'. Mutually exclusive with from/to."), + from: z.string().optional().describe("Start of time range (ISO 8601). Must be paired with 'to'."), + to: z.string().optional().describe("End of time range (ISO 8601). Must be paired with 'from'."), }); export type QueryInput = z.output; @@ -265,9 +257,7 @@ export const QuerySchemaInput = CommonProjectsInput.pick({ }).extend({ table: z .string() - .describe( - "The table name to get the schema for (e.g. 'runs', 'metrics', 'llm_metrics')." - ), + .describe("The table name to get the schema for (e.g. 'runs', 'metrics', 'llm_metrics')."), }); export type QuerySchemaInput = z.output; @@ -296,14 +286,8 @@ export const RunDashboardQueryInput = CommonProjectsInput.extend({ .string() .optional() .describe("Time period shorthand, e.g. '1h', '7d', '30d'. Defaults to 1d."), - from: z - .string() - .optional() - .describe("Start of time range (ISO 8601). Must be paired with 'to'."), - to: z - .string() - .optional() - .describe("End of time range (ISO 8601). Must be paired with 'from'."), + from: z.string().optional().describe("Start of time range (ISO 8601). Must be paired with 'to'."), + to: z.string().optional().describe("End of time range (ISO 8601). Must be paired with 'from'."), scope: z .enum(["environment", "project", "organization"]) .default("environment") diff --git a/packages/cli-v3/src/mcp/smoke.test.ts b/packages/cli-v3/src/mcp/smoke.test.ts index d14403aa2cf..b6d24bb4e3e 100644 --- a/packages/cli-v3/src/mcp/smoke.test.ts +++ b/packages/cli-v3/src/mcp/smoke.test.ts @@ -75,10 +75,7 @@ async function main() { stderr: "pipe", }); - const client = new Client( - { name: "mcp-smoke-test", version: "1.0.0" }, - { capabilities: {} } - ); + const client = new Client({ name: "mcp-smoke-test", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport); @@ -95,7 +92,13 @@ async function main() { const duration = Date.now() - start; if (isError) { - results.push({ tool: name, status: "fail", duration, error: preview(text), preview: preview(text) }); + results.push({ + tool: name, + status: "fail", + duration, + error: preview(text), + preview: preview(text), + }); return null; } @@ -166,10 +169,14 @@ async function main() { if (runsText) { const runMatch = runsText.match(/run_\w+/); if (runMatch) { - await test("get_run_details", { ...commonArgs, runId: runMatch[0], maxTraceLines: 10 }, (text) => { - assert(text.includes("Run Details"), "Expected run details header"); - assert(text.includes("Run Trace"), "Expected trace section"); - }); + await test( + "get_run_details", + { ...commonArgs, runId: runMatch[0], maxTraceLines: 10 }, + (text) => { + assert(text.includes("Run Details"), "Expected run details header"); + assert(text.includes("Run Trace"), "Expected trace section"); + } + ); } } @@ -198,11 +205,20 @@ async function main() { }); // 14. query - await test("query", { ...commonArgs, query: "SELECT status, count() as total FROM runs GROUP BY status ORDER BY total DESC LIMIT 5", period: "7d" }, (text) => { - assert(text.includes("Query Results"), "Expected results header"); - assert(text.includes("status"), "Expected status column"); - assert(!text.includes("```json"), "Should use text table, not JSON code block"); - }); + await test( + "query", + { + ...commonArgs, + query: + "SELECT status, count() as total FROM runs GROUP BY status ORDER BY total DESC LIMIT 5", + period: "7d", + }, + (text) => { + assert(text.includes("Query Results"), "Expected results header"); + assert(text.includes("status"), "Expected status column"); + assert(!text.includes("```json"), "Should use text table, not JSON code block"); + } + ); // 15. list_dashboards await test("list_dashboards", commonArgs, (text) => { @@ -211,9 +227,13 @@ async function main() { }); // 16. run_dashboard_query - await test("run_dashboard_query", { ...commonArgs, dashboardKey: "overview", widgetId: "9lDDdebQ", period: "7d" }, (text) => { - assert(text.includes("Total runs"), "Expected widget title"); - }); + await test( + "run_dashboard_query", + { ...commonArgs, dashboardKey: "overview", widgetId: "9lDDdebQ", period: "7d" }, + (text) => { + assert(text.includes("Total runs"), "Expected widget title"); + } + ); // 17. dev_server_status (should show stopped) await test("dev_server_status", {}, (text) => { @@ -234,7 +254,12 @@ async function main() { for (const r of results) { const icon = r.status === "pass" ? "āœ“" : r.status === "fail" ? "āœ—" : "ā—‹"; const dur = `${r.duration}ms`.padStart(6); - const status = r.status === "pass" ? "\x1b[32mpass\x1b[0m" : r.status === "fail" ? "\x1b[31mfail\x1b[0m" : "\x1b[33mskip\x1b[0m"; + const status = + r.status === "pass" + ? "\x1b[32mpass\x1b[0m" + : r.status === "fail" + ? "\x1b[31mfail\x1b[0m" + : "\x1b[33mskip\x1b[0m"; console.log(` ${icon} ${r.tool.padEnd(25)} ${status} ${dur} ${r.error ?? r.preview ?? ""}`); diff --git a/packages/cli-v3/src/mcp/tools.test.ts b/packages/cli-v3/src/mcp/tools.test.ts index eef627fd5aa..48d14886984 100644 --- a/packages/cli-v3/src/mcp/tools.test.ts +++ b/packages/cli-v3/src/mcp/tools.test.ts @@ -56,10 +56,7 @@ async function main() { stderr: "pipe", }); - const client = new Client( - { name: "mcp-test-cli", version: "1.0.0" }, - { capabilities: {} } - ); + const client = new Client({ name: "mcp-test-cli", version: "1.0.0" }, { capabilities: {} }); await client.connect(transport); diff --git a/packages/cli-v3/src/mcp/tools.ts b/packages/cli-v3/src/mcp/tools.ts index f438989258d..a440750eca5 100644 --- a/packages/cli-v3/src/mcp/tools.ts +++ b/packages/cli-v3/src/mcp/tools.ts @@ -30,11 +30,7 @@ import { reactivatePromptOverrideTool, } from "./tools/prompts.js"; import { listAgentsTool } from "./tools/agents.js"; -import { - startAgentChatTool, - sendAgentMessageTool, - closeAgentChatTool, -} from "./tools/agentChat.js"; +import { startAgentChatTool, sendAgentMessageTool, closeAgentChatTool } from "./tools/agentChat.js"; import { respondWithError } from "./utils.js"; /** Tool names that perform write/mutating operations. */ diff --git a/packages/cli-v3/src/mcp/tools/agentChat.ts b/packages/cli-v3/src/mcp/tools/agentChat.ts index 816d1d5f9d0..f718722dd29 100644 --- a/packages/cli-v3/src/mcp/tools/agentChat.ts +++ b/packages/cli-v3/src/mcp/tools/agentChat.ts @@ -64,9 +64,7 @@ function serializeInputChunk(chunk: ChatInputChunk): string { const StartAgentChatInput = CommonProjectsInput.extend({ agentId: z .string() - .describe( - "The agent task ID to chat with. Use get_current_worker to see available agents." - ), + .describe("The agent task ID to chat with. Use get_current_worker to see available agents."), chatId: z .string() .describe("A unique conversation ID. Reuse to resume a conversation.") @@ -90,9 +88,7 @@ export const startAgentChatTool = { ctx.logger?.log("calling start_agent_chat", { input }); if (ctx.options.devOnly && input.environment !== "dev") { - return respondWithError( - `This MCP server is only available for the dev environment.` - ); + return respondWithError(`This MCP server is only available for the dev environment.`); } const projectRef = await ctx.getProjectRef({ @@ -103,12 +99,7 @@ export const startAgentChatTool = { const apiClient = await ctx.getApiClient({ projectRef, environment: input.environment, - scopes: [ - "write:tasks", - "read:runs", - "read:sessions", - "write:sessions", - ], + scopes: ["write:tasks", "read:runs", "read:sessions", "write:sessions"], branch: input.branch, }); @@ -210,7 +201,9 @@ export const sendAgentMessageTool = { const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const userMessage: ChatMessage = { - id: msgId, role: "user", parts: [{ type: "text", text: input.message }], + id: msgId, + role: "user", + parts: [{ type: "text", text: input.message }], }; // Track the outgoing user message @@ -311,9 +304,7 @@ export const closeAgentChatTool = { const session = activeSessions.get(input.chatId); if (!session) { - return respondWithError( - `No active chat with ID "${input.chatId}".` - ); + return respondWithError(`No active chat with ID "${input.chatId}".`); } if (session.runId) { @@ -426,9 +417,7 @@ async function collectAgentResponse( // new run — reuse sessionId, swap runId. Slim-wire: ship only // the latest user message as the turn-N delta; prior turns // come back via snapshot+replay on the new run's boot. - const lastUserMessage = [...session.messages] - .reverse() - .find((m) => m.role === "user"); + const lastUserMessage = [...session.messages].reverse().find((m) => m.role === "user"); const previousRunId = session.runId; const result = await session.apiClient.triggerTask(session.agentId, { payload: { @@ -491,9 +480,7 @@ async function collectAgentResponse( if (chunk.type === "tool-output-available" && typeof chunk.toolCallId === "string") { // Update existing tool part with output - const toolPart = parts.find( - (p) => p.toolCallId === chunk.toolCallId - ); + const toolPart = parts.find((p) => p.toolCallId === chunk.toolCallId); if (toolPart) { toolPart.state = "output-available"; toolPart.output = chunk.output; @@ -516,9 +503,7 @@ async function collectAgentResponse( // ─── Response formatter ────────────────────────────────────────── -function formatAssistantParts( - parts: Array<{ type: string; [key: string]: unknown }> -): string { +function formatAssistantParts(parts: Array<{ type: string; [key: string]: unknown }>): string { const sections: string[] = []; for (const part of parts) { diff --git a/packages/cli-v3/src/mcp/tools/agents.ts b/packages/cli-v3/src/mcp/tools/agents.ts index e40bcafab6d..f1e8c7bc7f4 100644 --- a/packages/cli-v3/src/mcp/tools/agents.ts +++ b/packages/cli-v3/src/mcp/tools/agents.ts @@ -60,9 +60,7 @@ export const listAgentsTool = { contents.push( "Use `start_agent_chat` with an agent's slug as the `agentId` to start a conversation." ); - contents.push( - "Use `get_task_schema` with an agent's slug to see its payload schema." - ); + contents.push("Use `get_task_schema` with an agent's slug to see its payload schema."); return { content: [{ type: "text", text: contents.join("\n") }], diff --git a/packages/cli-v3/src/mcp/tools/deploys.ts b/packages/cli-v3/src/mcp/tools/deploys.ts index 2b071f44e9f..9caba1114cc 100644 --- a/packages/cli-v3/src/mcp/tools/deploys.ts +++ b/packages/cli-v3/src/mcp/tools/deploys.ts @@ -146,11 +146,15 @@ export const listDeploysTool = { for (const deploy of deploys) { const deployedAt = deploy.deployedAt - ? new Date(deploy.deployedAt).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC") + ? new Date(deploy.deployedAt) + .toISOString() + .replace("T", " ") + .replace(/\.\d+Z$/, " UTC") : "not deployed"; - const git = deploy.git && typeof deploy.git === "object" && "commitMessage" in deploy.git - ? ` | ${deploy.git.commitMessage}` - : ""; + const git = + deploy.git && typeof deploy.git === "object" && "commitMessage" in deploy.git + ? ` | ${deploy.git.commitMessage}` + : ""; lines.push( `- ${deploy.shortCode} | v${deploy.version} | ${deploy.status} | ${deployedAt}${git}` ); diff --git a/packages/cli-v3/src/mcp/tools/devServer.ts b/packages/cli-v3/src/mcp/tools/devServer.ts index 9a21477edac..f636b67cf51 100644 --- a/packages/cli-v3/src/mcp/tools/devServer.ts +++ b/packages/cli-v3/src/mcp/tools/devServer.ts @@ -217,10 +217,7 @@ export const devServerStatusTool = { const recentLogs = devLogs.slice(-input.lines).join("\n"); - const content = [ - `## Dev Server Status: ${devState}`, - "", - ]; + const content = [`## Dev Server Status: ${devState}`, ""]; if (devCwd) { content.push(`**Directory:** ${devCwd}`); diff --git a/packages/cli-v3/src/mcp/tools/profiles.ts b/packages/cli-v3/src/mcp/tools/profiles.ts index 56dfab7e331..0b02099cf2a 100644 --- a/packages/cli-v3/src/mcp/tools/profiles.ts +++ b/packages/cli-v3/src/mcp/tools/profiles.ts @@ -71,9 +71,7 @@ export const listProfilesTool = { } content.push(""); - content.push( - "Use `switch_profile` to change the active profile for this session." - ); + content.push("Use `switch_profile` to change the active profile for this session."); return { content: [{ type: "text" as const, text: content.join("\n") }], diff --git a/packages/cli-v3/src/mcp/tools/prompts.ts b/packages/cli-v3/src/mcp/tools/prompts.ts index 7c23f64c24b..e458f80c5c2 100644 --- a/packages/cli-v3/src/mcp/tools/prompts.ts +++ b/packages/cli-v3/src/mcp/tools/prompts.ts @@ -36,8 +36,8 @@ const RemoveOverrideInput = PromptSlugInput; const ReactivateOverrideInput = CommonProjectsInput.extend({ slug: z.string().describe("The prompt slug"), - version: z - .coerce.number() + version: z.coerce + .number() .int() .positive() .describe("The dashboard-sourced version number to reactivate as override"), @@ -251,9 +251,7 @@ export const updatePromptOverrideTool = { }); return { - content: [ - { type: "text" as const, text: `Updated override for "${input.slug}".` }, - ], + content: [{ type: "text" as const, text: `Updated override for "${input.slug}".` }], }; }), }; @@ -285,9 +283,7 @@ export const removePromptOverrideTool = { await apiClient.removePromptOverride(input.slug); return { - content: [ - { type: "text" as const, text: `Removed override for "${input.slug}".` }, - ], + content: [{ type: "text" as const, text: `Removed override for "${input.slug}".` }], }; }), }; diff --git a/packages/cli-v3/src/mcp/tools/query.ts b/packages/cli-v3/src/mcp/tools/query.ts index e09cb9d9048..709d1b8e907 100644 --- a/packages/cli-v3/src/mcp/tools/query.ts +++ b/packages/cli-v3/src/mcp/tools/query.ts @@ -144,9 +144,7 @@ export const getQuerySchemaTool = { if (!table) { const available = schema.tables.map((t) => `${t.name} (${t.description ?? ""})`).join(", "); - return respondWithError( - `Table "${input.table}" not found. Available tables: ${available}` - ); + return respondWithError(`Table "${input.table}" not found. Available tables: ${available}`); } const content = [formatSchemaTable(table)]; diff --git a/packages/cli-v3/src/mcp/tools/runs.ts b/packages/cli-v3/src/mcp/tools/runs.ts index d1d43df5f6f..b4311bee4c5 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -3,8 +3,20 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { toolsMetadata } from "../config.js"; -import { formatRun, formatRunList, formatRunShape, formatRunTrace, formatSpanDetail } from "../formatters.js"; -import { CommonRunsInput, GetRunDetailsInput, GetSpanDetailsInput, ListRunsInput, WaitForRunInput } from "../schemas.js"; +import { + formatRun, + formatRunList, + formatRunShape, + formatRunTrace, + formatSpanDetail, +} from "../formatters.js"; +import { + CommonRunsInput, + GetRunDetailsInput, + GetSpanDetailsInput, + ListRunsInput, + WaitForRunInput, +} from "../schemas.js"; import { respondWithError, toolHandler } from "../utils.js"; // Cache formatted traces in temp files keyed by runId. @@ -199,9 +211,7 @@ export const getSpanDetailsTool = { const spanDetail = await apiClient.retrieveSpan(input.runId, input.spanId); const formatted = formatSpanDetail(spanDetail); - const runUrl = await ctx.getDashboardUrl( - `/projects/v3/${projectRef}/runs/${input.runId}` - ); + const runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${input.runId}`); const content = [formatted]; if (runUrl) { @@ -243,9 +253,7 @@ export const waitForRunToCompleteTool = { const timeoutMs = input.timeoutInSeconds * 1000; const timeoutSignal = AbortSignal.timeout(timeoutMs); - const combinedSignal = signal - ? AbortSignal.any([signal, timeoutSignal]) - : timeoutSignal; + const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; const runSubscription = apiClient.subscribeToRun(input.runId, { signal: combinedSignal }); const readableStream = runSubscription.getReader(); diff --git a/packages/cli-v3/src/mcp/tools/tasks.ts b/packages/cli-v3/src/mcp/tools/tasks.ts index ffa39c4ca82..d7a01ecbeb2 100644 --- a/packages/cli-v3/src/mcp/tools/tasks.ts +++ b/packages/cli-v3/src/mcp/tools/tasks.ts @@ -49,9 +49,7 @@ export const getCurrentWorker = { } contents.push(""); - contents.push( - "Use the `get_task_schema` tool with a task slug to get its payload schema." - ); + contents.push("Use the `get_task_schema` tool with a task slug to get its payload schema."); } else { contents.push(`The worker has no tasks registered.`); } @@ -198,16 +196,10 @@ export const getTaskSchemaTool = { if (!task) { const available = workerResult.data.worker.tasks.map((t) => t.slug).join(", "); - return respondWithError( - `Task "${input.taskSlug}" not found. Available tasks: ${available}` - ); + return respondWithError(`Task "${input.taskSlug}" not found. Available tasks: ${available}`); } - const content = [ - `## ${task.slug}`, - "", - `**File:** ${task.filePath}`, - ]; + const content = [`## ${task.slug}`, "", `**File:** ${task.filePath}`]; if (task.payloadSchema) { content.push(""); diff --git a/packages/cli-v3/src/rules/install.ts b/packages/cli-v3/src/rules/install.ts index 8b137891791..e69de29bb2d 100644 --- a/packages/cli-v3/src/rules/install.ts +++ b/packages/cli-v3/src/rules/install.ts @@ -1 +0,0 @@ - diff --git a/packages/cli-v3/src/utilities/colorMarkup.test.ts b/packages/cli-v3/src/utilities/colorMarkup.test.ts index c6e64845ad2..8acd2370902 100644 --- a/packages/cli-v3/src/utilities/colorMarkup.test.ts +++ b/packages/cli-v3/src/utilities/colorMarkup.test.ts @@ -75,9 +75,21 @@ describe("applyColorMarkup", () => { it("handles all valid color tags", () => { const tags = [ - "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray", - "redBright", "greenBright", "yellowBright", "blueBright", - "magentaBright", "cyanBright", "whiteBright", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", + "gray", + "redBright", + "greenBright", + "yellowBright", + "blueBright", + "magentaBright", + "cyanBright", + "whiteBright", ]; for (const tag of tags) { const result = applyColorMarkup(`{${tag}}test{/${tag}}`); diff --git a/packages/cli-v3/src/utilities/colorMarkup.ts b/packages/cli-v3/src/utilities/colorMarkup.ts index 17c45eea3f6..f827a5e6613 100644 --- a/packages/cli-v3/src/utilities/colorMarkup.ts +++ b/packages/cli-v3/src/utilities/colorMarkup.ts @@ -26,10 +26,7 @@ type Token = { type: "text"; value: string } | { type: "styled"; tag: string; va * On malformed input (unclosed, mismatched, or nested tags), returns the entire * string styled with `fallbackStyle` (or unstyled if no fallback). */ -export function applyColorMarkup( - text: string, - fallbackStyle?: (t: string) => string -): string { +export function applyColorMarkup(text: string, fallbackStyle?: (t: string) => string): string { const tokens = tokenize(text); if (!tokens) { // Malformed markup — apply fallback to entire string diff --git a/packages/cli-v3/src/utilities/discoveryCheck.ts b/packages/cli-v3/src/utilities/discoveryCheck.ts index 8f0ceb46a64..632eaf76046 100644 --- a/packages/cli-v3/src/utilities/discoveryCheck.ts +++ b/packages/cli-v3/src/utilities/discoveryCheck.ts @@ -61,8 +61,7 @@ async function doEvaluate(spec: DiscoverySpec, projectRoot: string): Promise { +async function resolveFilePatterns(patterns: string[], projectRoot: string): Promise { const matched: string[] = []; for (const pattern of patterns) { @@ -120,10 +116,7 @@ async function resolveFilePatterns( return matched; } -async function checkContentPattern( - files: string[], - contentPattern: string -): Promise { +async function checkContentPattern(files: string[], contentPattern: string): Promise { const useFastPath = !REGEX_METACHARACTERS.test(contentPattern); // Pre-compile regex once outside the loop to avoid repeated compilation diff --git a/packages/cli-v3/src/utilities/platformNotifications.ts b/packages/cli-v3/src/utilities/platformNotifications.ts index cfdbc83ff48..8b04a66e272 100644 --- a/packages/cli-v3/src/utilities/platformNotifications.ts +++ b/packages/cli-v3/src/utilities/platformNotifications.ts @@ -27,10 +27,7 @@ export async function fetchPlatformNotification( options: FetchNotificationOptions ): Promise { const [error, result] = await tryCatch( - options.apiClient.getCliPlatformNotification( - options.projectRef, - AbortSignal.timeout(7000) - ) + options.apiClient.getCliPlatformNotification(options.projectRef, AbortSignal.timeout(7000)) ); if (error) { @@ -63,9 +60,7 @@ export async function fetchPlatformNotification( return { level: type, title, description, actionUrl }; } -function displayPlatformNotification( - notification: PlatformNotification | undefined -): void { +function displayPlatformNotification(notification: PlatformNotification | undefined): void { if (!notification) return; const message = formatNotificationMessage(notification); diff --git a/packages/core/src/schemas/eventFilter.ts b/packages/core/src/schemas/eventFilter.ts index 81be2e7c12e..66b4d645466 100644 --- a/packages/core/src/schemas/eventFilter.ts +++ b/packages/core/src/schemas/eventFilter.ts @@ -53,8 +53,8 @@ const EventMatcherSchema = z.union([ $includes: z.union([z.string(), z.number(), z.boolean()]), }), z.object({ - $not: z.union([z.string(), z.number(), z.boolean()]) - }) + $not: z.union([z.string(), z.number(), z.boolean()]), + }), ]) ), ]); diff --git a/packages/core/src/v3/apiClient/core.ts b/packages/core/src/v3/apiClient/core.ts index 5541bb2a9a1..5128aa50d47 100644 --- a/packages/core/src/v3/apiClient/core.ts +++ b/packages/core/src/v3/apiClient/core.ts @@ -603,14 +603,14 @@ async function waitForRetry( } // https://stackoverflow.com/a/34491287 -export function isEmptyObj(obj: Object | null | undefined): boolean { +export function isEmptyObj(obj: object | null | undefined): boolean { if (!obj) return true; for (const _k in obj) return false; return true; } // https://eslint.org/docs/latest/rules/no-prototype-builtins -export function hasOwn(obj: Object, key: string): boolean { +export function hasOwn(obj: object, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } diff --git a/packages/core/src/v3/apiClient/errors.ts b/packages/core/src/v3/apiClient/errors.ts index 5f38a4947b8..e49fe80759f 100644 --- a/packages/core/src/v3/apiClient/errors.ts +++ b/packages/core/src/v3/apiClient/errors.ts @@ -3,7 +3,7 @@ export type APIHeaders = Record; export class ApiError extends Error { readonly status: number | undefined; readonly headers: APIHeaders | undefined; - readonly error: Object | undefined; + readonly error: object | undefined; readonly code: string | null | undefined; readonly param: string | null | undefined; @@ -11,7 +11,7 @@ export class ApiError extends Error { constructor( status: number | undefined, - error: Object | undefined, + error: object | undefined, message: string | undefined, headers: APIHeaders | undefined ) { @@ -33,10 +33,10 @@ export class ApiError extends Error { ? error.message : JSON.stringify(error.message) : typeof error === "string" - ? error - : error - ? JSON.stringify(error) - : undefined; + ? error + : error + ? JSON.stringify(error) + : undefined; if (errorMessage) { return errorMessage; @@ -59,7 +59,7 @@ export class ApiError extends Error { static generate( status: number | undefined, - errorResponse: Object | undefined, + errorResponse: object | undefined, message: string | undefined, headers: APIHeaders | undefined ) { @@ -117,15 +117,15 @@ export class ApiConnectionError extends ApiError { } export class BadRequestError extends ApiError { - override readonly status: 400 = 400; + override readonly status = 400 as const; } export class AuthenticationError extends ApiError { - override readonly status: 401 = 401; + override readonly status = 401 as const; } export class PermissionDeniedError extends ApiError { - override readonly status: 403 = 403; + override readonly status = 403 as const; } /** @@ -141,19 +141,19 @@ export function isTriggerRealtimeAuthError(error: unknown): boolean { } export class NotFoundError extends ApiError { - override readonly status: 404 = 404; + override readonly status = 404 as const; } export class ConflictError extends ApiError { - override readonly status: 409 = 409; + override readonly status = 409 as const; } export class UnprocessableEntityError extends ApiError { - override readonly status: 422 = 422; + override readonly status = 422 as const; } export class RateLimitError extends ApiError { - override readonly status: 429 = 429; + override readonly status = 429 as const; get millisecondsUntilReset(): number | undefined { // x-ratelimit-reset is the unix timestamp in milliseconds when the rate limit will reset. @@ -177,7 +177,7 @@ export class RateLimitError extends ApiError { export class InternalServerError extends ApiError {} export class ApiSchemaValidationError extends ApiError { - override readonly status: 200 = 200; + override readonly status = 200 as const; readonly rawBody: any; constructor({ diff --git a/packages/core/src/v3/apiClient/getBranch.ts b/packages/core/src/v3/apiClient/getBranch.ts index 1e1873fc2ae..51ac8b1bc75 100644 --- a/packages/core/src/v3/apiClient/getBranch.ts +++ b/packages/core/src/v3/apiClient/getBranch.ts @@ -33,11 +33,7 @@ export function getBranch({ return undefined; } -export function getDevBranch({ - specified, -}: { - specified?: string; -}): string | undefined { +export function getDevBranch({ specified }: { specified?: string }): string | undefined { // For development we don't look at git/Vercel — only the flag and our env var. const branch = specified ?? getEnvVar("TRIGGER_DEV_BRANCH"); diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index e9e663223df..410bac0f172 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -118,10 +118,7 @@ import { RealtimeRunSkipColumns, type SSEStreamPart, } from "./runStream.js"; -import { - controlSubtype, - type ControlEvent, -} from "../sessionStreams/wireProtocol.js"; +import { controlSubtype, type ControlEvent } from "../sessionStreams/wireProtocol.js"; import { CreateEnvironmentVariableParams, ImportEnvironmentVariablesParams, @@ -497,9 +494,9 @@ export class ApiClient { await safeStreamCancel(forRetry); const errText = await response.text().catch((e) => (e as Error).message); - let errJSON: Object | undefined; + let errJSON: object | undefined; try { - errJSON = JSON.parse(errText) as Object; + errJSON = JSON.parse(errText) as object; } catch { // ignore } @@ -1868,9 +1865,7 @@ export class ApiClient { ); } - async listDashboards( - requestOptions?: ZodFetchOptions - ): Promise { + async listDashboards(requestOptions?: ZodFetchOptions): Promise { return zodfetch( ListDashboardsResponseBody, `${this.baseUrl}/api/v1/query/dashboards`, @@ -1949,11 +1944,7 @@ export class ApiClient { return headers; } - resolvePrompt( - slug: string, - body: ResolvePromptRequestBody, - requestOptions?: ZodFetchOptions - ) { + resolvePrompt(slug: string, body: ResolvePromptRequestBody, requestOptions?: ZodFetchOptions) { return zodfetch( ResolvePromptResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}`, @@ -1984,7 +1975,11 @@ export class ApiClient { ); } - promotePromptVersion(slug: string, body: PromotePromptVersionRequestBody, requestOptions?: ZodFetchOptions) { + promotePromptVersion( + slug: string, + body: PromotePromptVersionRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/promote`, @@ -1993,7 +1988,11 @@ export class ApiClient { ); } - createPromptOverride(slug: string, body: CreatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + createPromptOverride( + slug: string, + body: CreatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOverrideCreatedResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override`, @@ -2002,7 +2001,11 @@ export class ApiClient { ); } - updatePromptOverride(slug: string, body: UpdatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + updatePromptOverride( + slug: string, + body: UpdatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override`, @@ -2020,7 +2023,11 @@ export class ApiClient { ); } - reactivatePromptOverride(slug: string, body: ReactivatePromptOverrideRequestBody, requestOptions?: ZodFetchOptions) { + reactivatePromptOverride( + slug: string, + body: ReactivatePromptOverrideRequestBody, + requestOptions?: ZodFetchOptions + ) { return zodfetch( PromptOkResponseBody, `${this.baseUrl}/api/v1/prompts/${slug}/override/reactivate`, diff --git a/packages/core/src/v3/apiClient/runStream.test.ts b/packages/core/src/v3/apiClient/runStream.test.ts index 4ac2880976a..0dca73779af 100644 --- a/packages/core/src/v3/apiClient/runStream.test.ts +++ b/packages/core/src/v3/apiClient/runStream.test.ts @@ -451,7 +451,11 @@ describe("SSEStreamSubscription v2 batch parsing — record kinds", () => { vi.restoreAllMocks(); }); - type ParsedPart = { id: string; chunk: unknown; headers?: ReadonlyArray }; + type ParsedPart = { + id: string; + chunk: unknown; + headers?: ReadonlyArray; + }; // Build a v2 batch SSE response with the given records and close. function makeBatchResponse( diff --git a/packages/core/src/v3/apiClient/runStream.ts b/packages/core/src/v3/apiClient/runStream.ts index 6d58dff1a70..d89b533d578 100644 --- a/packages/core/src/v3/apiClient/runStream.ts +++ b/packages/core/src/v3/apiClient/runStream.ts @@ -251,9 +251,7 @@ export class SSEStreamSubscription implements StreamSubscription { this.retryJitter = options.retryJitter ?? 0.5; this.fetchTimeoutMs = options.fetchTimeoutMs ?? 30_000; this.stallTimeoutMs = options.stallTimeoutMs ?? 0; - this.nonRetryableStatuses = new Set( - options.nonRetryableStatuses ?? [400, 404, 409, 410, 422] - ); + this.nonRetryableStatuses = new Set(options.nonRetryableStatuses ?? [400, 404, 409, 410, 422]); } /** diff --git a/packages/core/src/v3/apiClientManager/index.ts b/packages/core/src/v3/apiClientManager/index.ts index 0107e622495..74ff7ed15a3 100644 --- a/packages/core/src/v3/apiClientManager/index.ts +++ b/packages/core/src/v3/apiClientManager/index.ts @@ -114,7 +114,13 @@ export class APIClientManagerAPI { const requestOptions = source?.requestOptions; const futureFlags = source?.future; - return new ApiClient(this.baseURL, this.accessToken, this.branchName, requestOptions, futureFlags); + return new ApiClient( + this.baseURL, + this.accessToken, + this.branchName, + requestOptions, + futureFlags + ); } clientOrThrow(config?: ApiClientConfiguration): ApiClient { diff --git a/packages/core/src/v3/auth/environment.ts b/packages/core/src/v3/auth/environment.ts index 8918f191300..498393722fe 100644 --- a/packages/core/src/v3/auth/environment.ts +++ b/packages/core/src/v3/auth/environment.ts @@ -20,11 +20,7 @@ // them here keeps the contract structural (no @trigger.dev/database // import) while giving downstream consumers the same exact union they // expect when this value is passed to a Prisma column. -export type RuntimeEnvironmentType = - | "PRODUCTION" - | "STAGING" - | "DEVELOPMENT" - | "PREVIEW"; +export type RuntimeEnvironmentType = "PRODUCTION" | "STAGING" | "DEVELOPMENT" | "PREVIEW"; export type RunEngineVersion = "V1" | "V2"; diff --git a/packages/core/src/v3/errors.test.ts b/packages/core/src/v3/errors.test.ts index b02bbf5f3cf..f5df741f17d 100644 --- a/packages/core/src/v3/errors.test.ts +++ b/packages/core/src/v3/errors.test.ts @@ -12,9 +12,7 @@ describe("DuplicateTaskIdsError", () => { }); test("names the id and both files when an id is defined in two files", () => { - const error = new DuplicateTaskIdsError([ - { id: "foo", filePaths: ["src/a.ts", "src/b.ts"] }, - ]); + const error = new DuplicateTaskIdsError([{ id: "foo", filePaths: ["src/a.ts", "src/b.ts"] }]); expect(error.message).toContain('"foo"'); expect(error.message).toContain("src/a.ts"); diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index 187cbef9b64..7b396c0b90f 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -339,13 +339,9 @@ export function sanitizeError(error: TaskRunError): TaskRunError { type: "INTERNAL_ERROR", code: error.code, message: - error.message != null - ? truncateMessage(error.message.replace(/\0/g, "")) - : undefined, + error.message != null ? truncateMessage(error.message.replace(/\0/g, "")) : undefined, stackTrace: - error.stackTrace != null - ? truncateStack(error.stackTrace.replace(/\0/g, "")) - : undefined, + error.stackTrace != null ? truncateStack(error.stackTrace.replace(/\0/g, "")) : undefined, }; } } diff --git a/packages/core/src/v3/idempotencyKeys.ts b/packages/core/src/v3/idempotencyKeys.ts index 3a6946ccbd0..aafe9f906f0 100644 --- a/packages/core/src/v3/idempotencyKeys.ts +++ b/packages/core/src/v3/idempotencyKeys.ts @@ -10,7 +10,10 @@ import { digestSHA256 } from "./utils/crypto.js"; import type { ZodFetchOptions } from "./apiClient/core.js"; // Re-export types from catalog for backwards compatibility -export type { IdempotencyKeyScope, IdempotencyKeyOptions } from "./idempotency-key-catalog/catalog.js"; +export type { + IdempotencyKeyScope, + IdempotencyKeyOptions, +} from "./idempotency-key-catalog/catalog.js"; /** * Extracts the user-provided key and scope from an idempotency key created with `idempotencyKeys.create()`. @@ -227,9 +230,7 @@ export async function resetIdempotencyKey( // Try to extract options from an IdempotencyKey created with idempotencyKeys.create() const attachedOptions = - typeof idempotencyKey === "string" - ? getIdempotencyKeyOptions(idempotencyKey) - : undefined; + typeof idempotencyKey === "string" ? getIdempotencyKeyOptions(idempotencyKey) : undefined; const scope = attachedOptions?.scope ?? options?.scope ?? "run"; const keyArray = Array.isArray(idempotencyKey) diff --git a/packages/core/src/v3/inputStreams/manager.ts b/packages/core/src/v3/inputStreams/manager.ts index 51424df39f7..11f9f497b21 100644 --- a/packages/core/src/v3/inputStreams/manager.ts +++ b/packages/core/src/v3/inputStreams/manager.ts @@ -23,7 +23,6 @@ type OnceWaiter = { abortHandler?: () => void; }; - type TailState = { abortController: AbortController; promise: Promise; @@ -198,7 +197,8 @@ export class StandardInputStreamManager implements InputStreamManager { // Abort tails that no longer have any once waiters either for (const [streamId, tail] of this.tails) { - const hasWaiters = this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; + const hasWaiters = + this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (!hasWaiters) { tail.abortController.abort(); this.tails.delete(streamId); @@ -304,8 +304,7 @@ export class StandardInputStreamManager implements InputStreamManager { // failure (auth rejected, 5xx, DNS) would reconnect in a tight // loop because `#runTail`'s error path only logs. `#dispatch` // resets the counter on every successful record. - const hasHandlers = - this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; + const hasHandlers = this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; const hasWaiters = this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (hasHandlers || hasWaiters) { @@ -318,8 +317,7 @@ export class StandardInputStreamManager implements InputStreamManager { const stillHasHandlers = this.handlers.has(streamId) && this.handlers.get(streamId)!.size > 0; const stillHasWaiters = - this.onceWaiters.has(streamId) && - this.onceWaiters.get(streamId)!.length > 0; + this.onceWaiters.has(streamId) && this.onceWaiters.get(streamId)!.length > 0; if (!stillHasHandlers && !stillHasWaiters) return; this.#ensureStreamTailConnected(streamId); }, delayMs); @@ -332,34 +330,30 @@ export class StandardInputStreamManager implements InputStreamManager { async #runTail(runId: string, streamId: string, signal: AbortSignal): Promise { try { const lastSeq = this.seqNums.get(streamId); - const stream = await this.apiClient.fetchStream( - runId, - `input/${streamId}`, - { - signal, - baseUrl: this.baseUrl, - // Max allowed by the SSE endpoint is 600s; the tail will reconnect on close - timeoutInSeconds: 600, - // Resume from last seen sequence number to avoid replaying history on reconnect - lastEventId: lastSeq !== undefined ? String(lastSeq) : undefined, - onPart: (part) => { - const seqNum = parseInt(part.id, 10); - if (Number.isFinite(seqNum)) { - this.seqNums.set(streamId, seqNum); - } - }, - onComplete: () => { - if (this.debug) { - console.log(`[InputStreamManager] Tail stream completed for "${streamId}"`); - } - }, - onError: (error) => { - if (this.debug) { - console.error(`[InputStreamManager] Tail stream error for "${streamId}":`, error); - } - }, - } - ); + const stream = await this.apiClient.fetchStream(runId, `input/${streamId}`, { + signal, + baseUrl: this.baseUrl, + // Max allowed by the SSE endpoint is 600s; the tail will reconnect on close + timeoutInSeconds: 600, + // Resume from last seen sequence number to avoid replaying history on reconnect + lastEventId: lastSeq !== undefined ? String(lastSeq) : undefined, + onPart: (part) => { + const seqNum = parseInt(part.id, 10); + if (Number.isFinite(seqNum)) { + this.seqNums.set(streamId, seqNum); + } + }, + onComplete: () => { + if (this.debug) { + console.log(`[InputStreamManager] Tail stream completed for "${streamId}"`); + } + }, + onError: (error) => { + if (this.debug) { + console.error(`[InputStreamManager] Tail stream error for "${streamId}":`, error); + } + }, + }); for await (const record of stream) { if (signal.aborted) break; diff --git a/packages/core/src/v3/inputStreams/noopManager.ts b/packages/core/src/v3/inputStreams/noopManager.ts index 612da832d7e..5aca9c8ed9d 100644 --- a/packages/core/src/v3/inputStreams/noopManager.ts +++ b/packages/core/src/v3/inputStreams/noopManager.ts @@ -24,7 +24,9 @@ export class NoopInputStreamManager implements InputStreamManager { setLastSeqNum(_streamId: string, _seqNum: number): void {} - shiftBuffer(_streamId: string): boolean { return false; } + shiftBuffer(_streamId: string): boolean { + return false; + } disconnectStream(_streamId: string): void {} diff --git a/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts b/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts index aeb4ce4bee5..aa24fddfa00 100644 --- a/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts +++ b/packages/core/src/v3/otel/nodejsRuntimeMetrics.ts @@ -56,26 +56,23 @@ export function startNodejsRuntimeMetrics(meterProvider: MeterProvider) { observables.push(heapUsed, heapTotal); // Single batch callback for all metrics - meter.addBatchObservableCallback( - (obs) => { - // ELU - const currentElu = performance.eventLoopUtilization(); - const diff = performance.eventLoopUtilization(currentElu, lastElu); - lastElu = currentElu; - obs.observe(eluGauge, diff.utilization); + meter.addBatchObservableCallback((obs) => { + // ELU + const currentElu = performance.eventLoopUtilization(); + const diff = performance.eventLoopUtilization(currentElu, lastElu); + lastElu = currentElu; + obs.observe(eluGauge, diff.utilization); - // Event loop delay (nanoseconds -> seconds) - if (eld && eldP95 && eldMax) { - obs.observe(eldP95, eld.percentile(95) / 1e9); - obs.observe(eldMax, eld.max / 1e9); - eld.reset(); - } + // Event loop delay (nanoseconds -> seconds) + if (eld && eldP95 && eldMax) { + obs.observe(eldP95, eld.percentile(95) / 1e9); + obs.observe(eldMax, eld.max / 1e9); + eld.reset(); + } - // Heap - const mem = process.memoryUsage(); - obs.observe(heapUsed, mem.heapUsed); - obs.observe(heapTotal, mem.heapTotal); - }, - observables - ); + // Heap + const mem = process.memoryUsage(); + obs.observe(heapUsed, mem.heapUsed); + obs.observe(heapTotal, mem.heapTotal); + }, observables); } diff --git a/packages/core/src/v3/otel/tracingSDK.ts b/packages/core/src/v3/otel/tracingSDK.ts index f03bd2fc3a6..0dd56f5a6f7 100644 --- a/packages/core/src/v3/otel/tracingSDK.ts +++ b/packages/core/src/v3/otel/tracingSDK.ts @@ -175,24 +175,19 @@ export class TracingSDK { for (const exporter of config.exporters ?? []) { spanProcessors.push( getEnvVar("TRIGGER_OTEL_BATCH_PROCESSING_ENABLED") === "1" - ? new BatchSpanProcessor( - new ExternalSpanExporterWrapper(exporter, externalTraceId), - { - maxExportBatchSize: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE") ?? "64" - ), - scheduledDelayMillis: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS") ?? "200" - ), - exportTimeoutMillis: parseInt( - getEnvVar("TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS") ?? "30000" - ), - maxQueueSize: parseInt(getEnvVar("TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE") ?? "512"), - } - ) - : new SimpleSpanProcessor( - new ExternalSpanExporterWrapper(exporter, externalTraceId) - ) + ? new BatchSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId), { + maxExportBatchSize: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE") ?? "64" + ), + scheduledDelayMillis: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS") ?? "200" + ), + exportTimeoutMillis: parseInt( + getEnvVar("TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS") ?? "30000" + ), + maxQueueSize: parseInt(getEnvVar("TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE") ?? "512"), + }) + : new SimpleSpanProcessor(new ExternalSpanExporterWrapper(exporter, externalTraceId)) ); } @@ -282,9 +277,7 @@ export class TracingSDK { // Metrics setup const metricsUrl = - config.metricsUrl ?? - getEnvVar("TRIGGER_OTEL_METRICS_ENDPOINT") ?? - `${config.url}/v1/metrics`; + config.metricsUrl ?? getEnvVar("TRIGGER_OTEL_METRICS_ENDPOINT") ?? `${config.url}/v1/metrics`; const rawMetricExporter = new OTLPMetricExporter({ url: metricsUrl, @@ -511,9 +504,7 @@ class ExternalLogRecordExporterWrapper { return; } - const modifiedLogs = logs.map((log) => - this.transformLogRecord(log, externalTraceContext) - ); + const modifiedLogs = logs.map((log) => this.transformLogRecord(log, externalTraceContext)); this.underlyingExporter.export(modifiedLogs, resultCallback); } @@ -527,9 +518,7 @@ class ExternalLogRecordExporterWrapper { forceFlush?: () => Promise; }; - return underlyingExporter.forceFlush - ? underlyingExporter.forceFlush() - : Promise.resolve(); + return underlyingExporter.forceFlush ? underlyingExporter.forceFlush() : Promise.resolve(); } transformLogRecord( diff --git a/packages/core/src/v3/realtimeStreams/manager.test.ts b/packages/core/src/v3/realtimeStreams/manager.test.ts index 179754bc752..93268953c5e 100644 --- a/packages/core/src/v3/realtimeStreams/manager.test.ts +++ b/packages/core/src/v3/realtimeStreams/manager.test.ts @@ -58,10 +58,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { const { client, spy } = makeApiClient(async () => ({ version: "v1", headers: {} })); const manager = new StandardRealtimeStreamsManager(client, "http://localhost"); - await Promise.all([ - getCached(manager, "run-1", "chat"), - getCached(manager, "run-2", "chat"), - ]); + await Promise.all([getCached(manager, "run-1", "chat"), getCached(manager, "run-2", "chat")]); expect(spy).toHaveBeenCalledTimes(2); }); @@ -108,11 +105,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { // writer's `wait()` rejects. ( manager as unknown as { - evictCreateStreamIfStale: ( - runId: string, - key: string, - expected: Promise - ) => void; + evictCreateStreamIfStale: (runId: string, key: string, expected: Promise) => void; } ).evictCreateStreamIfStale("run-1", "chat", cachedPromise); @@ -132,11 +125,7 @@ describe("StandardRealtimeStreamsManager createStream cache", () => { const stalePromise = Promise.resolve({ version: "v1", headers: {} }); ( manager as unknown as { - evictCreateStreamIfStale: ( - runId: string, - key: string, - expected: Promise - ) => void; + evictCreateStreamIfStale: (runId: string, key: string, expected: Promise) => void; } ).evictCreateStreamIfStale("run-1", "chat", stalePromise); diff --git a/packages/core/src/v3/realtimeStreams/manager.ts b/packages/core/src/v3/realtimeStreams/manager.ts index f4d915acc3f..b72ce8af022 100644 --- a/packages/core/src/v3/realtimeStreams/manager.ts +++ b/packages/core/src/v3/realtimeStreams/manager.ts @@ -98,17 +98,13 @@ export class StandardRealtimeStreamsManager implements RealtimeStreamsManager { const abortController = new AbortController(); // Chain with user-provided signal if present const combinedSignal = options?.signal - ? AbortSignal.any?.([options.signal, abortController.signal]) ?? abortController.signal + ? (AbortSignal.any?.([options.signal, abortController.signal]) ?? abortController.signal) : abortController.signal; // Capture which cached promise this writer uses so reactive // invalidation below evicts only if the cache still holds it (a // concurrent caller may have already refreshed it). - const activeCreatePromise = this.getCachedCreateStream( - runId, - key, - options?.requestOptions - ); + const activeCreatePromise = this.getCachedCreateStream(runId, key, options?.requestOptions); const streamInstance = new StreamInstance({ apiClient: this.apiClient, diff --git a/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts b/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts index c19faf6c2f8..3b51f8540b3 100644 --- a/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts +++ b/packages/core/src/v3/realtimeStreams/streamsWriterV1.ts @@ -433,7 +433,7 @@ export class StreamsWriterV1 implements StreamsWriter { const lastChunkHeader = res.headers["x-last-chunk-index"]; if (lastChunkHeader) { const lastChunkIndex = parseInt( - Array.isArray(lastChunkHeader) ? lastChunkHeader[0] ?? "0" : lastChunkHeader ?? "0", + Array.isArray(lastChunkHeader) ? (lastChunkHeader[0] ?? "0") : (lastChunkHeader ?? "0"), 10 ); resolve(lastChunkIndex); diff --git a/packages/core/src/v3/realtimeStreams/types.ts b/packages/core/src/v3/realtimeStreams/types.ts index 5e537d991ff..87614ff0625 100644 --- a/packages/core/src/v3/realtimeStreams/types.ts +++ b/packages/core/src/v3/realtimeStreams/types.ts @@ -200,7 +200,9 @@ export type RealtimeDefinedInputStream = { * then suspends via `.wait()` if no data arrives. If data arrives during * the idle phase the task responds instantly without suspending. */ - waitWithIdleTimeout: (options: InputStreamWaitWithIdleTimeoutOptions) => Promise<{ ok: true; output: TData } | { ok: false; error?: any }>; + waitWithIdleTimeout: ( + options: InputStreamWaitWithIdleTimeoutOptions + ) => Promise<{ ok: true; output: TData } | { ok: false; error?: any }>; /** * Send data to this input stream on a specific run. * This is used from outside the task (e.g., from your backend or another task). @@ -272,9 +274,8 @@ export type InputStreamWaitWithIdleTimeoutOptions = { skipSuspend?: boolean; }; -export type InferInputStreamType = T extends RealtimeDefinedInputStream - ? TData - : unknown; +export type InferInputStreamType = + T extends RealtimeDefinedInputStream ? TData : unknown; /** * Internal record format for multiplexed input stream data on S2. diff --git a/packages/core/src/v3/resource-catalog/catalog.ts b/packages/core/src/v3/resource-catalog/catalog.ts index 1036152c928..3c128716966 100644 --- a/packages/core/src/v3/resource-catalog/catalog.ts +++ b/packages/core/src/v3/resource-catalog/catalog.ts @@ -6,7 +6,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; export interface ResourceCatalog { setCurrentFileContext(filePath: string, entryPoint: string): void; diff --git a/packages/core/src/v3/resource-catalog/index.ts b/packages/core/src/v3/resource-catalog/index.ts index 4e6e278ad02..8ef04516f80 100644 --- a/packages/core/src/v3/resource-catalog/index.ts +++ b/packages/core/src/v3/resource-catalog/index.ts @@ -8,7 +8,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js"; import { type ResourceCatalog } from "./catalog.js"; import { NoopResourceCatalog } from "./noopResourceCatalog.js"; diff --git a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts index 509b76c1792..4697dc8c250 100644 --- a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts @@ -6,7 +6,11 @@ import { TaskManifest, WorkerManifest, } from "../schemas/index.js"; -import { type PromptMetadataWithFunctions, type TaskMetadataWithFunctions, type TaskSchema } from "../types/index.js"; +import { + type PromptMetadataWithFunctions, + type TaskMetadataWithFunctions, + type TaskSchema, +} from "../types/index.js"; import { ResourceCatalog } from "./catalog.js"; export class NoopResourceCatalog implements ResourceCatalog { diff --git a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts index bf200936d6f..8446144af94 100644 --- a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts @@ -9,7 +9,11 @@ import { WorkerManifest, QueueManifest, } from "../schemas/index.js"; -import { PromptMetadataWithFunctions, TaskMetadataWithFunctions, TaskSchema } from "../types/index.js"; +import { + PromptMetadataWithFunctions, + TaskMetadataWithFunctions, + TaskSchema, +} from "../types/index.js"; import { ResourceCatalog } from "./catalog.js"; /** @@ -118,10 +122,7 @@ export class StandardResourceCatalog implements ResourceCatalog { // registration. Skip the runtime sentinel context (a task() firing during // another task's run) — that's a re-registration, not a duplicate // definition, and the indexer never uses the sentinel. - if ( - this._taskMetadata.has(task.id) && - this._currentFileContext.filePath !== NO_FILE_CONTEXT - ) { + if (this._taskMetadata.has(task.id) && this._currentFileContext.filePath !== NO_FILE_CONTEXT) { const existingFilePath = this._taskFileMetadata.get(task.id)?.filePath; const currentFilePath = this._currentFileContext.filePath; const collision = this._taskIdCollisions.find((c) => c.id === task.id); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts index bbf93ad96fc..f19c18aa8ca 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.test.ts @@ -66,7 +66,9 @@ describe("RunQueueConsumer dequeue latency metric", () => { const text = await register.metrics(); // One observation for the whole batch, not one per message. - expect(text).toContain('queue_consumer_pool_dequeue_duration_seconds_count{outcome="success"} 1'); + expect(text).toContain( + 'queue_consumer_pool_dequeue_duration_seconds_count{outcome="success"} 1' + ); }); it('records outcome="error" when the response is unsuccessful', async () => { @@ -99,6 +101,8 @@ describe("RunQueueConsumer dequeue latency metric", () => { ).resolves.not.toThrow(); // Histogram has no observations - the labelled count line should be absent. - expect(await register.metrics()).not.toContain("queue_consumer_pool_dequeue_duration_seconds_count"); + expect(await register.metrics()).not.toContain( + "queue_consumer_pool_dequeue_duration_seconds_count" + ); }); }); diff --git a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts index cf48ce2e9e0..91e05a1654b 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/queueConsumer.ts @@ -18,7 +18,10 @@ export type RunQueueConsumerOptions = { maxRunCount?: number; /** Which worker-queue class this consumer pulls from. Defaults to the worker's region queue. */ queueClass?: WorkerQueueClass; - onDequeue: (messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }) => Promise; + onDequeue: ( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ) => Promise; /** Optional shared pool metrics. When provided, dequeue API latency is recorded as a histogram. */ metrics?: ConsumerPoolMetrics; }; @@ -29,7 +32,10 @@ export class RunQueueConsumer implements QueueConsumer { private readonly preSkip?: PreSkipFn; private readonly maxRunCount?: number; private readonly queueClass?: WorkerQueueClass; - private readonly onDequeue: (messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }) => Promise; + private readonly onDequeue: ( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ) => Promise; private readonly metrics?: ConsumerPoolMetrics; private readonly logger = new SimpleStructuredLogger("queue-consumer"); @@ -142,7 +148,10 @@ export class RunQueueConsumer implements QueueConsumer { ); try { - await this.onDequeue(response.data, { dequeueResponseMs, pollingIntervalMs: this.lastScheduledIntervalMs }); + await this.onDequeue(response.data, { + dequeueResponseMs, + pollingIntervalMs: this.lastScheduledIntervalMs, + }); if (response.data.length > 0) { nextIntervalMs = this.intervalMs; diff --git a/packages/core/src/v3/runEngineWorker/supervisor/session.ts b/packages/core/src/v3/runEngineWorker/supervisor/session.ts index de823d792b7..10e70994a0c 100644 --- a/packages/core/src/v3/runEngineWorker/supervisor/session.ts +++ b/packages/core/src/v3/runEngineWorker/supervisor/session.ts @@ -87,7 +87,10 @@ export class SupervisorSession extends EventEmitter { }); } - private async onDequeue(messages: WorkerApiDequeueResponseBody, timing?: { dequeueResponseMs: number; pollingIntervalMs: number }): Promise { + private async onDequeue( + messages: WorkerApiDequeueResponseBody, + timing?: { dequeueResponseMs: number; pollingIntervalMs: number } + ): Promise { this.logger.verbose("Dequeued messages with contents", { count: messages.length, messages }); for (const message of messages) { diff --git a/packages/core/src/v3/runMetadata/noopManager.ts b/packages/core/src/v3/runMetadata/noopManager.ts index 28366b066b3..1573dcce535 100644 --- a/packages/core/src/v3/runMetadata/noopManager.ts +++ b/packages/core/src/v3/runMetadata/noopManager.ts @@ -48,7 +48,7 @@ export class NoopRunMetadataManager implements RunMetadataManager { get parent(): RunMetadataUpdater { // Store a reference to this object const self = this; - + // Create a local reference to ensure proper context const parentUpdater: RunMetadataUpdater = { append: () => parentUpdater, @@ -65,14 +65,14 @@ export class NoopRunMetadataManager implements RunMetadataManager { }), update: () => parentUpdater, }; - + return parentUpdater; } get root(): RunMetadataUpdater { // Store a reference to this object const self = this; - + // Create a local reference to ensure proper context const rootUpdater: RunMetadataUpdater = { append: () => rootUpdater, @@ -89,7 +89,7 @@ export class NoopRunMetadataManager implements RunMetadataManager { }), update: () => rootUpdater, }; - + return rootUpdater; } } diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index e1543529a48..52a353aaf6d 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -1,12 +1,6 @@ import { z } from "zod"; import { ConfigManifest } from "./config.js"; -import { - PromptManifest, - QueueManifest, - SkillManifest, - TaskFile, - TaskManifest, -} from "./schemas.js"; +import { PromptManifest, QueueManifest, SkillManifest, TaskFile, TaskManifest } from "./schemas.js"; export const BuildExternal = z.object({ name: z.string(), diff --git a/packages/core/src/v3/serverOnly/httpServer.ts b/packages/core/src/v3/serverOnly/httpServer.ts index 1dcddf8f15a..d0cd9275f8a 100644 --- a/packages/core/src/v3/serverOnly/httpServer.ts +++ b/packages/core/src/v3/serverOnly/httpServer.ts @@ -160,8 +160,14 @@ export class HttpServer { return reply.empty(405); } - const { handler, paramsSchema, querySchema, bodySchema, keepConnectionAlive, skipBodyParsing } = - routeDefinition; + const { + handler, + paramsSchema, + querySchema, + bodySchema, + keepConnectionAlive, + skipBodyParsing, + } = routeDefinition; const params = this.parseRouteParams(route, url); const parsedParams = this.optionalSchema(paramsSchema, params); diff --git a/packages/core/src/v3/serverOnly/idempotencyKeys.ts b/packages/core/src/v3/serverOnly/idempotencyKeys.ts index 91dac04fd43..c2a6a469902 100644 --- a/packages/core/src/v3/serverOnly/idempotencyKeys.ts +++ b/packages/core/src/v3/serverOnly/idempotencyKeys.ts @@ -37,7 +37,10 @@ export function extractIdempotencyKeyScope(run: { export function unsafeExtractIdempotencyKeyScope(run: { idempotencyKeyOptions: unknown; }): "run" | "attempt" | "global" | undefined { - const unsafe = run.idempotencyKeyOptions as { scope?: "run" | "attempt" | "global" } | undefined | null; + const unsafe = run.idempotencyKeyOptions as + | { scope?: "run" | "attempt" | "global" } + | undefined + | null; return unsafe?.scope ?? undefined; } @@ -65,5 +68,3 @@ export function unsafeExtractIdempotencyKeyUser(run: { return unsafe?.key ?? undefined; } - - diff --git a/packages/core/src/v3/sessionStreams/index.ts b/packages/core/src/v3/sessionStreams/index.ts index c150f0fe9df..b4e2e2eb291 100644 --- a/packages/core/src/v3/sessionStreams/index.ts +++ b/packages/core/src/v3/sessionStreams/index.ts @@ -1,10 +1,6 @@ import { getGlobal, registerGlobal } from "../utils/globals.js"; import { NoopSessionStreamManager } from "./noopManager.js"; -import { - InputStreamOncePromise, - SessionChannelIO, - SessionStreamManager, -} from "./types.js"; +import { InputStreamOncePromise, SessionChannelIO, SessionStreamManager } from "./types.js"; import { InputStreamOnceOptions } from "../realtimeStreams/types.js"; const API_NAME = "session-streams"; @@ -63,11 +59,7 @@ export class SessionStreamsAPI implements SessionStreamManager { return this.#getManager().lastDispatchedSeqNum(sessionId, io); } - public setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void { + public setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void { this.#getManager().setLastDispatchedSeqNum(sessionId, io, seqNum); } diff --git a/packages/core/src/v3/sessionStreams/manager.test.ts b/packages/core/src/v3/sessionStreams/manager.test.ts index 6089d705783..e01bd5fff87 100644 --- a/packages/core/src/v3/sessionStreams/manager.test.ts +++ b/packages/core/src/v3/sessionStreams/manager.test.ts @@ -52,7 +52,10 @@ describe("StandardSessionStreamManager — minTimestamp filter", () => { { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, { id: "1", chunk: { kind: "message", payload: { id: "u2" } }, timestamp: 2000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); const first = await manager.once(sessionId, io); expect(first).toEqual({ ok: true, output: { kind: "message", payload: { id: "u1" } } }); @@ -70,7 +73,10 @@ describe("StandardSessionStreamManager — minTimestamp filter", () => { { id: "1", chunk: { kind: "message", payload: { id: "u2" } }, timestamp: 2000 }, { id: "2", chunk: { kind: "message", payload: { id: "u3" } }, timestamp: 3000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); // Cutoff at 2000 (inclusive: `<=` is dropped). Only u3 should pass. manager.setMinTimestamp(sessionId, io, 2000); @@ -86,7 +92,10 @@ describe("StandardSessionStreamManager — minTimestamp filter", () => { const records = [ { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); manager.setMinTimestamp(sessionId, io, 5000); manager.setMinTimestamp(sessionId, io, undefined); @@ -99,9 +108,7 @@ describe("StandardSessionStreamManager — minTimestamp filter", () => { }); it("filter is per-(sessionId, io) and doesn't bleed across streams", async () => { - const inApi = singleShotApiClient([ - { id: "0", chunk: { kind: "in-record" }, timestamp: 1000 }, - ]); + const inApi = singleShotApiClient([{ id: "0", chunk: { kind: "in-record" }, timestamp: 1000 }]); const manager = new StandardSessionStreamManager(inApi, "http://localhost"); manager.setMinTimestamp(sessionId, "in", 5000); @@ -137,7 +144,10 @@ describe("StandardSessionStreamManager — minTimestamp filter", () => { const records = [ { id: "0", chunk: { kind: "message", payload: { id: "u1" } }, timestamp: 1000 }, ]; - const manager = new StandardSessionStreamManager(singleShotApiClient(records), "http://localhost"); + const manager = new StandardSessionStreamManager( + singleShotApiClient(records), + "http://localhost" + ); manager.setMinTimestamp(sessionId, io, 5000); manager.reset(); diff --git a/packages/core/src/v3/sessionStreams/manager.ts b/packages/core/src/v3/sessionStreams/manager.ts index 65eaf40f9cc..1c9af1c7d6b 100644 --- a/packages/core/src/v3/sessionStreams/manager.ts +++ b/packages/core/src/v3/sessionStreams/manager.ts @@ -98,11 +98,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { private debug: boolean = false ) {} - on( - sessionId: string, - io: SessionChannelIO, - handler: SessionStreamHandler - ): { off: () => void } { + on(sessionId: string, io: SessionChannelIO, handler: SessionStreamHandler): { off: () => void } { const key = keyFor(sessionId, io); let handlerSet = this.handlers.get(key); @@ -250,11 +246,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { return this.lastDispatchedSeqNums.get(keyFor(sessionId, io)); } - setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void { + setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void { this.#advanceLastDispatched(keyFor(sessionId, io), seqNum); } @@ -265,11 +257,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { } } - setMinTimestamp( - sessionId: string, - io: SessionChannelIO, - minTimestamp: number | undefined - ): void { + setMinTimestamp(sessionId: string, io: SessionChannelIO, minTimestamp: number | undefined): void { const key = keyFor(sessionId, io); if (minTimestamp === undefined) { this.minTimestamps.delete(key); @@ -397,8 +385,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { } const hasHandlers = this.handlers.has(key) && this.handlers.get(key)!.size > 0; - const hasWaiters = - this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; + const hasWaiters = this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; if (hasHandlers || hasWaiters) { // Exponential backoff with jitter. 1s base, doubling each // attempt, capped at 30s. Without this, a persistent backend @@ -415,8 +402,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { // handlers/waiters means we should stay quiet. if (this.tails.has(key)) return; if (this.explicitlyDisconnected.has(key)) return; - const stillHasHandlers = - this.handlers.has(key) && this.handlers.get(key)!.size > 0; + const stillHasHandlers = this.handlers.has(key) && this.handlers.get(key)!.size > 0; const stillHasWaiters = this.onceWaiters.has(key) && this.onceWaiters.get(key)!.length > 0; if (!stillHasHandlers && !stillHasWaiters) return; @@ -427,11 +413,7 @@ export class StandardSessionStreamManager implements SessionStreamManager { this.tails.set(key, { abortController, promise }); } - async #runTail( - sessionId: string, - io: SessionChannelIO, - signal: AbortSignal - ): Promise { + async #runTail(sessionId: string, io: SessionChannelIO, signal: AbortSignal): Promise { const key = keyFor(sessionId, io); try { const lastSeq = this.seqNums.get(key); diff --git a/packages/core/src/v3/sessionStreams/noopManager.ts b/packages/core/src/v3/sessionStreams/noopManager.ts index 77d1f40ac9f..5284c9a9fed 100644 --- a/packages/core/src/v3/sessionStreams/noopManager.ts +++ b/packages/core/src/v3/sessionStreams/noopManager.ts @@ -35,11 +35,7 @@ export class NoopSessionStreamManager implements SessionStreamManager { return undefined; } - setLastDispatchedSeqNum( - _sessionId: string, - _io: SessionChannelIO, - _seqNum: number - ): void {} + setLastDispatchedSeqNum(_sessionId: string, _io: SessionChannelIO, _seqNum: number): void {} setMinTimestamp( _sessionId: string, diff --git a/packages/core/src/v3/sessionStreams/types.ts b/packages/core/src/v3/sessionStreams/types.ts index 45be5149e2e..bd98fa04385 100644 --- a/packages/core/src/v3/sessionStreams/types.ts +++ b/packages/core/src/v3/sessionStreams/types.ts @@ -70,11 +70,7 @@ export interface SessionStreamManager { * `session-in-event-id` header on the latest `turn-complete` on * `.out`. Monotonic: only ever advances forward, never backwards. */ - setLastDispatchedSeqNum( - sessionId: string, - io: SessionChannelIO, - seqNum: number - ): void; + setLastDispatchedSeqNum(sessionId: string, io: SessionChannelIO, seqNum: number): void; /** * Set a per-stream lower-bound SSE timestamp. Records whose timestamp @@ -84,11 +80,7 @@ export interface SessionStreamManager { * * Pass `undefined` to clear the filter. */ - setMinTimestamp( - sessionId: string, - io: SessionChannelIO, - minTimestamp: number | undefined - ): void; + setMinTimestamp(sessionId: string, io: SessionChannelIO, minTimestamp: number | undefined): void; /** Remove and discard the first buffered record. Returns true if one was removed. */ shiftBuffer(sessionId: string, io: SessionChannelIO): boolean; diff --git a/packages/core/src/v3/taskContext/otelProcessors.ts b/packages/core/src/v3/taskContext/otelProcessors.ts index fc30e9d1145..452ecf391a0 100644 --- a/packages/core/src/v3/taskContext/otelProcessors.ts +++ b/packages/core/src/v3/taskContext/otelProcessors.ts @@ -190,8 +190,7 @@ export class TaskContextMetricExporter implements PushMetricExporter { } if (taskContext.conversationId) { - contextAttrs[SemanticInternalAttributes.GEN_AI_CONVERSATION_ID] = - taskContext.conversationId; + contextAttrs[SemanticInternalAttributes.GEN_AI_CONVERSATION_ID] = taskContext.conversationId; } const modified: ResourceMetrics = { @@ -288,7 +287,10 @@ export class BufferingMetricExporter implements PushMetricExporter { const base = batch[0]!; // Merge all scopeMetrics by scope name, then metrics by descriptor name - const scopeMap = new Map }>(); + const scopeMap = new Map< + string, + { scope: ScopeMetrics["scope"]; metricsMap: Map } + >(); for (const rm of batch) { for (const sm of rm.scopeMetrics) { diff --git a/packages/core/src/v3/test/mock-task-context.ts b/packages/core/src/v3/test/mock-task-context.ts index 66e58490019..0da633c6a7e 100644 --- a/packages/core/src/v3/test/mock-task-context.ts +++ b/packages/core/src/v3/test/mock-task-context.ts @@ -258,15 +258,13 @@ export async function runInMockTaskContext( }, locals: { get: (key: LocalsKey) => localsManager.getLocal(key), - set: (key: LocalsKey, value: TValue) => - localsManager.setLocal(key, value), + set: (key: LocalsKey, value: TValue) => localsManager.setLocal(key, value), }, sessions: { in: { send: (sessionId, data, io = "in") => sessionStreamManager.__sendFromTest(sessionId, io, data), - close: (sessionId, io = "in") => - sessionStreamManager.__closeFromTest(sessionId, io), + close: (sessionId, io = "in") => sessionStreamManager.__closeFromTest(sessionId, io), }, }, ctx, diff --git a/packages/core/src/v3/test/test-input-stream-manager.ts b/packages/core/src/v3/test/test-input-stream-manager.ts index 933b92d07c6..4a8bbf20fe1 100644 --- a/packages/core/src/v3/test/test-input-stream-manager.ts +++ b/packages/core/src/v3/test/test-input-stream-manager.ts @@ -184,9 +184,7 @@ export class TestInputStreamManager implements InputStreamManager { } if (hasHandlers) { - await Promise.all( - Array.from(handlers!).map((h) => Promise.resolve().then(() => h(data))) - ); + await Promise.all(Array.from(handlers!).map((h) => Promise.resolve().then(() => h(data)))); } } diff --git a/packages/core/src/v3/test/test-session-stream-manager.ts b/packages/core/src/v3/test/test-session-stream-manager.ts index f19e01dc33c..388c6dca877 100644 --- a/packages/core/src/v3/test/test-session-stream-manager.ts +++ b/packages/core/src/v3/test/test-session-stream-manager.ts @@ -4,10 +4,7 @@ import { InputStreamTimeoutError, } from "../inputStreams/types.js"; import type { InputStreamOnceOptions } from "../realtimeStreams/types.js"; -import type { - SessionChannelIO, - SessionStreamManager, -} from "../sessionStreams/types.js"; +import type { SessionChannelIO, SessionStreamManager } from "../sessionStreams/types.js"; type OnceWaiter = { resolve: (value: InputStreamOnceResult) => void; @@ -40,11 +37,7 @@ export class TestSessionStreamManager implements SessionStreamManager { private buffer = new Map(); private seqNums = new Map(); - on( - sessionId: string, - io: SessionChannelIO, - handler: Handler - ): { off: () => void } { + on(sessionId: string, io: SessionChannelIO, handler: Handler): { off: () => void } { const key = keyFor(sessionId, io); let set = this.handlers.get(key); @@ -167,11 +160,7 @@ export class TestSessionStreamManager implements SessionStreamManager { return undefined; } - setLastDispatchedSeqNum( - _sessionId: string, - _io: SessionChannelIO, - _seqNum: number - ): void { + setLastDispatchedSeqNum(_sessionId: string, _io: SessionChannelIO, _seqNum: number): void { // no-op — see comment on `lastDispatchedSeqNum`. } @@ -242,11 +231,7 @@ export class TestSessionStreamManager implements SessionStreamManager { * resolves. Consumption is decided on the synchronous return value, * exactly like production. */ - async __sendFromTest( - sessionId: string, - io: SessionChannelIO, - data: unknown - ): Promise { + async __sendFromTest(sessionId: string, io: SessionChannelIO, data: unknown): Promise { const key = keyFor(sessionId, io); const waiters = this.onceWaiters.get(key); diff --git a/packages/core/src/v3/timeout/api.ts b/packages/core/src/v3/timeout/api.ts index dd211bc42c0..864a46bbfe7 100644 --- a/packages/core/src/v3/timeout/api.ts +++ b/packages/core/src/v3/timeout/api.ts @@ -47,7 +47,9 @@ export class TimeoutAPI implements TimeoutManager { this.disable(); } - public registerListener(listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise) { + public registerListener( + listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise + ) { const manager = this.#getManager(); if (manager.registerListener) { manager.registerListener(listener); diff --git a/packages/core/src/v3/timeout/types.ts b/packages/core/src/v3/timeout/types.ts index 69786896768..03f773c2e54 100644 --- a/packages/core/src/v3/timeout/types.ts +++ b/packages/core/src/v3/timeout/types.ts @@ -2,7 +2,9 @@ export interface TimeoutManager { abortAfterTimeout: (timeoutInSeconds?: number) => AbortController; signal?: AbortSignal; reset: () => void; - registerListener?: (listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise) => void; + registerListener?: ( + listener: (timeoutInSeconds: number, elapsedTimeInSeconds: number) => void | Promise + ) => void; } export class TaskRunExceededMaxDuration extends Error { diff --git a/packages/core/src/v3/types/schemas.ts b/packages/core/src/v3/types/schemas.ts index e5ae9c3d88e..b4121029925 100644 --- a/packages/core/src/v3/types/schemas.ts +++ b/packages/core/src/v3/types/schemas.ts @@ -73,20 +73,18 @@ export type SchemaWithInputOutput = export type Schema = SchemaWithInputOutput | SchemaWithoutInput; -export type inferSchema = TSchema extends SchemaWithInputOutput< - infer $TIn, - infer $TOut -> - ? { - in: $TIn; - out: $TOut; - } - : TSchema extends SchemaWithoutInput - ? { - in: $InOut; - out: $InOut; - } - : never; +export type inferSchema = + TSchema extends SchemaWithInputOutput + ? { + in: $TIn; + out: $TOut; + } + : TSchema extends SchemaWithoutInput + ? { + in: $InOut; + out: $InOut; + } + : never; export type inferSchemaIn< TSchema extends Schema | undefined, diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index 978a6e5bd0a..f0c86de627d 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -475,21 +475,14 @@ export type BatchRunHandle = TPayload >; -export type RunHandleOutput = TRunHandle extends RunHandle - ? TOutput - : never; +export type RunHandleOutput = + TRunHandle extends RunHandle ? TOutput : never; -export type RunHandlePayload = TRunHandle extends RunHandle - ? TPayload - : never; +export type RunHandlePayload = + TRunHandle extends RunHandle ? TPayload : never; -export type RunHandleTaskIdentifier = TRunHandle extends RunHandle< - infer TTaskIdentifier, - any, - any -> - ? TTaskIdentifier - : never; +export type RunHandleTaskIdentifier = + TRunHandle extends RunHandle ? TTaskIdentifier : never; export type TaskRunResult = | { @@ -507,13 +500,10 @@ export type TaskRunResult = export type AnyTaskRunResult = TaskRunResult; -export type TaskRunResultFromTask = TTask extends Task< - infer TIdentifier, - any, - infer TOutput -> - ? TaskRunResult - : never; +export type TaskRunResultFromTask = + TTask extends Task + ? TaskRunResult + : never; export type BatchResult = { id: string; @@ -668,7 +658,7 @@ export interface Task */ triggerAndSubscribe: ( payload: TInput, - options?: TriggerAndSubscribeOptions, + options?: TriggerAndSubscribeOptions ) => TaskRunPromise; /** @@ -722,33 +712,24 @@ export interface ToolTask< export type AnyTask = Task; -export type TaskPayload = TTask extends Task - ? TInput - : never; +export type TaskPayload = + TTask extends Task ? TInput : never; -export type TaskOutput = TTask extends Task - ? TOutput - : never; +export type TaskOutput = + TTask extends Task ? TOutput : never; -export type TaskOutputHandle = TTask extends Task< - infer TIdentifier, - infer TInput, - infer TOutput -> - ? RunHandle - : never; +export type TaskOutputHandle = + TTask extends Task + ? RunHandle + : never; -export type TaskBatchOutputHandle = TTask extends Task< - infer TIdentifier, - infer TInput, - infer TOutput -> - ? BatchRunHandle - : never; +export type TaskBatchOutputHandle = + TTask extends Task + ? BatchRunHandle + : never; -export type TaskIdentifier = TTask extends Task - ? TIdentifier - : never; +export type TaskIdentifier = + TTask extends Task ? TIdentifier : never; export type TaskFromIdentifier< TTask extends AnyTask, @@ -1087,17 +1068,14 @@ export type RunTypes = { export type AnyRunTypes = RunTypes; -export type InferRunTypes = T extends RunHandle< - infer TTaskIdentifier, - infer TPayload, - infer TOutput -> - ? RunTypes - : T extends BatchedRunHandle - ? RunTypes - : T extends Task - ? RunTypes - : AnyRunTypes; +export type InferRunTypes = + T extends RunHandle + ? RunTypes + : T extends BatchedRunHandle + ? RunTypes + : T extends Task + ? RunTypes + : AnyRunTypes; export type RunHandleFromTypes = RunHandle< TRunTypes["taskIdentifier"], diff --git a/packages/core/src/v3/types/tools.ts b/packages/core/src/v3/types/tools.ts index 8c3aac80bf1..b77ef8ffd4f 100644 --- a/packages/core/src/v3/types/tools.ts +++ b/packages/core/src/v3/types/tools.ts @@ -8,8 +8,8 @@ export type inferToolParameters = PARAMETERS extends AISchema ? PARAMETERS["_type"] : PARAMETERS extends z.ZodTypeAny - ? z.infer - : never; + ? z.infer + : never; export function convertToolParametersToSchema( toolParameters: TToolParameters diff --git a/packages/core/src/v3/utils/durations.ts b/packages/core/src/v3/utils/durations.ts index 7bec968fe6e..f588fcb8f05 100644 --- a/packages/core/src/v3/utils/durations.ts +++ b/packages/core/src/v3/utils/durations.ts @@ -46,8 +46,8 @@ export function formatDurationMilliseconds( units: options?.units ? options.units : milliseconds < 1000 - ? belowOneSecondUnits - : aboveOneSecondUnits, + ? belowOneSecondUnits + : aboveOneSecondUnits, maxDecimalPoints: options?.maxDecimalPoints ?? 1, largest: options?.maxUnits ?? 2, }); diff --git a/packages/core/src/v3/utils/reconnectBackoff.test.ts b/packages/core/src/v3/utils/reconnectBackoff.test.ts index 5de5a2db8e8..c0ac540edf0 100644 --- a/packages/core/src/v3/utils/reconnectBackoff.test.ts +++ b/packages/core/src/v3/utils/reconnectBackoff.test.ts @@ -48,9 +48,7 @@ describe("computeReconnectDelayMs", () => { it("never exceeds RECONNECT_BACKOFF_MAX_MS + 1000ms (cap + jitter ceiling)", () => { withFixedRandom(0.999, () => { for (let attempt = 0; attempt < 100; attempt++) { - expect(computeReconnectDelayMs(attempt)).toBeLessThan( - RECONNECT_BACKOFF_MAX_MS + 1000 - ); + expect(computeReconnectDelayMs(attempt)).toBeLessThan(RECONNECT_BACKOFF_MAX_MS + 1000); } }); }); diff --git a/packages/core/src/v3/utils/structuredLogger.ts b/packages/core/src/v3/utils/structuredLogger.ts index 1aae399bbf8..97049a9090a 100644 --- a/packages/core/src/v3/utils/structuredLogger.ts +++ b/packages/core/src/v3/utils/structuredLogger.ts @@ -26,8 +26,8 @@ export class SimpleStructuredLogger implements StructuredLogger { private level: LogLevel = ["1", "true"].includes(process.env.VERBOSE ?? "") ? LogLevel.verbose : ["1", "true"].includes(process.env.DEBUG ?? "") - ? LogLevel.debug - : LogLevel.info, + ? LogLevel.debug + : LogLevel.info, private fields?: Record ) {} diff --git a/packages/core/src/v3/waitpoints/index.ts b/packages/core/src/v3/waitpoints/index.ts index fc70ff57f21..5aa238a57ff 100644 --- a/packages/core/src/v3/waitpoints/index.ts +++ b/packages/core/src/v3/waitpoints/index.ts @@ -7,15 +7,11 @@ export class WaitpointTimeoutError extends Error { } } -export class ManualWaitpointPromise extends Promise< - WaitpointTokenTypedResult -> { +export class ManualWaitpointPromise extends Promise> { constructor( executor: ( resolve: ( - value: - | WaitpointTokenTypedResult - | PromiseLike> + value: WaitpointTokenTypedResult | PromiseLike> ) => void, reject: (reason?: any) => void ) => void diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 838ef3c6e77..40a5932e963 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -1418,8 +1418,8 @@ export class TaskExecutor { error instanceof Error ? `${error.name}: ${error.message}` : typeof error === "string" - ? error - : undefined, + ? error + : undefined, stackTrace: error instanceof Error ? error.stack : undefined, }, skippedRetrying, diff --git a/packages/core/test/errors.test.ts b/packages/core/test/errors.test.ts index 4bcf89d7e9e..930286996f8 100644 --- a/packages/core/test/errors.test.ts +++ b/packages/core/test/errors.test.ts @@ -112,7 +112,9 @@ describe("parseError truncation", () => { expect(parsed.type).toBe("BUILT_IN_ERROR"); if (parsed.type === "BUILT_IN_ERROR") { - const frameLines = parsed.stackTrace.split("\n").filter((l) => l.trimStart().startsWith("at ")); + const frameLines = parsed.stackTrace + .split("\n") + .filter((l) => l.trimStart().startsWith("at ")); expect(frameLines.length).toBe(50); expect(parsed.stackTrace).toContain("frames omitted"); } @@ -139,7 +141,9 @@ describe("sanitizeError truncation", () => { }); if (result.type === "BUILT_IN_ERROR") { - const frameLines = result.stackTrace.split("\n").filter((l) => l.trimStart().startsWith("at ")); + const frameLines = result.stackTrace + .split("\n") + .filter((l) => l.trimStart().startsWith("at ")); expect(frameLines.length).toBe(50); } }); @@ -249,7 +253,7 @@ describe("truncateStack message line bounding", () => { describe("shouldRetryError + shouldLookupRetrySettings", () => { const internal = (code: string): TaskRunError => - ({ type: "INTERNAL_ERROR", code } as TaskRunError); + ({ type: "INTERNAL_ERROR", code }) as TaskRunError; it("retries SIGSEGV (changed from non-retriable) and looks up retry settings", () => { const err = internal("TASK_PROCESS_SIGSEGV"); diff --git a/packages/core/test/externalSpanExporterWrapper.test.ts b/packages/core/test/externalSpanExporterWrapper.test.ts index 4cff69e3aea..9b51653a1ec 100644 --- a/packages/core/test/externalSpanExporterWrapper.test.ts +++ b/packages/core/test/externalSpanExporterWrapper.test.ts @@ -62,10 +62,7 @@ describe("ExternalSpanExporterWrapper warm-start regression", () => { manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_A } }; - const wrapper = new ExternalSpanExporterWrapper( - exporter, - "ffffffffffffffffffffffffffffffff" - ); + const wrapper = new ExternalSpanExporterWrapper(exporter, "ffffffffffffffffffffffffffffffff"); manager.traceContext = { external: { traceparent: TRACEPARENT_RUN_B } }; diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 3fd11fa5d04..345a5f42fc6 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -674,45 +674,35 @@ describe("unflattenAttributes", () => { // unflattener used to crash with TypeError when it tried to descend into a // primitive sibling. it("does not throw when a scalar precedes a deeper path at the same prefix", () => { - expect(() => - unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "scalar", "a.b.c": "value" })).toEqual({ a: { b: { c: "value" } }, }); }); it("does not throw when a deeper path precedes a scalar at the same prefix", () => { - expect(() => - unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })).not.toThrow(); expect(unflattenAttributes({ "a.b.c": "value", "a.b": "scalar" })).toEqual({ a: { b: "scalar" }, }); }); it("treats an intermediate null sentinel as overwritable when a deeper path follows", () => { - expect(() => - unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "$@null((", "a.b.c": "value" })).toEqual({ a: { b: { c: "value" } }, }); }); it("does not throw when a scalar prefix conflicts with a numeric-index path", () => { - expect(() => - unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })).not.toThrow(); expect(unflattenAttributes({ "a.b": "scalar", "a.b.[0]": "indexed" })).toEqual({ a: { b: ["indexed"] }, }); }); it("converts an existing object slot to an array when a numeric-index path follows", () => { - expect(() => - unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" }) - ).not.toThrow(); + expect(() => unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })).not.toThrow(); expect(unflattenAttributes({ "a.b.c": "value", "a.b.[0]": "indexed" })).toEqual({ a: { b: ["indexed"] }, }); diff --git a/packages/core/test/recordSpanException.test.ts b/packages/core/test/recordSpanException.test.ts index 6b2d194ce25..67e1aa29b02 100644 --- a/packages/core/test/recordSpanException.test.ts +++ b/packages/core/test/recordSpanException.test.ts @@ -6,7 +6,10 @@ function createMockSpan() { return { recordException: vi.fn(), setStatus: vi.fn(), - } as unknown as Span & { recordException: ReturnType; setStatus: ReturnType }; + } as unknown as Span & { + recordException: ReturnType; + setStatus: ReturnType; + }; } describe("recordSpanException", () => { diff --git a/packages/core/test/resourceCatalog.test.ts b/packages/core/test/resourceCatalog.test.ts index a34e9373f0a..5d4e7708895 100644 --- a/packages/core/test/resourceCatalog.test.ts +++ b/packages/core/test/resourceCatalog.test.ts @@ -82,8 +82,7 @@ describe("StandardResourceCatalog — runtime registration via sentinel context" vi.spyOn(console, "warn").mockImplementation(() => {}); const catalog = new StandardResourceCatalog(); - (globalThis as { __catalogRegisterTaskMetadata?: unknown }) - .__catalogRegisterTaskMetadata = ( + (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = ( task: Parameters[0] ) => { catalog.registerTaskMetadata(task); @@ -124,33 +123,26 @@ describe("StandardResourceCatalog — runtime registration via sentinel context" catalog.clearCurrentFileContext(); }); - it( - "control: real file context registers without firing the sentinel warning", - async () => { - const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); - const catalog = new StandardResourceCatalog(); + it("control: real file context registers without firing the sentinel warning", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const catalog = new StandardResourceCatalog(); - (globalThis as { __catalogRegisterTaskMetadata?: unknown }) - .__catalogRegisterTaskMetadata = ( - task: Parameters[0] - ) => { - catalog.registerTaskMetadata(task); - }; + (globalThis as { __catalogRegisterTaskMetadata?: unknown }).__catalogRegisterTaskMetadata = ( + task: Parameters[0] + ) => { + catalog.registerTaskMetadata(task); + }; - catalog.setCurrentFileContext( - "/app/dist/lazy-task.entry.mjs", - "src/tasks/lazy-task.ts" - ); - await import("./fixtures/dynamic-task-module.mjs?control"); - catalog.clearCurrentFileContext(); + catalog.setCurrentFileContext("/app/dist/lazy-task.entry.mjs", "src/tasks/lazy-task.ts"); + await import("./fixtures/dynamic-task-module.mjs?control"); + catalog.clearCurrentFileContext(); - const task = catalog.getTask("lazy-task"); - expect(task).toBeDefined(); - expect(task?.filePath).toBe("/app/dist/lazy-task.entry.mjs"); - expect(task?.entryPoint).toBe("src/tasks/lazy-task.ts"); - expect(warn).not.toHaveBeenCalled(); - } - ); + const task = catalog.getTask("lazy-task"); + expect(task).toBeDefined(); + expect(task?.filePath).toBe("/app/dist/lazy-task.entry.mjs"); + expect(task?.entryPoint).toBe("src/tasks/lazy-task.ts"); + expect(warn).not.toHaveBeenCalled(); + }); }); describe("StandardResourceCatalog — duplicate task id collisions", () => { diff --git a/packages/core/test/skillCatalog.test.ts b/packages/core/test/skillCatalog.test.ts index 3f1d29bf572..eb00e5b56e2 100644 --- a/packages/core/test/skillCatalog.test.ts +++ b/packages/core/test/skillCatalog.test.ts @@ -69,6 +69,11 @@ describe("StandardResourceCatalog — skills", () => { catalog.registerSkillMetadata({ id: "pdf", sourcePath: "./skills/pdf" }); catalog.registerSkillMetadata({ id: "researcher", sourcePath: "./skills/researcher" }); - expect(catalog.listSkillManifests().map((s) => s.id).sort()).toEqual(["pdf", "researcher"]); + expect( + catalog + .listSkillManifests() + .map((s) => s.id) + .sort() + ).toEqual(["pdf", "researcher"]); }); }); diff --git a/packages/plugins/src/rbac.ts b/packages/plugins/src/rbac.ts index ca9a1a0494f..189252afd58 100644 --- a/packages/plugins/src/rbac.ts +++ b/packages/plugins/src/rbac.ts @@ -397,10 +397,7 @@ export interface RoleBaseAccessController { // Org-scoped only — project-scoped reads still go through getUserRole. // Returns a Map keyed by userId; users with no resolvable role map to // null. The default fallback returns a Map of all userIds → null. - getUserRoles( - userIds: string[], - organizationId: string - ): Promise>; + getUserRoles(userIds: string[], organizationId: string): Promise>; setUserRole(params: { userId: string; @@ -422,9 +419,7 @@ export interface RoleBaseAccessController { // Mutation result for role create/update — success carries the new // `role`, failure carries a user-facing `error` string. -export type RoleMutationResult = - | { ok: true; role: Role } - | { ok: false; error: string }; +export type RoleMutationResult = { ok: true; role: Role } | { ok: false; error: string }; // Result for assignment / deletion mutations that don't return a value. export type RoleAssignmentResult = { ok: true } | { ok: false; error: string }; diff --git a/packages/plugins/src/sso.ts b/packages/plugins/src/sso.ts index 7939b7b70d0..4805195e411 100644 --- a/packages/plugins/src/sso.ts +++ b/packages/plugins/src/sso.ts @@ -32,9 +32,7 @@ export type OrgSsoStatus = { }>; }; -export type SsoRouteDecision = - | { kind: "no_sso" } - | { kind: "sso_required"; idpOrgId: string }; +export type SsoRouteDecision = { kind: "no_sso" } | { kind: "sso_required"; idpOrgId: string }; export const SSO_FLOWS = [ "user_initiated", @@ -65,10 +63,7 @@ export type SsoResolutionDecision = export type SsoDecisionError = "internal"; -export type SsoBeginError = - | "no_org_for_domain" - | "no_active_connection" - | "feature_disabled"; +export type SsoBeginError = "no_org_for_domain" | "no_active_connection" | "feature_disabled"; export type SsoCompleteError = | "state_replayed_or_expired" diff --git a/packages/redis-worker/package.json b/packages/redis-worker/package.json index 22b8a1b62d8..9c4b228477e 100644 --- a/packages/redis-worker/package.json +++ b/packages/redis-worker/package.json @@ -54,4 +54,4 @@ "require": "./dist/index.cjs" } } -} \ No newline at end of file +} diff --git a/packages/redis-worker/src/fair-queue/index.ts b/packages/redis-worker/src/fair-queue/index.ts index 0c0b7921a6b..219dde58910 100644 --- a/packages/redis-worker/src/fair-queue/index.ts +++ b/packages/redis-worker/src/fair-queue/index.ts @@ -192,7 +192,6 @@ export class FairQueue { shardCount: this.shardCount, }); - if (options.concurrencyGroups && options.concurrencyGroups.length > 0) { this.concurrencyManager = new ConcurrencyManager({ redis: options.redis, @@ -1248,11 +1247,11 @@ export class FairQueue { } const descriptor: QueueDescriptor = storedMessage - ? this.queueDescriptorCache.get(queueId) ?? { + ? (this.queueDescriptorCache.get(queueId) ?? { id: queueId, tenantId: storedMessage.tenantId, metadata: storedMessage.metadata ?? {}, - } + }) : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; // Complete in visibility manager @@ -1264,10 +1263,7 @@ export class FairQueue { } // Update both old and new indexes, clean up caches if queue is empty - const { queueEmpty } = await this.#updateAllIndexesAfterDequeue( - queueId, - descriptor.tenantId - ); + const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId); if (queueEmpty) { this.queueDescriptorCache.delete(queueId); this.queueCooloffStates.delete(queueId); @@ -1306,11 +1302,11 @@ export class FairQueue { } const descriptor: QueueDescriptor = storedMessage - ? this.queueDescriptorCache.get(queueId) ?? { + ? (this.queueDescriptorCache.get(queueId) ?? { id: queueId, tenantId: storedMessage.tenantId, metadata: storedMessage.metadata ?? {}, - } + }) : { id: queueId, tenantId: this.keys.extractTenantId(queueId), metadata: {} }; // Release back to queue (visibility manager updates dispatch indexes atomically) diff --git a/packages/redis-worker/src/fair-queue/scheduler.ts b/packages/redis-worker/src/fair-queue/scheduler.ts index 00a22bb97a8..318424b7cd0 100644 --- a/packages/redis-worker/src/fair-queue/scheduler.ts +++ b/packages/redis-worker/src/fair-queue/scheduler.ts @@ -114,4 +114,3 @@ export class NoopScheduler extends BaseScheduler { return []; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/drr.ts b/packages/redis-worker/src/fair-queue/schedulers/drr.ts index 3e05ae8a34f..43d0c01dbb9 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/drr.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/drr.ts @@ -103,9 +103,7 @@ export class DRRScheduler extends BaseScheduler { ); // Filter out tenants at capacity or with no deficit - const eligibleTenants = tenantData.filter( - (t) => !t.isAtCapacity && t.deficit >= 1 - ); + const eligibleTenants = tenantData.filter((t) => !t.isAtCapacity && t.deficit >= 1); // Log tenants blocked by capacity const blockedTenants = tenantData.filter((t) => t.isAtCapacity); @@ -451,11 +449,6 @@ declare module "@internal/redis" { drrDecrementDeficit(deficitKey: string, tenantId: string): Promise; - drrDecrementDeficitBatch( - deficitKey: string, - tenantId: string, - count: string - ): Promise; + drrDecrementDeficitBatch(deficitKey: string, tenantId: string, count: string): Promise; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/index.ts b/packages/redis-worker/src/fair-queue/schedulers/index.ts index bef962f58d7..328cdf0ec33 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/index.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/index.ts @@ -5,4 +5,3 @@ export { DRRScheduler } from "./drr.js"; export { WeightedScheduler } from "./weighted.js"; export { RoundRobinScheduler } from "./roundRobin.js"; - diff --git a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts index ac559673526..4e7d740d4bd 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/roundRobin.ts @@ -154,4 +154,3 @@ export class RoundRobinScheduler extends BaseScheduler { return [...array.slice(normalizedIndex), ...array.slice(0, normalizedIndex)]; } } - diff --git a/packages/redis-worker/src/fair-queue/schedulers/weighted.ts b/packages/redis-worker/src/fair-queue/schedulers/weighted.ts index de0d45d3cf2..7b44a7df177 100644 --- a/packages/redis-worker/src/fair-queue/schedulers/weighted.ts +++ b/packages/redis-worker/src/fair-queue/schedulers/weighted.ts @@ -80,11 +80,7 @@ export class WeightedScheduler extends BaseScheduler { consumerId: string, context: SchedulerContext ): Promise { - const snapshot = await this.#getOrCreateSnapshot( - masterQueueShard, - consumerId, - context - ); + const snapshot = await this.#getOrCreateSnapshot(masterQueueShard, consumerId, context); if (snapshot.queues.length === 0) { return []; @@ -431,4 +427,3 @@ export class WeightedScheduler extends BaseScheduler { return result; } } - diff --git a/packages/redis-worker/src/fair-queue/telemetry.ts b/packages/redis-worker/src/fair-queue/telemetry.ts index e9531fc812a..fa47ed93c06 100644 --- a/packages/redis-worker/src/fair-queue/telemetry.ts +++ b/packages/redis-worker/src/fair-queue/telemetry.ts @@ -334,7 +334,6 @@ export class FairQueueTelemetry { } }); } - } // ============================================================================ diff --git a/packages/redis-worker/src/fair-queue/tests/drr.test.ts b/packages/redis-worker/src/fair-queue/tests/drr.test.ts index 66575d6486f..e0f1dc78758 100644 --- a/packages/redis-worker/src/fair-queue/tests/drr.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/drr.test.ts @@ -9,111 +9,127 @@ describe("DRRScheduler", () => { let keys: FairQueueKeyProducer; describe("deficit management", () => { - redisTest("should initialize deficit to 0 for new tenants", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + redisTest( + "should initialize deficit to 0 for new tenants", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const deficit = await scheduler.getDeficit("new-tenant"); - expect(deficit).toBe(0); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - await scheduler.close(); - }); + const deficit = await scheduler.getDeficit("new-tenant"); + expect(deficit).toBe(0); - redisTest("should add quantum atomically with capping", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + await scheduler.close(); + } + ); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + redisTest( + "should add quantum atomically with capping", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - // Setup: put queues in the master shard - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); - - await redis.zadd(masterKey, now, "tenant:t1:queue:q1"); - - // Create context mock - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - // Run multiple iterations to accumulate deficit - for (let i = 0; i < 15; i++) { - await scheduler.selectQueues(masterKey, "consumer-1", context); - } + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Deficit should be capped at maxDeficit (50) - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBeLessThanOrEqual(50); + // Setup: put queues in the master shard + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + await redis.zadd(masterKey, now, "tenant:t1:queue:q1"); + + // Create context mock + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + // Run multiple iterations to accumulate deficit + for (let i = 0; i < 15; i++) { + await scheduler.selectQueues(masterKey, "consumer-1", context); + } + + // Deficit should be capped at maxDeficit (50) + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBeLessThanOrEqual(50); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should decrement deficit when processing", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should decrement deficit when processing", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Manually set some deficit - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "10"); + // Manually set some deficit + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "10"); - // Record processing - await scheduler.recordProcessed("t1", "queue:q1"); + // Record processing + await scheduler.recordProcessed("t1", "queue:q1"); - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBe(9); + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBe(9); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should not go below 0 on decrement", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should not go below 0 on decrement", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "0.5"); + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "0.5"); - await scheduler.recordProcessed("t1", "queue:q1"); + await scheduler.recordProcessed("t1", "queue:q1"); - const deficit = await scheduler.getDeficit("t1"); - expect(deficit).toBe(0); + const deficit = await scheduler.getDeficit("t1"); + expect(deficit).toBe(0); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); redisTest( "should decrement deficit by count when using recordProcessedBatch", @@ -198,191 +214,219 @@ describe("DRRScheduler", () => { }); describe("queue selection", () => { - redisTest("should return queues grouped by tenant", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); - - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); - - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); - - // Add queues for different tenants (all timestamps in the past) - await redis.zadd( - masterKey, - now - 200, - "tenant:t1:queue:q1", - now - 100, - "tenant:t1:queue:q2", - now - 50, - "tenant:t2:queue:q1" - ); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Should have both tenants - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).toContain("t1"); - expect(tenantIds).toContain("t2"); - - // t1 should have 2 queues - const t1 = result.find((r) => r.tenantId === "t1"); - expect(t1?.queues).toHaveLength(2); - - await scheduler.close(); - await redis.quit(); - }); - - redisTest("should filter out tenants at capacity", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should return queues grouped by tenant", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + // Add queues for different tenants (all timestamps in the past) + await redis.zadd( + masterKey, + now - 200, + "tenant:t1:queue:q1", + now - 100, + "tenant:t1:queue:q2", + now - 50, + "tenant:t2:queue:q1" + ); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Should have both tenants + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).toContain("t1"); + expect(tenantIds).toContain("t2"); + + // t1 should have 2 queues + const t1 = result.find((r) => r.tenantId === "t1"); + expect(t1?.queues).toHaveLength(2); - await redis.zadd(masterKey, now - 100, "tenant:t1:queue:q1", now - 50, "tenant:t2:queue:q1"); + await scheduler.close(); + await redis.quit(); + } + ); - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async (_, groupId) => groupId === "t1", // t1 at capacity - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; + redisTest( + "should filter out tenants at capacity", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - // Only t2 should be returned - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).not.toContain("t1"); - expect(tenantIds).toContain("t2"); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); + + await redis.zadd( + masterKey, + now - 100, + "tenant:t1:queue:q1", + now - 50, + "tenant:t2:queue:q1" + ); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async (_, groupId) => groupId === "t1", // t1 at capacity + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Only t2 should be returned + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).not.toContain("t1"); + expect(tenantIds).toContain("t2"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should skip tenants with insufficient deficit", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should skip tenants with insufficient deficit", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); - await redis.zadd(masterKey, now - 100, "tenant:t1:queue:q1", now - 50, "tenant:t2:queue:q1"); + await redis.zadd( + masterKey, + now - 100, + "tenant:t1:queue:q1", + now - 50, + "tenant:t2:queue:q1" + ); - // Set t1 deficit to 0 (no credits) - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "0"); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - // First call adds quantum to both tenants - // t1: 0 + 5 = 5, t2: 0 + 5 = 5 - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Both should be returned (both have deficit >= 1 after quantum added) - const tenantIds = result.map((r) => r.tenantId); - expect(tenantIds).toContain("t1"); - expect(tenantIds).toContain("t2"); + // Set t1 deficit to 0 (no credits) + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "0"); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + // First call adds quantum to both tenants + // t1: 0 + 5 = 5, t2: 0 + 5 = 5 + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Both should be returned (both have deficit >= 1 after quantum added) + const tenantIds = result.map((r) => r.tenantId); + expect(tenantIds).toContain("t1"); + expect(tenantIds).toContain("t2"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); - redisTest("should order tenants by deficit (highest first)", { timeout: 10000 }, async ({ redisOptions }) => { - keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); - const redis = createRedisClient(redisOptions); + redisTest( + "should order tenants by deficit (highest first)", + { timeout: 10000 }, + async ({ redisOptions }) => { + keys = new DefaultFairQueueKeyProducer({ prefix: "test" }); + const redis = createRedisClient(redisOptions); - const scheduler = new DRRScheduler({ - redis: redisOptions, - keys, - quantum: 5, - maxDeficit: 50, - }); + const scheduler = new DRRScheduler({ + redis: redisOptions, + keys, + quantum: 5, + maxDeficit: 50, + }); - const masterKey = keys.masterQueueKey(0); - const now = Date.now(); + const masterKey = keys.masterQueueKey(0); + const now = Date.now(); - await redis.zadd( - masterKey, - now - 300, - "tenant:t1:queue:q1", - now - 200, - "tenant:t2:queue:q1", - now - 100, - "tenant:t3:queue:q1" - ); + await redis.zadd( + masterKey, + now - 300, + "tenant:t1:queue:q1", + now - 200, + "tenant:t2:queue:q1", + now - 100, + "tenant:t3:queue:q1" + ); - // Set different deficits - const deficitKey = `test:drr:deficit`; - await redis.hset(deficitKey, "t1", "10"); - await redis.hset(deficitKey, "t2", "30"); - await redis.hset(deficitKey, "t3", "20"); - - const context: SchedulerContext = { - getCurrentConcurrency: async () => 0, - getConcurrencyLimit: async () => 100, - isAtCapacity: async () => false, - getQueueDescriptor: (queueId) => ({ - id: queueId, - tenantId: keys.extractTenantId(queueId), - metadata: {}, - }), - }; - - const result = await scheduler.selectQueues(masterKey, "consumer-1", context); - - // Should be ordered by deficit: t2 (35), t3 (25), t1 (15) - // (original + quantum of 5) - expect(result[0]?.tenantId).toBe("t2"); - expect(result[1]?.tenantId).toBe("t3"); - expect(result[2]?.tenantId).toBe("t1"); + // Set different deficits + const deficitKey = `test:drr:deficit`; + await redis.hset(deficitKey, "t1", "10"); + await redis.hset(deficitKey, "t2", "30"); + await redis.hset(deficitKey, "t3", "20"); + + const context: SchedulerContext = { + getCurrentConcurrency: async () => 0, + getConcurrencyLimit: async () => 100, + isAtCapacity: async () => false, + getQueueDescriptor: (queueId) => ({ + id: queueId, + tenantId: keys.extractTenantId(queueId), + metadata: {}, + }), + }; + + const result = await scheduler.selectQueues(masterKey, "consumer-1", context); + + // Should be ordered by deficit: t2 (35), t3 (25), t1 (15) + // (original + quantum of 5) + expect(result[0]?.tenantId).toBe("t2"); + expect(result[1]?.tenantId).toBe("t3"); + expect(result[2]?.tenantId).toBe("t1"); - await scheduler.close(); - await redis.quit(); - }); + await scheduler.close(); + await redis.quit(); + } + ); }); describe("get all deficits", () => { diff --git a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts index f1634ad2fe0..7a6d7c6a576 100644 --- a/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/fairQueue.test.ts @@ -134,7 +134,6 @@ class TestFairQueueHelper { return this.fairQueue.getTotalInflightCount(); } - registerTelemetryGauges(options?: { observedTenants?: string[] }) { return this.fairQueue.registerTelemetryGauges(options); } @@ -1371,5 +1370,4 @@ describe("FairQueue", () => { } ); }); - }); diff --git a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts index 4d85603b984..955cc494576 100644 --- a/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/raceConditions.test.ts @@ -11,7 +11,12 @@ import { WorkerQueueManager, FixedDelayRetry, } from "../index.js"; -import type { FairQueueKeyProducer, FairQueueOptions, QueueDescriptor, StoredMessage } from "../types.js"; +import type { + FairQueueKeyProducer, + FairQueueOptions, + QueueDescriptor, + StoredMessage, +} from "../types.js"; import { createRedisClient, type RedisOptions } from "@internal/redis"; const TestPayloadSchema = z.object({ id: z.number(), value: z.string() }); diff --git a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts index b26fb981193..695d5e462a5 100644 --- a/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/tenantDispatch.test.ts @@ -801,10 +801,7 @@ describe("Two-Level Tenant Dispatch", () => { })(); // Wait for t2's messages to be processed (they shouldn't be blocked by t1) - await waitFor( - () => processed.filter((p) => p.tenantId === "t2").length === 3, - 10000 - ); + await waitFor(() => processed.filter((p) => p.tenantId === "t2").length === 3, 10000); const t2ProcessedCount = processed.filter((p) => p.tenantId === "t2").length; expect(t2ProcessedCount).toBe(3); diff --git a/packages/redis-worker/src/fair-queue/tests/visibility.test.ts b/packages/redis-worker/src/fair-queue/tests/visibility.test.ts index 2ecdeb41e90..0a0f7c7f443 100644 --- a/packages/redis-worker/src/fair-queue/tests/visibility.test.ts +++ b/packages/redis-worker/src/fair-queue/tests/visibility.test.ts @@ -41,7 +41,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim the message (moves it to in-flight set) - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 5000); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 5000 + ); expect(claimResult.claimed).toBe(true); // Heartbeat should succeed since message is in-flight @@ -110,7 +116,13 @@ describe("VisibilityManager", () => { await redis.zadd(queueKey, storedMessage.timestamp, messageId); await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 5000); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 5000 + ); expect(claimResult.claimed).toBe(true); // Heartbeat should work before complete @@ -358,7 +370,13 @@ describe("VisibilityManager", () => { } // Request 10 messages but only 3 exist - const claimed = await manager.claimBatch(queueId, queueKey, queueItemsKey, "consumer-1", 10); + const claimed = await manager.claimBatch( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 10 + ); expect(claimed).toHaveLength(3); expect(claimed[0]!.messageId).toBe("msg-1"); @@ -546,7 +564,15 @@ describe("VisibilityManager", () => { const dispatchKey = keys.dispatchKey(0); // Should not throw when releasing empty array - await manager.releaseBatch([], queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, "t1"); + await manager.releaseBatch( + [], + queueId, + queueKey, + queueItemsKey, + tenantQueueIndexKey, + dispatchKey, + "t1" + ); await manager.close(); } @@ -591,7 +617,15 @@ describe("VisibilityManager", () => { const claimed = await manager.claimBatch(queueId, queueKey, queueItemsKey, "consumer-1", 3); // Release all messages back - await manager.releaseBatch(claimed, queueId, queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, "t1"); + await manager.releaseBatch( + claimed, + queueId, + queueKey, + queueItemsKey, + tenantQueueIndexKey, + dispatchKey, + "t1" + ); // Tenant queue index should have the queue with correct score const tenantQueueScore = await redis.zscore(tenantQueueIndexKey, queueId); @@ -644,7 +678,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim with very short timeout - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 100); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 100 + ); expect(claimResult.claimed).toBe(true); // Wait for timeout to expire @@ -836,7 +876,13 @@ describe("VisibilityManager", () => { await redis.hset(queueItemsKey, messageId, JSON.stringify(storedMessage)); // Claim the message - const claimResult = await manager.claim(queueId, queueKey, queueItemsKey, "consumer-1", 100); + const claimResult = await manager.claim( + queueId, + queueKey, + queueItemsKey, + "consumer-1", + 100 + ); expect(claimResult.claimed).toBe(true); // Corrupt the in-flight data by setting invalid JSON @@ -868,4 +914,3 @@ describe("VisibilityManager", () => { ); }); }); - diff --git a/packages/redis-worker/src/fair-queue/visibility.ts b/packages/redis-worker/src/fair-queue/visibility.ts index a182a4e790d..40c7ce59190 100644 --- a/packages/redis-worker/src/fair-queue/visibility.ts +++ b/packages/redis-worker/src/fair-queue/visibility.ts @@ -45,8 +45,8 @@ export class VisibilityManager { this.shardCount = options.shardCount; this.defaultTimeoutMs = options.defaultTimeoutMs; this.logger = options.logger ?? { - debug: () => { }, - error: () => { }, + debug: () => {}, + error: () => {}, }; this.#registerCommands(); diff --git a/packages/redis-worker/src/mollifier/buffer.test.ts b/packages/redis-worker/src/mollifier/buffer.test.ts index 3a775bbb8f6..00716e5d54d 100644 --- a/packages/redis-worker/src/mollifier/buffer.test.ts +++ b/packages/redis-worker/src/mollifier/buffer.test.ts @@ -42,9 +42,7 @@ describe("mollifierReconnectDelayMs", () => { }); it("decorrelates concurrent reconnects (distinct values across random draws)", () => { - const draws = [0.05, 0.3, 0.55, 0.8, 0.95].map((r) => - mollifierReconnectDelayMs(20, () => r), - ); + const draws = [0.05, 0.3, 0.55, 0.8, 0.95].map((r) => mollifierReconnectDelayMs(20, () => r)); // Lockstep would collapse to a single value; jitter spreads them. expect(new Set(draws).size).toBeGreaterThan(1); }); @@ -127,107 +125,123 @@ describe("MollifierBuffer construction", () => { }); describe("MollifierBuffer.accept", () => { - redisTest("accept writes entry, enqueues, and tracks env", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "accept writes entry, enqueues, and tracks env", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: serialiseSnapshot({ taskId: "t" }), - }); - - const entry = await buffer.getEntry("run_1"); - expect(entry).not.toBeNull(); - expect(entry!.runId).toBe("run_1"); - expect(entry!.envId).toBe("env_a"); - expect(entry!.orgId).toBe("org_1"); - expect(entry!.status).toBe("QUEUED"); - expect(entry!.attempts).toBe(0); - expect(entry!.createdAt).toBeInstanceOf(Date); - - const envs = await buffer.listEnvsForOrg("org_1"); - expect(envs).toContain("env_a"); - } finally { - await buffer.close(); + try { + await buffer.accept({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: serialiseSnapshot({ taskId: "t" }), + }); + + const entry = await buffer.getEntry("run_1"); + expect(entry).not.toBeNull(); + expect(entry!.runId).toBe("run_1"); + expect(entry!.envId).toBe("env_a"); + expect(entry!.orgId).toBe("org_1"); + expect(entry!.status).toBe("QUEUED"); + expect(entry!.attempts).toBe(0); + expect(entry!.createdAt).toBeInstanceOf(Date); + + const envs = await buffer.listEnvsForOrg("org_1"); + expect(envs).toContain("env_a"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.pop", () => { - redisTest("pop returns next QUEUED entry and transitions to DRAINING", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "pop returns next QUEUED entry and transitions to DRAINING", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "run_1", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.accept({ runId: "run_2", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_1", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ runId: "run_2", envId: "env_a", orgId: "org_1", payload: "{}" }); - const popped = await buffer.pop("env_a"); - expect(popped).not.toBeNull(); - expect(popped!.runId).toBe("run_1"); - expect(popped!.status).toBe("DRAINING"); + const popped = await buffer.pop("env_a"); + expect(popped).not.toBeNull(); + expect(popped!.runId).toBe("run_1"); + expect(popped!.status).toBe("DRAINING"); - const stored = await buffer.getEntry("run_1"); - expect(stored!.status).toBe("DRAINING"); - } finally { - await buffer.close(); + const stored = await buffer.getEntry("run_1"); + expect(stored!.status).toBe("DRAINING"); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("pop returns null when env queue is empty", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "pop returns null when env queue is empty", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const popped = await buffer.pop("env_nonexistent"); - expect(popped).toBeNull(); - } finally { - await buffer.close(); + try { + const popped = await buffer.pop("env_nonexistent"); + expect(popped).toBeNull(); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("atomic RPOP across two parallel pops on the same env", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "atomic RPOP across two parallel pops on the same env", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "only", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "only", envId: "env_a", orgId: "org_1", payload: "{}" }); - const [a, b] = await Promise.all([buffer.pop("env_a"), buffer.pop("env_a")]); - const winners = [a, b].filter((x) => x !== null); - expect(winners).toHaveLength(1); - expect(winners[0]!.runId).toBe("only"); - } finally { - await buffer.close(); + const [a, b] = await Promise.all([buffer.pop("env_a"), buffer.pop("env_a")]); + const winners = [a, b].filter((x) => x !== null); + expect(winners).toHaveLength(1); + expect(winners[0]!.runId).toBe("only"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.ack", () => { @@ -261,7 +275,7 @@ describe("MollifierBuffer.ack", () => { } finally { await buffer.close(); } - }, + } ); redisTest("ack on missing entry is a no-op", { timeout: 20_000 }, async ({ redisContainer }) => { @@ -318,7 +332,7 @@ describe("MollifierBuffer.pop orphan handling", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -364,36 +378,40 @@ describe("MollifierBuffer.pop orphan handling", () => { } finally { await buffer.close(); } - }, + } ); }); describe("MollifierBuffer.requeue", () => { - redisTest("requeue increments attempts, restores QUEUED, re-LPUSHes", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "requeue increments attempts, restores QUEUED, re-LPUSHes", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.pop("env_a"); - await buffer.requeue("run_r"); + try { + await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.pop("env_a"); + await buffer.requeue("run_r"); - const entry = await buffer.getEntry("run_r"); - expect(entry!.status).toBe("QUEUED"); - expect(entry!.attempts).toBe(1); + const entry = await buffer.getEntry("run_r"); + expect(entry!.status).toBe("QUEUED"); + expect(entry!.attempts).toBe(1); - const popped = await buffer.pop("env_a"); - expect(popped!.runId).toBe("run_r"); - } finally { - await buffer.close(); + const popped = await buffer.pop("env_a"); + expect(popped!.runId).toBe("run_r"); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierBuffer.fail", () => { @@ -432,7 +450,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -460,7 +478,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -516,7 +534,7 @@ describe("MollifierBuffer.fail", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -548,7 +566,7 @@ describe("MollifierBuffer TTL", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -567,7 +585,7 @@ describe("MollifierBuffer payload encoding", () => { }); const tricky = { - quotes: 'a"b\'c', + quotes: "a\"b'c", backslash: "x\\y\\z", newlines: "line1\nline2\r\nline3", tab: "col1\tcol2", @@ -589,7 +607,7 @@ describe("MollifierBuffer payload encoding", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -618,7 +636,7 @@ describe("MollifierBuffer.requeue on missing entry", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -663,7 +681,7 @@ describe("MollifierBuffer.requeue ordering", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -674,127 +692,147 @@ describe("MollifierBuffer.evaluateTrip", () => { holdMs: 100, }; - redisTest("under threshold: not tripped, count increments", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "under threshold: not tripped, count increments", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const r1 = await buffer.evaluateTrip("env_a", tripOptions); - expect(r1).toEqual({ tripped: false, count: 1 }); + try { + const r1 = await buffer.evaluateTrip("env_a", tripOptions); + expect(r1).toEqual({ tripped: false, count: 1 }); - const r2 = await buffer.evaluateTrip("env_a", tripOptions); - expect(r2).toEqual({ tripped: false, count: 2 }); - } finally { - await buffer.close(); + const r2 = await buffer.evaluateTrip("env_a", tripOptions); + expect(r2).toEqual({ tripped: false, count: 2 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("crossing threshold sets the tripped marker", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "crossing threshold sets the tripped marker", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - for (let i = 0; i < 5; i++) { - const r = await buffer.evaluateTrip("env_a", tripOptions); - expect(r.tripped).toBe(false); - } + try { + for (let i = 0; i < 5; i++) { + const r = await buffer.evaluateTrip("env_a", tripOptions); + expect(r.tripped).toBe(false); + } - const after = await buffer.evaluateTrip("env_a", tripOptions); - expect(after).toEqual({ tripped: true, count: 6 }); + const after = await buffer.evaluateTrip("env_a", tripOptions); + expect(after).toEqual({ tripped: true, count: 6 }); - const sticky = await buffer.evaluateTrip("env_a", tripOptions); - expect(sticky.tripped).toBe(true); - } finally { - await buffer.close(); + const sticky = await buffer.evaluateTrip("env_a", tripOptions); + expect(sticky.tripped).toBe(true); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("hold-down marker expires after holdMs and env resets", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "hold-down marker expires after holdMs and env resets", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const fastWindow = { windowMs: 100, threshold: 2, holdMs: 100 }; - await buffer.evaluateTrip("env_a", fastWindow); - await buffer.evaluateTrip("env_a", fastWindow); - const tripped = await buffer.evaluateTrip("env_a", fastWindow); - expect(tripped.tripped).toBe(true); + try { + const fastWindow = { windowMs: 100, threshold: 2, holdMs: 100 }; + await buffer.evaluateTrip("env_a", fastWindow); + await buffer.evaluateTrip("env_a", fastWindow); + const tripped = await buffer.evaluateTrip("env_a", fastWindow); + expect(tripped.tripped).toBe(true); - // Wait past windowMs AND holdMs so both rate counter and tripped marker expire - await new Promise((r) => setTimeout(r, 220)); + // Wait past windowMs AND holdMs so both rate counter and tripped marker expire + await new Promise((r) => setTimeout(r, 220)); - const recovered = await buffer.evaluateTrip("env_a", fastWindow); - expect(recovered).toEqual({ tripped: false, count: 1 }); - } finally { - await buffer.close(); + const recovered = await buffer.evaluateTrip("env_a", fastWindow); + expect(recovered).toEqual({ tripped: false, count: 1 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("env isolation: tripping env_a does not affect env_b", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "env isolation: tripping env_a does not affect env_b", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - for (let i = 0; i < 6; i++) { - await buffer.evaluateTrip("env_a", tripOptions); - } - const aTripped = await buffer.evaluateTrip("env_a", tripOptions); - expect(aTripped.tripped).toBe(true); + try { + for (let i = 0; i < 6; i++) { + await buffer.evaluateTrip("env_a", tripOptions); + } + const aTripped = await buffer.evaluateTrip("env_a", tripOptions); + expect(aTripped.tripped).toBe(true); - const b = await buffer.evaluateTrip("env_b", tripOptions); - expect(b).toEqual({ tripped: false, count: 1 }); - } finally { - await buffer.close(); + const b = await buffer.evaluateTrip("env_b", tripOptions); + expect(b).toEqual({ tripped: false, count: 1 }); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("window expires and counter resets when no traffic", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "window expires and counter resets when no traffic", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - const fastWindow = { windowMs: 100, threshold: 100, holdMs: 100 }; - await buffer.evaluateTrip("env_x", fastWindow); - await buffer.evaluateTrip("env_x", fastWindow); - // both incremented within a fresh window — count should be 2 - - await new Promise((r) => setTimeout(r, 150)); - const fresh = await buffer.evaluateTrip("env_x", fastWindow); - expect(fresh.count).toBe(1); - } finally { - await buffer.close(); + try { + const fastWindow = { windowMs: 100, threshold: 100, holdMs: 100 }; + await buffer.evaluateTrip("env_x", fastWindow); + await buffer.evaluateTrip("env_x", fastWindow); + // both incremented within a fresh window — count should be 2 + + await new Promise((r) => setTimeout(r, 150)); + const fresh = await buffer.evaluateTrip("env_x", fastWindow); + expect(fresh.count).toBe(1); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "tripped marker outlives the rate counter window", @@ -825,7 +863,7 @@ describe("MollifierBuffer.evaluateTrip", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -846,7 +884,7 @@ describe("MollifierBuffer.evaluateTrip", () => { // so trip semantics don't interfere with the count assertion. const opts = { windowMs: 5000, threshold: 1_000_000, holdMs: 100 }; const results = await Promise.all( - Array.from({ length: 100 }, () => buffer.evaluateTrip("env_atomic", opts)), + Array.from({ length: 100 }, () => buffer.evaluateTrip("env_atomic", opts)) ); // Every return value is unique (no two callers saw the same INCR result). @@ -858,7 +896,7 @@ describe("MollifierBuffer.evaluateTrip", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -891,7 +929,7 @@ describe("MollifierBuffer entry lifecycle invariants", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -944,7 +982,7 @@ describe("MollifierBuffer entry lifecycle invariants", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -994,7 +1032,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1016,7 +1054,12 @@ describe("MollifierBuffer.accept idempotency", () => { const stored = await buffer.getEntry("run_dr"); expect(stored!.status).toBe("DRAINING"); - const dup = await buffer.accept({ runId: "run_dr", envId: "env_a", orgId: "org_1", payload: "{}" }); + const dup = await buffer.accept({ + runId: "run_dr", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); expect(dup).toEqual({ kind: "duplicate_run_id" }); const afterDup = await buffer.getEntry("run_dr"); @@ -1024,7 +1067,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1067,7 +1110,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1114,7 +1157,7 @@ describe("MollifierBuffer.accept idempotency", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1141,7 +1184,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1170,7 +1213,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1198,7 +1241,7 @@ describe("MollifierBuffer envs set lifecycle", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1247,7 +1290,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1292,7 +1335,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1325,7 +1368,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1350,7 +1393,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1386,7 +1429,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1424,7 +1467,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1482,7 +1525,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1507,7 +1550,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1550,7 +1593,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1571,7 +1614,13 @@ describe("MollifierBuffer idempotency lookup", () => { }); const idem = { idempotencyKey: "kheal", taskIdentifier: "t" }; try { - await buffer.accept({ runId: "heal_old", envId: "env_h", orgId: "org_1", payload: "{}", ...idem }); + await buffer.accept({ + runId: "heal_old", + envId: "env_h", + orgId: "org_1", + payload: "{}", + ...idem, + }); // Simulate eviction of the entry hash while the lookup survives. await buffer["redis"].del("mollifier:entries:heal_old"); const lookupKey = idempotencyLookupKeyFor({ envId: "env_h", ...idem }); @@ -1591,7 +1640,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1610,7 +1659,13 @@ describe("MollifierBuffer idempotency lookup", () => { }); const idem = { idempotencyKey: "klive", taskIdentifier: "t" }; try { - await buffer.accept({ runId: "live_win", envId: "env_h", orgId: "org_1", payload: "{}", ...idem }); + await buffer.accept({ + runId: "live_win", + envId: "env_h", + orgId: "org_1", + payload: "{}", + ...idem, + }); const result = await buffer.accept({ runId: "live_lose", envId: "env_h", @@ -1622,7 +1677,7 @@ describe("MollifierBuffer idempotency lookup", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1661,7 +1716,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1701,7 +1756,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1742,7 +1797,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1780,7 +1835,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1832,7 +1887,7 @@ describe("MollifierBuffer.casSetMetadata", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1858,7 +1913,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1902,7 +1957,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1935,7 +1990,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1990,7 +2045,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2028,7 +2083,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2061,7 +2116,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2099,7 +2154,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2130,7 +2185,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2168,7 +2223,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2200,7 +2255,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2225,7 +2280,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { const tagsToAdd = Array.from({ length: 50 }, (_, i) => `t${i}`); await Promise.all( - tagsToAdd.map((t) => buffer.mutateSnapshot("rcc", { type: "append_tags", tags: [t] })), + tagsToAdd.map((t) => buffer.mutateSnapshot("rcc", { type: "append_tags", tags: [t] })) ); const entry = await buffer.getEntry("rcc"); @@ -2234,7 +2289,7 @@ describe("MollifierBuffer.mutateSnapshot", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2266,14 +2321,16 @@ describe("MollifierBuffer LIST storage", () => { // createdAtMicros lives on the entry hash (for dwell metrics) and // is plausibly recent (within the last minute, as microseconds). - const micros = Number(await buffer["redis"].hget("mollifier:entries:z1", "createdAtMicros")); + const micros = Number( + await buffer["redis"].hget("mollifier:entries:z1", "createdAtMicros") + ); const nowMicros = Date.now() * 1000; expect(micros).toBeGreaterThan(nowMicros - 60_000_000); expect(micros).toBeLessThanOrEqual(nowMicros + 1_000_000); } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2305,7 +2362,7 @@ describe("MollifierBuffer LIST storage", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2323,7 +2380,10 @@ describe("MollifierBuffer LIST storage", () => { try { await buffer.accept({ runId: "rq", envId: "env_rq", orgId: "org_1", payload: "{}" }); - const originalMicros = await buffer["redis"].hget("mollifier:entries:rq", "createdAtMicros"); + const originalMicros = await buffer["redis"].hget( + "mollifier:entries:rq", + "createdAtMicros" + ); await buffer.pop("env_rq"); // Queue is empty after the pop. @@ -2338,7 +2398,7 @@ describe("MollifierBuffer LIST storage", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2377,25 +2437,29 @@ describe("MollifierBuffer.listEntriesForEnv", () => { } finally { await buffer.close(); } - }, + } ); - redisTest("returns empty array when env queue is empty", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "returns empty array when env queue is empty", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - expect(await buffer.listEntriesForEnv("env_empty", 10)).toEqual([]); - } finally { - await buffer.close(); + try { + expect(await buffer.listEntriesForEnv("env_empty", 10)).toEqual([]); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "skips entries whose hash was torn down between LRANGE and HGETALL (concurrent drainer ack/fail race)", @@ -2429,26 +2493,30 @@ describe("MollifierBuffer.listEntriesForEnv", () => { } finally { await buffer.close(); } - }, + } ); - redisTest("maxCount <= 0 returns empty without hitting redis", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest( + "maxCount <= 0 returns empty without hitting redis", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - expect(await buffer.listEntriesForEnv("env_a", 0)).toEqual([]); - expect(await buffer.listEntriesForEnv("env_a", -5)).toEqual([]); - } finally { - await buffer.close(); + try { + expect(await buffer.listEntriesForEnv("env_a", 0)).toEqual([]); + expect(await buffer.listEntriesForEnv("env_a", -5)).toEqual([]); + } finally { + await buffer.close(); + } } - }); + ); }); // Composite-key safety. The Redis-key builders concatenate @@ -2510,7 +2578,7 @@ describe("MollifierBuffer composite-key encoding (collision resistance)", () => } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2533,7 +2601,7 @@ describe("MollifierBuffer composite-key encoding (collision resistance)", () => // base64url alphabet has no `:`, `+`, `/`, or `=`. const afterNamespace = key.slice("mollifier:idempotency:".length); expect(afterNamespace).toMatch(/^[A-Za-z0-9_\-]+:[A-Za-z0-9_\-]+:[A-Za-z0-9_\-]+$/); - }, + } ); }); @@ -2583,7 +2651,7 @@ describe("MollifierBuffer pre-gate claim — ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2614,7 +2682,7 @@ describe("MollifierBuffer pre-gate claim — ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2657,7 +2725,7 @@ describe("MollifierBuffer pre-gate claim — ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2722,7 +2790,7 @@ describe("MollifierBuffer pre-gate claim — ownership token safety", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -2767,35 +2835,31 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); - redisTest( - "ack ZREMs from the draining set", - { timeout: 20_000 }, - async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - logger: new Logger("test", "log"), - }); + redisTest("ack ZREMs from the draining set", { timeout: 20_000 }, async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + logger: new Logger("test", "log"), + }); - try { - await buffer.accept({ runId: "drn_ack", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.pop("env_a"); - expect(await buffer.getDrainingCount()).toBe(1); + try { + await buffer.accept({ runId: "drn_ack", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.pop("env_a"); + expect(await buffer.getDrainingCount()).toBe(1); - await buffer.ack("drn_ack"); - expect(await buffer.getDrainingCount()).toBe(0); - expect(await buffer["redis"].zscore(DRAINING_SET_KEY, "drn_ack")).toBeNull(); - } finally { - await buffer.close(); - } - }, - ); + await buffer.ack("drn_ack"); + expect(await buffer.getDrainingCount()).toBe(0); + expect(await buffer["redis"].zscore(DRAINING_SET_KEY, "drn_ack")).toBeNull(); + } finally { + await buffer.close(); + } + }); redisTest( "fail ZREMs from the draining set even though the entry hash is torn down", @@ -2820,7 +2884,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2850,7 +2914,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2888,7 +2952,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2928,7 +2992,7 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -2969,6 +3033,6 @@ describe("MollifierBuffer.draining tracker (observability)", () => { } finally { await buffer.close(); } - }, + } ); }); diff --git a/packages/redis-worker/src/mollifier/buffer.ts b/packages/redis-worker/src/mollifier/buffer.ts index 66afe186a8a..c61d681043c 100644 --- a/packages/redis-worker/src/mollifier/buffer.ts +++ b/packages/redis-worker/src/mollifier/buffer.ts @@ -55,7 +55,7 @@ export function mollifierReconnectDelayMs( times: number, random: () => number = Math.random, stepMs: number = DEFAULT_RECONNECT_STEP_MS, - maxMs: number = DEFAULT_RECONNECT_MAX_MS, + maxMs: number = DEFAULT_RECONNECT_MAX_MS ): number { const base = Math.min(times * stepMs, maxMs); const half = Math.floor(base / 2); @@ -73,11 +73,7 @@ export type SnapshotPatch = | { type: "set_delay"; delayUntil: string } | { type: "mark_cancelled"; cancelledAt: string; cancelReason?: string }; -export type MutateSnapshotResult = - | "applied_to_snapshot" - | "not_found" - | "busy" - | "limit_exceeded"; +export type MutateSnapshotResult = "applied_to_snapshot" | "not_found" | "busy" | "limit_exceeded"; export type CasSetMetadataResult = | { kind: "applied"; newVersion: number } @@ -160,7 +156,7 @@ export class MollifierBuffer { onError: (error) => { this.logger.error("MollifierBuffer redis client error:", { error }); }, - }, + } ); this.#registerCommands(); } @@ -216,7 +212,7 @@ export class MollifierBuffer { String(createdAtMicros), "mollifier:org-envs:", idempotencyLookupKey, - "mollifier:entries:", + "mollifier:entries:" ); // Lua returns 1 (accepted), 0 (duplicate runId), or a string runId // (duplicate idempotency — value is the existing winner's runId). @@ -237,7 +233,7 @@ export class MollifierBuffer { DRAINING_SET_KEY, entryPrefix, envId, - "mollifier:org-envs:", + "mollifier:org-envs:" )) as string | null; if (!encoded) return null; @@ -304,11 +300,7 @@ export class MollifierBuffer { // not an error. async listEntriesForEnv(envId: string, maxCount: number): Promise { if (maxCount <= 0) return []; - const runIds = await this.redis.lrange( - `mollifier:queue:${envId}`, - 0, - maxCount - 1, - ); + const runIds = await this.redis.lrange(`mollifier:queue:${envId}`, 0, maxCount - 1); if (runIds.length === 0) return []; const pipeline = this.redis.pipeline(); @@ -356,7 +348,7 @@ export class MollifierBuffer { async mutateSnapshot(runId: string, patch: SnapshotPatch): Promise { const result = (await this.redis.mutateMollifierSnapshot( `mollifier:entries:${runId}`, - JSON.stringify(patch), + JSON.stringify(patch) )) as string; if ( result === "applied_to_snapshot" || @@ -386,7 +378,7 @@ export class MollifierBuffer { entryKey, String(input.expectedVersion), input.newMetadata, - input.newMetadataType, + input.newMetadataType )) as string; if (raw === "not_found") return { kind: "not_found" }; if (raw === "busy") return { kind: "busy" }; @@ -417,14 +409,14 @@ export class MollifierBuffer { // - "resolved": the claim already holds a runId; the caller can // return that runId as a cached hit. async claimIdempotency( - input: IdempotencyLookupInput & { token: string; ttlSeconds: number }, + input: IdempotencyLookupInput & { token: string; ttlSeconds: number } ): Promise { const claimKey = makeIdempotencyClaimKey(input); const raw = (await this.redis.claimMollifierIdempotency( claimKey, `${PENDING_PREFIX}${input.token}`, PENDING_PREFIX, - String(input.ttlSeconds), + String(input.ttlSeconds) )) as string; if (raw === "claimed") return { kind: "claimed" }; if (raw === "pending") return { kind: "pending" }; @@ -445,14 +437,14 @@ export class MollifierBuffer { // Returns true if we published; false if the claim slot was no longer // ours. async publishClaim( - input: IdempotencyLookupInput & { token: string; runId: string; ttlSeconds: number }, + input: IdempotencyLookupInput & { token: string; runId: string; ttlSeconds: number } ): Promise { const claimKey = makeIdempotencyClaimKey(input); const result = (await this.redis.publishMollifierClaim( claimKey, `${PENDING_PREFIX}${input.token}`, input.runId, - String(input.ttlSeconds), + String(input.ttlSeconds) )) as number; return result === 1; } @@ -466,10 +458,7 @@ export class MollifierBuffer { // never wiped by a slow predecessor. async releaseClaim(input: IdempotencyLookupInput & { token: string }): Promise { const claimKey = makeIdempotencyClaimKey(input); - await this.redis.releaseMollifierClaim( - claimKey, - `${PENDING_PREFIX}${input.token}`, - ); + await this.redis.releaseMollifierClaim(claimKey, `${PENDING_PREFIX}${input.token}`); } // Read the current claim value, used by the wait/poll loop on losers @@ -511,7 +500,7 @@ export class MollifierBuffer { const clearedRunId = (await this.redis.resetMollifierIdempotency( lookupKey, "mollifier:entries:", - claimKey, + claimKey )) as string; return { clearedRunId: clearedRunId.length > 0 ? clearedRunId : null }; } @@ -526,7 +515,7 @@ export class MollifierBuffer { `mollifier:entries:${runId}`, DRAINING_SET_KEY, String(this.ackGraceTtlSeconds), - runId, + runId ); } @@ -537,7 +526,7 @@ export class MollifierBuffer { DRAINING_SET_KEY, "mollifier:queue:", runId, - "mollifier:org-envs:", + "mollifier:org-envs:" ); } @@ -552,7 +541,7 @@ export class MollifierBuffer { `mollifier:entries:${runId}`, DRAINING_SET_KEY, JSON.stringify(error), - runId, + runId ); return result === 1; } @@ -579,7 +568,7 @@ export class MollifierBuffer { String(maxScore), "LIMIT", 0, - Math.max(0, limit), + Math.max(0, limit) ); } @@ -592,10 +581,9 @@ export class MollifierBuffer { return this.redis.ttl(`mollifier:entries:${runId}`); } - async evaluateTrip( envId: string, - options: { windowMs: number; threshold: number; holdMs: number }, + options: { windowMs: number; threshold: number; holdMs: number } ): Promise<{ tripped: boolean; count: number }> { const rateKey = `mollifier:rate:${envId}`; const trippedKey = `mollifier:tripped:${envId}`; @@ -604,7 +592,7 @@ export class MollifierBuffer { trippedKey, String(options.windowMs), String(options.threshold), - String(options.holdMs), + String(options.holdMs) )) as [number, number]; return { count: result[0], tripped: result[1] === 1 }; @@ -1171,7 +1159,7 @@ declare module "@internal/redis" { orgEnvsPrefix: string, idempotencyLookupKey: string, entryPrefix: string, - callback?: Callback, + callback?: Callback ): Result; popAndMarkDraining( queueKey: string, @@ -1180,7 +1168,7 @@ declare module "@internal/redis" { entryPrefix: string, envId: string, orgEnvsPrefix: string, - callback?: Callback, + callback?: Callback ): Result; requeueMollifierEntry( entryKey: string, @@ -1189,63 +1177,63 @@ declare module "@internal/redis" { queuePrefix: string, runId: string, orgEnvsPrefix: string, - callback?: Callback, + callback?: Callback ): Result; mutateMollifierSnapshot( entryKey: string, patchJson: string, - callback?: Callback, + callback?: Callback ): Result; casSetMollifierMetadata( entryKey: string, expectedVersion: string, newMetadata: string, newMetadataType: string, - callback?: Callback, + callback?: Callback ): Result; resetMollifierIdempotency( lookupKey: string, entryPrefix: string, claimKey: string, - callback?: Callback, + callback?: Callback ): Result; claimMollifierIdempotency( claimKey: string, pendingMarker: string, pendingPrefix: string, ttlSeconds: string, - callback?: Callback, + callback?: Callback ): Result; publishMollifierClaim( claimKey: string, ownerMarker: string, runId: string, ttlSeconds: string, - callback?: Callback, + callback?: Callback ): Result; releaseMollifierClaim( claimKey: string, ownerMarker: string, - callback?: Callback, + callback?: Callback ): Result; ackMollifierEntry( entryKey: string, drainingSetKey: string, graceTtlSeconds: string, runId: string, - callback?: Callback, + callback?: Callback ): Result; failMollifierEntry( entryKey: string, drainingSetKey: string, errorPayload: string, runId: string, - callback?: Callback, + callback?: Callback ): Result; delMollifierKeyIfEquals( key: string, expected: string, - callback?: Callback, + callback?: Callback ): Result; mollifierEvaluateTrip( rateKey: string, @@ -1253,7 +1241,7 @@ declare module "@internal/redis" { windowMs: string, threshold: string, holdMs: string, - callback?: Callback<[number, number]>, + callback?: Callback<[number, number]> ): Result<[number, number], Context>; } } diff --git a/packages/redis-worker/src/mollifier/drainer.test.ts b/packages/redis-worker/src/mollifier/drainer.test.ts index d4250641ee5..6d42be29cb7 100644 --- a/packages/redis-worker/src/mollifier/drainer.test.ts +++ b/packages/redis-worker/src/mollifier/drainer.test.ts @@ -38,96 +38,104 @@ function eachEnvAsOwnOrg(envs: string[]): Partial { } describe("MollifierDrainer.runOnce", () => { - redisTest("drains one queued entry through the handler and acks", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); - - const handlerCalls: Array<{ runId: string; envId: string; orgId: string; payload: unknown }> = - []; - const handler = async (input: { - runId: string; - envId: string; - orgId: string; - payload: unknown; - }) => { - handlerCalls.push(input); - }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); - - try { - await buffer.accept({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: serialiseSnapshot({ foo: 1 }), + redisTest( + "drains one queued entry through the handler and acks", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, }); - const result = await drainer.runOnce(); - expect(result.drained).toBe(1); - expect(result.failed).toBe(0); - expect(handlerCalls).toHaveLength(1); - expect(handlerCalls[0]).toMatchObject({ - runId: "run_1", - envId: "env_a", - orgId: "org_1", - payload: { foo: 1 }, + const handlerCalls: Array<{ runId: string; envId: string; orgId: string; payload: unknown }> = + []; + const handler = async (input: { + runId: string; + envId: string; + orgId: string; + payload: unknown; + }) => { + handlerCalls.push(input); + }; + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), }); - // After ack the entry persists as a read-fallback safety net with - // materialised=true and a fresh grace TTL. - const entry = await buffer.getEntry("run_1"); - expect(entry).not.toBeNull(); - expect(entry!.materialised).toBe(true); - } finally { - await buffer.close(); + try { + await buffer.accept({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: serialiseSnapshot({ foo: 1 }), + }); + + const result = await drainer.runOnce(); + expect(result.drained).toBe(1); + expect(result.failed).toBe(0); + expect(handlerCalls).toHaveLength(1); + expect(handlerCalls[0]).toMatchObject({ + runId: "run_1", + envId: "env_a", + orgId: "org_1", + payload: { foo: 1 }, + }); + + // After ack the entry persists as a read-fallback safety net with + // materialised=true and a fresh grace TTL. + const entry = await buffer.getEntry("run_1"); + expect(entry).not.toBeNull(); + expect(entry!.materialised).toBe(true); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("runOnce with no entries does nothing", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "runOnce with no entries does nothing", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let handlerCalls = 0; - const handler = async () => { - handlerCalls++; - }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); + let handlerCalls = 0; + const handler = async () => { + handlerCalls++; + }; + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), + }); - try { - const result = await drainer.runOnce(); - expect(result.drained).toBe(0); - expect(result.failed).toBe(0); - expect(handlerCalls).toBe(0); - } finally { - await buffer.close(); + try { + const result = await drainer.runOnce(); + expect(result.drained).toBe(0); + expect(result.failed).toBe(0); + expect(handlerCalls).toBe(0); + } finally { + await buffer.close(); + } } - }); + ); }); describe("MollifierDrainer.drainBatchSize", () => { @@ -314,7 +322,7 @@ describe("MollifierDrainer.drainBatchSize", () => { for (let i = 0; i < envCount; i++) { queues.set( `env_${i}`, - Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`), + Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`) ); } const handled: string[] = []; @@ -379,7 +387,7 @@ describe("MollifierDrainer.drainBatchSize", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set( @@ -387,7 +395,7 @@ describe("MollifierDrainer.drainBatchSize", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${orgBEnv}_run_${i}`, orgId: "org_B", - })), + })) ); const drainedByOrg: Record = { org_A: 0, org_B: 0 }; @@ -508,7 +516,7 @@ describe("MollifierDrainer.drainBatchSize", () => { for (let i = 0; i < envCount; i++) { queues.set( `env_${i}`, - Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`), + Array.from({ length: perEnv }, (_, j) => `env_${i}_run_${j}`) ); } let inflightPoppedNotAcked = 0; @@ -608,95 +616,103 @@ describe("MollifierDrainer.drainBatchSize", () => { }); describe("MollifierDrainer error handling", () => { - redisTest("retryable error requeues and increments attempts", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "retryable error requeues and increments attempts", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let calls = 0; - const handler = async () => { - calls++; - throw new Error("transient"); - }; + let calls = 0; + const handler = async () => { + calls++; + throw new Error("transient"); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => true, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => true, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_r", envId: "env_a", orgId: "org_1", payload: "{}" }); - await drainer.runOnce(); - const after1 = await buffer.getEntry("run_r"); - expect(after1!.status).toBe("QUEUED"); - expect(after1!.attempts).toBe(1); + await drainer.runOnce(); + const after1 = await buffer.getEntry("run_r"); + expect(after1!.status).toBe("QUEUED"); + expect(after1!.attempts).toBe(1); - await drainer.runOnce(); - const after2 = await buffer.getEntry("run_r"); - expect(after2!.status).toBe("QUEUED"); - expect(after2!.attempts).toBe(2); - - const result3 = await drainer.runOnce(); - // On attempt 3 the drainer hits maxAttempts and calls fail(), - // which deletes the entry — once the drainer-handler has written - // the SYSTEM_FAILURE PG row the buffer entry is no longer - // load-bearing. The runOnce result is the surviving signal. - const after3 = await buffer.getEntry("run_r"); - expect(after3).toBeNull(); - expect(result3.failed).toBe(1); - expect(calls).toBe(3); - } finally { - await buffer.close(); + await drainer.runOnce(); + const after2 = await buffer.getEntry("run_r"); + expect(after2!.status).toBe("QUEUED"); + expect(after2!.attempts).toBe(2); + + const result3 = await drainer.runOnce(); + // On attempt 3 the drainer hits maxAttempts and calls fail(), + // which deletes the entry — once the drainer-handler has written + // the SYSTEM_FAILURE PG row the buffer entry is no longer + // load-bearing. The runOnce result is the surviving signal. + const after3 = await buffer.getEntry("run_r"); + expect(after3).toBeNull(); + expect(result3.failed).toBe(1); + expect(calls).toBe(3); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("non-retryable error transitions directly to FAILED", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "non-retryable error transitions directly to FAILED", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - const handler = async () => { - throw new Error("validation failure"); - }; + const handler = async () => { + throw new Error("validation failure"); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => false, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => false, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "run_nr", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "run_nr", envId: "env_a", orgId: "org_1", payload: "{}" }); - const result = await drainer.runOnce(); + const result = await drainer.runOnce(); - // fail() deletes the entry once the drainer-handler has written - // the canonical SYSTEM_FAILURE PG row. - const entry = await buffer.getEntry("run_nr"); - expect(entry).toBeNull(); - expect(result.failed).toBe(1); - } finally { - await buffer.close(); + // fail() deletes the entry once the drainer-handler has written + // the canonical SYSTEM_FAILURE PG row. + const entry = await buffer.getEntry("run_nr"); + expect(entry).toBeNull(); + expect(result.failed).toBe(1); + } finally { + await buffer.close(); + } } - }); + ); redisTest( "multi-org round-robin: drains one item per org per runOnce", @@ -752,7 +768,7 @@ describe("MollifierDrainer error handling", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -809,7 +825,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_exhaust", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_exhaust", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); // Attempt 1: retryable error → requeue, no terminal callback fires. const r1 = await drainer.runOnce(); @@ -836,7 +857,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -882,7 +903,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -929,7 +950,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_cb_retry", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_cb_retry", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); // Tick 1: handler throws → attempts=1 < maxAttempts=3 → requeue // (no callback invocation, retryable path). @@ -957,7 +983,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -992,7 +1018,12 @@ describe("MollifierDrainer.onTerminalFailure", () => { }); try { - await buffer.accept({ runId: "run_cb_dead", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ + runId: "run_cb_dead", + envId: "env_a", + orgId: "org_1", + payload: "{}", + }); const r = await drainer.runOnce(); expect(r.failed).toBe(1); @@ -1003,7 +1034,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); redisTest( @@ -1042,7 +1073,7 @@ describe("MollifierDrainer.onTerminalFailure", () => { } finally { await buffer.close(); } - }, + } ); }); @@ -1398,17 +1429,15 @@ describe("MollifierDrainer per-tick org cap", () => { for (const h of heavy) { queues.set( h, - Array.from({ length: 100 }, (_, i) => `${h}_run_${i}`), + Array.from({ length: 100 }, (_, i) => `${h}_run_${i}`) ); } queues.set(light, [`${light}_run_0`]); - const activeEnvs = () => - [...queues.keys()].filter((k) => (queues.get(k)?.length ?? 0) > 0); + const activeEnvs = () => [...queues.keys()].filter((k) => (queues.get(k)?.length ?? 0) > 0); const buffer = makeStubBuffer({ listOrgs: async () => activeEnvs(), - listEnvsForOrg: async (orgId: string) => - activeEnvs().includes(orgId) ? [orgId] : [], + listEnvsForOrg: async (orgId: string) => (activeEnvs().includes(orgId) ? [orgId] : []), pop: async (envId: string) => { const q = queues.get(envId); if (!q || q.length === 0) return null; @@ -1472,7 +1501,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set(orgBEnv, [{ runId: `${orgBEnv}_run_0`, orgId: "org_B" }]); @@ -1563,7 +1592,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${e}_run_${i}`, orgId: "org_A", - })), + })) ); } queues.set( @@ -1571,7 +1600,7 @@ describe("MollifierDrainer per-tick org cap", () => { Array.from({ length: 100 }, (_, i) => ({ runId: `${orgBEnv}_run_${i}`, orgId: "org_B", - })), + })) ); const drainedByOrg: Record = { org_A: 0, org_B: 0 }; @@ -1648,9 +1677,7 @@ describe("MollifierDrainer per-tick org cap", () => { return anyEnvActive ? [orgId] : []; }, listEnvsForOrg: async (org: string) => - org === orgId - ? [...queues.keys()].filter((k) => (queues.get(k) ?? 0) > 0) - : [], + org === orgId ? [...queues.keys()].filter((k) => (queues.get(k) ?? 0) > 0) : [], pop: async (envId: string) => { const remaining = queues.get(envId) ?? 0; if (remaining === 0) return null; @@ -1696,7 +1723,6 @@ describe("MollifierDrainer per-tick org cap", () => { }); describe("MollifierDrainer additional coverage", () => { - it("a malformed payload is treated as a non-retryable handler error and goes terminal", async () => { // The deserialise call lives inside processEntry's try, so a JSON parse // failure is caught by the same handler-error branch. With @@ -1934,101 +1960,109 @@ describe("MollifierDrainer additional coverage", () => { }); describe("MollifierDrainer.start/stop", () => { - redisTest("start polls and processes, stop halts the loop", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "start polls and processes, stop halts the loop", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - const handled: string[] = []; - const handler = async (input: { runId: string }) => { - handled.push(input.runId); - }; + const handled: string[] = []; + const handler = async (input: { runId: string }) => { + handled.push(input.runId); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 5, - maxAttempts: 3, - isRetryable: () => false, - pollIntervalMs: 20, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 5, + maxAttempts: 3, + isRetryable: () => false, + pollIntervalMs: 20, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "live_1", envId: "env_a", orgId: "org_1", payload: "{}" }); - await buffer.accept({ runId: "live_2", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "live_1", envId: "env_a", orgId: "org_1", payload: "{}" }); + await buffer.accept({ runId: "live_2", envId: "env_a", orgId: "org_1", payload: "{}" }); - drainer.start(); + drainer.start(); - const deadline = Date.now() + 5_000; - while (handled.length < 2 && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 50)); - } + const deadline = Date.now() + 5_000; + while (handled.length < 2 && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 50)); + } - await drainer.stop(); + await drainer.stop(); - expect(new Set(handled)).toEqual(new Set(["live_1", "live_2"])); - } finally { - await buffer.close(); + expect(new Set(handled)).toEqual(new Set(["live_1", "live_2"])); + } finally { + await buffer.close(); + } } - }); + ); - redisTest("stop returns after timeoutMs even if a handler is hung", { timeout: 20_000 }, async ({ redisContainer }) => { - const buffer = new MollifierBuffer({ - redisOptions: { - host: redisContainer.getHost(), - port: redisContainer.getPort(), - password: redisContainer.getPassword(), - }, - ...noopOptions, - }); + redisTest( + "stop returns after timeoutMs even if a handler is hung", + { timeout: 20_000 }, + async ({ redisContainer }) => { + const buffer = new MollifierBuffer({ + redisOptions: { + host: redisContainer.getHost(), + port: redisContainer.getPort(), + password: redisContainer.getPassword(), + }, + ...noopOptions, + }); - let handlerStarted = false; - const handler = async () => { - handlerStarted = true; - await new Promise(() => {}); - }; + let handlerStarted = false; + const handler = async () => { + handlerStarted = true; + await new Promise(() => {}); + }; - const drainer = new MollifierDrainer({ - buffer, - handler, - concurrency: 1, - maxAttempts: 3, - isRetryable: () => false, - pollIntervalMs: 20, - logger: new Logger("test-drainer", "log"), - }); + const drainer = new MollifierDrainer({ + buffer, + handler, + concurrency: 1, + maxAttempts: 3, + isRetryable: () => false, + pollIntervalMs: 20, + logger: new Logger("test-drainer", "log"), + }); - try { - await buffer.accept({ runId: "hung", envId: "env_a", orgId: "org_1", payload: "{}" }); + try { + await buffer.accept({ runId: "hung", envId: "env_a", orgId: "org_1", payload: "{}" }); - drainer.start(); + drainer.start(); - const deadline = Date.now() + 2_000; - while (!handlerStarted && Date.now() < deadline) { - await new Promise((r) => setTimeout(r, 25)); + const deadline = Date.now() + 2_000; + while (!handlerStarted && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 25)); + } + expect(handlerStarted).toBe(true); + + const stopStart = Date.now(); + await drainer.stop({ timeoutMs: 500 }); + const stopElapsed = Date.now() - stopStart; + + // Allow a small jitter window below `timeoutMs` — Node's setTimeout can + // fire a millisecond or two early under CI load. The behaviour we're + // pinning is "stop honors the deadline instead of waiting for the hung + // handler indefinitely", not millisecond-precise timing. + expect(stopElapsed).toBeGreaterThanOrEqual(450); + expect(stopElapsed).toBeLessThan(2_000); + } finally { + await buffer.close(); } - expect(handlerStarted).toBe(true); - - const stopStart = Date.now(); - await drainer.stop({ timeoutMs: 500 }); - const stopElapsed = Date.now() - stopStart; - - // Allow a small jitter window below `timeoutMs` — Node's setTimeout can - // fire a millisecond or two early under CI load. The behaviour we're - // pinning is "stop honors the deadline instead of waiting for the hung - // handler indefinitely", not millisecond-precise timing. - expect(stopElapsed).toBeGreaterThanOrEqual(450); - expect(stopElapsed).toBeLessThan(2_000); - } finally { - await buffer.close(); } - }); + ); }); describe("MollifierDrainer concurrency cap", () => { @@ -2093,6 +2127,6 @@ describe("MollifierDrainer concurrency cap", () => { } finally { await buffer.close(); } - }, + } ); }); diff --git a/packages/redis-worker/src/mollifier/drainer.ts b/packages/redis-worker/src/mollifier/drainer.ts index c831d0b56a0..fc264d2421d 100644 --- a/packages/redis-worker/src/mollifier/drainer.ts +++ b/packages/redis-worker/src/mollifier/drainer.ts @@ -132,9 +132,7 @@ export class MollifierDrainer { // commands into one batch, so the burst is cheap; SMEMBERS on a small set // is O(N) per org and trivial at this scale. `Promise.all` preserves // order, so the org→envs pairing below stays deterministic. - const envsByOrg = await Promise.all( - orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId)), - ); + const envsByOrg = await Promise.all(orgSlice.map((orgId) => this.buffer.listEnvsForOrg(orgId))); const targets: string[] = []; for (let i = 0; i < orgSlice.length; i++) { const orgId = orgSlice[i]!; @@ -291,7 +289,7 @@ export class MollifierDrainer { if (winner === timeoutSentinel) { this.logger.warn( "MollifierDrainer.stop: deadline exceeded; returning while loop iteration is in flight", - { timeoutMs: options.timeoutMs }, + { timeoutMs: options.timeoutMs } ); } } finally { @@ -419,14 +417,11 @@ export class MollifierDrainer { } catch (writeErr) { if (this.isRetryable(writeErr)) { await this.buffer.requeue(entry.runId); - this.logger.warn( - "MollifierDrainer: terminal-failure callback retryable; requeued", - { - runId: entry.runId, - attempts: nextAttempts, - writeErr, - }, - ); + this.logger.warn("MollifierDrainer: terminal-failure callback retryable; requeued", { + runId: entry.runId, + attempts: nextAttempts, + writeErr, + }); return "failed"; } this.logger.error("MollifierDrainer: terminal-failure callback failed", { diff --git a/packages/redis-worker/src/mollifier/schemas.ts b/packages/redis-worker/src/mollifier/schemas.ts index 5acd0c7c15d..db75ed6e452 100644 --- a/packages/redis-worker/src/mollifier/schemas.ts +++ b/packages/redis-worker/src/mollifier/schemas.ts @@ -35,7 +35,10 @@ const stringToError = z.string().transform((v, ctx) => { try { return BufferEntryError.parse(JSON.parse(v)); } catch { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: "expected JSON-encoded BufferEntryError" }); + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "expected JSON-encoded BufferEntryError", + }); return z.NEVER; } }); diff --git a/packages/redis-worker/src/utils.ts b/packages/redis-worker/src/utils.ts index a577de3b0ed..7e4a4cb57cc 100644 --- a/packages/redis-worker/src/utils.ts +++ b/packages/redis-worker/src/utils.ts @@ -6,7 +6,5 @@ * - Native Node.js AbortError from timers/promises (sets .name) */ export function isAbortError(error: unknown): boolean { - return ( - error instanceof Error && (error.name === "AbortError" || error.message === "AbortError") - ); + return error instanceof Error && (error.name === "AbortError" || error.message === "AbortError"); } diff --git a/packages/redis-worker/src/worker.ts b/packages/redis-worker/src/worker.ts index 51812e575ea..92f1451033e 100644 --- a/packages/redis-worker/src/worker.ts +++ b/packages/redis-worker/src/worker.ts @@ -66,10 +66,11 @@ export type JobHandler = params: JobHandlerParams ) => Promise; -type JobHandlerFor = - Catalog[K] extends { batch: BatchConfig } - ? (items: Array>) => Promise - : (params: JobHandlerParams) => Promise; +type JobHandlerFor = Catalog[K] extends { + batch: BatchConfig; +} + ? (items: Array>) => Promise + : (params: JobHandlerParams) => Promise; export type WorkerConcurrencyOptions = { workers?: number; @@ -583,15 +584,15 @@ class Worker { await this.flushBatch(queueItem.job, workerId, limiter); } } else { - limiter(() => - this.processItem(queueItem, items.length, workerId, limiter) - ).catch((err) => { - this.logger.error("Unhandled error in processItem:", { - error: err, - workerId, - item, - }); - }); + limiter(() => this.processItem(queueItem, items.length, workerId, limiter)).catch( + (err) => { + this.logger.error("Unhandled error in processItem:", { + error: err, + workerId, + item, + }); + } + ); } } } catch (error) { @@ -785,13 +786,25 @@ class Worker { }; if (!shouldLogError) { - this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.info( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else if (errorLogLevel === "warn") { - this.logger.warn(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.warn( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else if (errorLogLevel === "info") { - this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.info( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } else { - this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes); + this.logger.error( + `Worker batch item reached max attempts. Moving to DLQ.`, + dlqLogAttributes + ); } await this.queue.moveToDeadLetterQueue(item.id, errorMessage); diff --git a/packages/rsc/src/build.ts b/packages/rsc/src/build.ts index 5436bf6fb09..2b87216042f 100644 --- a/packages/rsc/src/build.ts +++ b/packages/rsc/src/build.ts @@ -90,7 +90,7 @@ export function rscExtension(options?: RSCExtensionOptions): BuildExtension { build.onResolve({ filter: /^react-dom\/server$/ }, (args) => { const condition = - context.config.runtime === "bun" ? "bun" : options?.reactDomEnvironment ?? "node"; + context.config.runtime === "bun" ? "bun" : (options?.reactDomEnvironment ?? "node"); context.logger.debug("Resolving react-dom/server", { args, condition }); diff --git a/packages/schema-to-json/tsconfig.json b/packages/schema-to-json/tsconfig.json index 5bf5eba8d50..cc79a638942 100644 --- a/packages/schema-to-json/tsconfig.json +++ b/packages/schema-to-json/tsconfig.json @@ -8,4 +8,4 @@ "path": "./tsconfig.test.json" } ] -} \ No newline at end of file +} diff --git a/packages/schema-to-json/vitest.config.ts b/packages/schema-to-json/vitest.config.ts index c7da6b38e14..3f824fb954c 100644 --- a/packages/schema-to-json/vitest.config.ts +++ b/packages/schema-to-json/vitest.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, - environment: 'node', + environment: "node", }, -}); \ No newline at end of file +}); diff --git a/packages/trigger-sdk/src/v3/ai-shared.ts b/packages/trigger-sdk/src/v3/ai-shared.ts index a0ea3036cff..a77ffc21056 100644 --- a/packages/trigger-sdk/src/v3/ai-shared.ts +++ b/packages/trigger-sdk/src/v3/ai-shared.ts @@ -172,13 +172,8 @@ export type ChatInputChunk; * ``` */ -export type InferChatClientData = TTask extends Task< - string, - ChatTaskWirePayload, - any -> - ? TMetadata - : unknown; +export type InferChatClientData = + TTask extends Task, any> ? TMetadata : unknown; /** * Extracts the UI message type from a chat task (wire payload `message` items). @@ -191,13 +186,10 @@ export type InferChatClientData = TTask extends Task< * type Msg = InferChatUIMessage; * ``` */ -export type InferChatUIMessage = TTask extends Task< - string, - ChatTaskWirePayload, - any -> - ? TUIM - : UIMessage; +export type InferChatUIMessage = + TTask extends Task, any> + ? TUIM + : UIMessage; /** * Derive the chat `UIMessage` type for a given tool set. The tool-part types @@ -326,18 +318,13 @@ function isToolPartType(type: unknown): boolean { * Pairs with the per-turn merge on the agent side * (`mergeIncomingIntoHydrated` in `ai.ts`). */ -export function slimSubmitMessageForWire( - message: TMsg -): TMsg { +export function slimSubmitMessageForWire(message: TMsg): TMsg { if (!message) return message; if (message.role !== "assistant") return message; const parts = (message.parts ?? []) as any[]; const advancedToolParts = parts.filter( (p) => - p && - typeof p === "object" && - isToolPartType(p.type) && - isWireAdvanceableToolState(p.state) + p && typeof p === "object" && isToolPartType(p.type) && isWireAdvanceableToolState(p.state) ); if (advancedToolParts.length === 0) return message; const slimParts = advancedToolParts.map((p: any) => { diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index e523c9c9a86..d1286315b1d 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -204,9 +204,7 @@ const lastTurnCompleteSeqNumKey = locals.create<{ value: number | undefined }>( * merge handles any dedup against snapshot-restored messages. * @internal */ -async function findLatestSessionInCursor( - chatId: string -): Promise { +async function findLatestSessionInCursor(chatId: string): Promise { const apiClient = apiClientManager.clientOrThrow(); const response = await apiClient.readSessionStreamRecords(chatId, "out"); let latestCursor: number | undefined; @@ -261,10 +259,9 @@ async function seedSessionInResumeCursorForCustomLoop( sessionStreams.setLastDispatchedSeqNum(payload.chatId, "in", cursor); } } catch (error) { - logger.warn( - "chat session: session.in resume cursor lookup failed; old messages may replay", - { error: error instanceof Error ? error.message : String(error) } - ); + logger.warn("chat session: session.in resume cursor lookup failed; old messages may replay", { + error: error instanceof Error ? error.message : String(error), + }); } } @@ -727,9 +724,7 @@ async function replaySessionOutTail( * Not part of the public API. * @internal */ -export async function __replaySessionOutTailProductionPathForTests< - TUIMessage extends UIMessage, ->( +export async function __replaySessionOutTailProductionPathForTests( sessionId: string, options?: { lastEventId?: string } ): Promise { @@ -840,9 +835,7 @@ async function replaySessionInTail( * `__replaySessionOutTailProductionPathForTests`. * @internal */ -export async function __replaySessionInTailProductionPathForTests< - TUIMessage extends UIMessage, ->( +export async function __replaySessionInTailProductionPathForTests( sessionId: string, options?: { lastEventId?: string } ): Promise<{ message: TUIMessage; metadata: unknown; seqNum: number }[]> { @@ -903,14 +896,14 @@ function stampConversationIdOnActiveSpan( type ToolResultContent = Array< | { - type: "text"; - text: string; - } + type: "text"; + text: string; + } | { - type: "image"; - data: string; - mimeType?: string; - } + type: "image"; + data: string; + mimeType?: string; + } >; export type ToolOptions = { @@ -1094,7 +1087,9 @@ function toolFromTask< : {}), } as any); return staticTool as unknown as ToolSetCompatible< - TTaskSchema extends TaskSchema ? Tool, TOutput> : Tool + TTaskSchema extends TaskSchema + ? Tool, TOutput> + : Tool >; } @@ -1109,7 +1104,9 @@ function toolFromTask< }); return toolDefinition as unknown as ToolSetCompatible< - TTaskSchema extends TaskSchema ? Tool, TOutput> : Tool + TTaskSchema extends TaskSchema + ? Tool, TOutput> + : Tool >; } @@ -1170,7 +1167,7 @@ function getToolChatContextOrThrow(): ChatT if (!ctx) { throw new Error( "ai.chatContextOrThrow() called outside of a chat.agent context. " + - "This helper can only be used inside a subtask invoked via ai.toolExecute() (or legacy ai.tool()) from a chat.agent." + "This helper can only be used inside a subtask invoked via ai.toolExecute() (or legacy ai.tool()) from a chat.agent." ); } return ctx; @@ -1797,10 +1794,7 @@ const handoverInput = { while (true) { const result = await getChatSession().in.waitWithIdleTimeout(options); if (!result.ok) return result; - if ( - result.output.kind === "handover" || - result.output.kind === "handover-skip" - ) { + if (result.output.kind === "handover" || result.output.kind === "handover-skip") { return { ok: true as const, output: result.output as HandoverSignal }; } // Other kinds (message, stop) are not expected during handover-prepare. @@ -2017,11 +2011,11 @@ const chatUIStreamPerTurnKey = locals.create>( - "chat.toolCallToMessageId" -); +const chatToolCallToMessageIdKey = locals.create>("chat.toolCallToMessageId"); -function recordToolCallIdsFromMessage(message: { id?: string; role?: string; parts?: unknown[] } | undefined) { +function recordToolCallIdsFromMessage( + message: { id?: string; role?: string; parts?: unknown[] } | undefined +) { if (!message || message.role !== "assistant" || !message.id) return; let map = locals.get(chatToolCallToMessageIdKey); if (!map) { @@ -2291,9 +2285,7 @@ function extractNewToolResultsFromHistory( message: UIMessage, messages: UIMessage[] ): ChatNewToolResult[] { - const resolved = new Set( - getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId) - ); + const resolved = new Set(getResolvedToolCallsFromHistory(messages).map((r) => r.toolCallId)); const seen = new Set(); const out: ChatNewToolResult[] = []; for (const { part, toolCallId, toolName, state } of iterateToolParts(message)) { @@ -2811,9 +2803,9 @@ export type PrepareMessagesEvent = { messages: ModelMessage[]; /** Why messages are being prepared. */ reason: - | "run" // Messages being passed to run() for streamText - | "compaction-rebuild" // Rebuilding from a previous compaction summary - | "compaction-result"; // Fresh compaction just produced these messages + | "run" // Messages being passed to run() for streamText + | "compaction-rebuild" // Rebuilding from a previous compaction summary + | "compaction-result"; // Fresh compaction just produced these messages /** The chat session ID. */ chatId: string; /** The current turn number (0-indexed). */ @@ -3090,9 +3082,12 @@ async function applyPrepareMessages( * declared. * @internal */ -async function resolveTurnTools( - override?: { chatId: string; turn: number; continuation: boolean; clientData: unknown } -): Promise { +async function resolveTurnTools(override?: { + chatId: string; + turn: number; + continuation: boolean; + clientData: unknown; +}): Promise { const option = locals.get(chatToolsOptionKey); if (!option) return; @@ -3193,18 +3188,18 @@ async function chatCompact( const shouldTrigger = options.shouldCompact ? await options.shouldCompact({ - messages, - totalTokens, - inputTokens, - outputTokens, - usage: currentStep.usage, - source: "inner", - stepNumber, - steps, - chatId: turnCtx?.chatId, - turn: turnCtx?.turn, - clientData: turnCtx?.clientData, - }) + messages, + totalTokens, + inputTokens, + outputTokens, + usage: currentStep.usage, + source: "inner", + stepNumber, + steps, + chatId: turnCtx?.chatId, + turn: turnCtx?.turn, + clientData: turnCtx?.clientData, + }) : totalTokens != null && options.threshold != null && totalTokens > options.threshold; if (!shouldTrigger) { @@ -3520,16 +3515,16 @@ function isCompactionSafe(messages: UIMessage[]): boolean { export type ChatPromptValue = | ResolvedPrompt | { - text: string; - model: undefined; - config: undefined; - promptId: string; - version: number; - labels: string[]; - toAISDKTelemetry: (additionalMetadata?: Record) => { - experimental_telemetry: { isEnabled: true; metadata: Record }; + text: string; + model: undefined; + config: undefined; + promptId: string; + version: number; + labels: string[]; + toAISDKTelemetry: (additionalMetadata?: Record) => { + experimental_telemetry: { isEnabled: true; metadata: Record }; + }; }; - }; /** @internal */ const chatPromptKey = locals.create("chat.prompt"); @@ -3629,9 +3624,7 @@ function getChatSkills(): ResolvedSkill[] | undefined { */ function buildSkillsSystemPrompt(skills: ResolvedSkill[]): string { if (skills.length === 0) return ""; - const lines = skills.map( - (s) => `- ${s.frontmatter.name}: ${s.frontmatter.description}` - ); + const lines = skills.map((s) => `- ${s.frontmatter.name}: ${s.frontmatter.description}`); return [ "Available skills (call `loadSkill` to read the full instructions before using one):", ...lines, @@ -3692,7 +3685,8 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record { skill: { type: "string", description: "The skill's name (from frontmatter)." }, path: { type: "string", - description: "Relative path inside the skill folder (e.g. `references/citation-style.md`).", + description: + "Relative path inside the skill folder (e.g. `references/citation-style.md`).", }, }, required: ["skill", "path"], @@ -3723,7 +3717,8 @@ export function buildSkillTools(skills: ResolvedSkill[]): Record { skill: { type: "string", description: "The skill's name (from frontmatter)." }, command: { type: "string", - description: "Bash command to run. Relative script paths resolve against the skill's root.", + description: + "Bash command to run. Relative script paths resolve against the skill's root.", }, }, required: ["skill", "command"], @@ -4079,7 +4074,7 @@ async function pipeChat( } else { throw new Error( "pipeChat: source must be a StreamTextResult (with .toUIMessageStream()), " + - "an AsyncIterable, or a ReadableStream" + "an AsyncIterable, or a ReadableStream" ); } @@ -4544,53 +4539,53 @@ export type BeforeTurnCompleteEvent< */ export type ChatSuspendEvent = | { - /** Suspend is happening after onPreload, before the first message. */ - phase: "preload"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** Suspend is happening after onPreload, before the first message. */ + phase: "preload"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** - * Suspend is happening on a continuation run that booted with no incoming - * message (post-`endRun`, post-waitpoint-timeout, etc.) and is waiting - * for the next session.in record before running any turn. Distinct from - * `phase: "preload"` — the chat already started; `onPreload` has not - * fired and will not fire on this run. - */ - phase: "continuation"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** + * Suspend is happening on a continuation run that booted with no incoming + * message (post-`endRun`, post-waitpoint-timeout, etc.) and is waiting + * for the next session.in record before running any turn. Distinct from + * `phase: "preload"` — the chat already started; `onPreload` has not + * fired and will not fire on this run. + */ + phase: "continuation"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** Suspend is happening after a completed turn, waiting for the next message. */ - phase: "turn"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** The turn number (0-indexed) that just completed. */ - turn: number; - /** The accumulated model messages after the completed turn. */ - messages: ModelMessage[]; - /** The accumulated UI messages after the completed turn. */ - uiMessages: TUIM[]; - /** Custom data from the frontend. */ - clientData?: TClientData; - }; + /** Suspend is happening after a completed turn, waiting for the next message. */ + phase: "turn"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** The turn number (0-indexed) that just completed. */ + turn: number; + /** The accumulated model messages after the completed turn. */ + messages: ModelMessage[]; + /** The accumulated UI messages after the completed turn. */ + uiMessages: TUIM[]; + /** Custom data from the frontend. */ + clientData?: TClientData; + }; /** * Discriminated event passed to the `onChatResume` callback. @@ -4598,52 +4593,52 @@ export type ChatSuspendEvent = | { - /** First message arrived after preload suspension. */ - phase: "preload"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** First message arrived after preload suspension. */ + phase: "preload"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** - * First message arrived after continuation-wait suspension. Distinct - * from `phase: "preload"` — the chat already started; this is a new - * run picking up after a prior run ended (`endRun`, waitpoint timeout, - * etc.). - */ - phase: "continuation"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** Custom data from the frontend. */ - clientData?: TClientData; - } + /** + * First message arrived after continuation-wait suspension. Distinct + * from `phase: "preload"` — the chat already started; this is a new + * run picking up after a prior run ended (`endRun`, waitpoint timeout, + * etc.). + */ + phase: "continuation"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** Custom data from the frontend. */ + clientData?: TClientData; + } | { - /** Next message arrived after turn suspension. */ - phase: "turn"; - /** Task run context. */ - ctx: TaskRunContext; - /** The chat session ID. */ - chatId: string; - /** The Trigger.dev run ID. */ - runId: string; - /** The turn number that was completed before suspension. */ - turn: number; - /** The accumulated model messages (from before suspension). */ - messages: ModelMessage[]; - /** The accumulated UI messages (from before suspension). */ - uiMessages: TUIM[]; - /** Custom data from the frontend. */ - clientData?: TClientData; - }; + /** Next message arrived after turn suspension. */ + phase: "turn"; + /** Task run context. */ + ctx: TaskRunContext; + /** The chat session ID. */ + chatId: string; + /** The Trigger.dev run ID. */ + runId: string; + /** The turn number that was completed before suspension. */ + turn: number; + /** The accumulated model messages (from before suspension). */ + messages: ModelMessage[]; + /** The accumulated UI messages (from before suspension). */ + uiMessages: TUIM[]; + /** Custom data from the frontend. */ + clientData?: TClientData; + }; export type ChatAgentOptions< TIdentifier extends string, @@ -4806,9 +4801,7 @@ export type ChatAgentOptions< * **Auto-piping:** If this function returns a value with `.toUIMessageStream()`, * the stream is automatically piped to the frontend. */ - run: ( - payload: ChatTaskRunPayload, TTools> - ) => Promise; + run: (payload: ChatTaskRunPayload, TTools>) => Promise; /** * Called once at the start of every run boot — for the initial run, for @@ -5573,12 +5566,9 @@ function chatAgent< // pump kicks in. Populated by `onRecoveryBoot.recoveredTurns` (or its // default, `inFlightUsers`). The turn-loop checks this queue ahead of // `messagesInput.waitWithIdleTimeout` so recovered turns fire first. - const bootInjectedQueue: ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >[] = []; - const couldHavePriorState = - payload.continuation === true || ctx.attempt.number > 1; + const bootInjectedQueue: ChatTaskWirePayload>[] = + []; + const couldHavePriorState = payload.continuation === true || ctx.attempt.number > 1; // `.in` resume cursor, computed at most once per boot. The boot // block below resolves it (snapshot field or records scan) and the @@ -5600,13 +5590,10 @@ function chatAgent< } catch (error) { // `readChatSnapshot` already swallows + warns internally; this catch // is just belt-and-suspenders against tracer/span errors. - logger.warn( - "chat.agent: snapshot read failed; continuing without snapshot", - { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - } - ); + logger.warn("chat.agent: snapshot read failed; continuing without snapshot", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } bootSpan.setAttribute("chat.boot.snapshot.durationMs", Date.now() - snapStart); bootSpan.setAttribute("chat.boot.snapshot.present", !!bootSnapshot); @@ -5636,26 +5623,19 @@ function chatAgent< const replayOutPhase = async () => { const replayOutStart = Date.now(); try { - const replayResult = await replaySessionOutTail( - sessionIdForSnapshot, - { lastEventId: bootSnapshot?.lastOutEventId } - ); + const replayResult = await replaySessionOutTail(sessionIdForSnapshot, { + lastEventId: bootSnapshot?.lastOutEventId, + }); replayedSettled = replayResult.settled; replayedPartial = replayResult.partial; replayedPartialRaw = replayResult.partialRaw; } catch (error) { - logger.warn( - "chat.agent: session.out replay failed; using snapshot only", - { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - } - ); + logger.warn("chat.agent: session.out replay failed; using snapshot only", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } - bootSpan.setAttribute( - "chat.boot.replay.out.durationMs", - Date.now() - replayOutStart - ); + bootSpan.setAttribute("chat.boot.replay.out.durationMs", Date.now() - replayOutStart); bootSpan.setAttribute("chat.boot.replay.out.settledCount", replayedSettled.length); bootSpan.setAttribute( "chat.boot.replay.out.partialPresent", @@ -5710,14 +5690,8 @@ function chatAgent< { error: error instanceof Error ? error.message : String(error) } ); } - bootSpan.setAttribute( - "chat.boot.replay.in.durationMs", - Date.now() - replayInStart - ); - bootSpan.setAttribute( - "chat.boot.replay.in.userCount", - replayedInTail.length - ); + bootSpan.setAttribute("chat.boot.replay.in.durationMs", Date.now() - replayInStart); + bootSpan.setAttribute("chat.boot.replay.in.userCount", replayedInTail.length); }; await Promise.all([replayOutPhase(), replayInPhase()]); @@ -5760,9 +5734,7 @@ function chatAgent< // - Snapshot exists at all (catches edge cases where the wire // didn't set `continuation` but a snapshot indicates prior turns) const needsResumeCursor = - ctx.attempt.number > 1 || - payload.continuation === true || - bootSnapshot !== undefined; + ctx.attempt.number > 1 || payload.continuation === true || bootSnapshot !== undefined; if (needsResumeCursor) { try { @@ -5854,8 +5826,7 @@ function chatAgent< if (Array.isArray(hookResult.chain)) hookChain = hookResult.chain; if (Array.isArray(hookResult.recoveredTurns)) hookRecoveredTurns = hookResult.recoveredTurns; - if (typeof hookResult.beforeBoot === "function") - hookBeforeBoot = hookResult.beforeBoot; + if (typeof hookResult.beforeBoot === "function") hookBeforeBoot = hookResult.beforeBoot; } } @@ -5912,8 +5883,7 @@ function chatAgent< // across attempts, but session.in records it once), the wire // payload already runs turn 0 — drop the duplicate from the queue // so we don't fire the same turn twice. - const wireMessageId = - (payload.message as { id?: string } | undefined)?.id; + const wireMessageId = (payload.message as { id?: string } | undefined)?.id; const metadataById = new Map(); for (const entry of replayedInTail) { metadataById.set(entry.message.id, entry.metadata); @@ -5993,7 +5963,6 @@ function chatAgent< // Make the seeded UI accumulator visible to `chat.history.*` // before any hook (`onChatStart`, `onTurnStart`, etc.) fires. locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); - } // Token usage tracking across turns @@ -6148,9 +6117,7 @@ function chatAgent< // `transport.preload(..., { idleTimeoutInSeconds })` / wire payload so // `chat.agent({ idleTimeoutInSeconds, preloadIdleTimeoutInSeconds })` is authoritative. const effectivePreloadIdleTimeout = - preloadIdleTimeoutInSeconds ?? - idleTimeoutInSeconds ?? - payload.idleTimeoutInSeconds; + preloadIdleTimeoutInSeconds ?? idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; const effectivePreloadTimeout = (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? @@ -6164,51 +6131,51 @@ function chatAgent< skipSuspend: exitAfterPreloadIdle, onSuspend: onChatSuspend ? async () => { - await tracer.startActiveSpan( - "onChatSuspend()", - async () => { - await onChatSuspend({ - phase: "preload", - ctx, - chatId: payload.chatId, - runId: currentRunId, - clientData: preloadClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.suspend.phase": "preload", + await tracer.startActiveSpan( + "onChatSuspend()", + async () => { + await onChatSuspend({ + phase: "preload", + ctx, + chatId: payload.chatId, + runId: currentRunId, + clientData: preloadClientData, + }); }, - } - ); - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.suspend.phase": "preload", + }, + } + ); + } : undefined, onResume: onChatResume ? async () => { - await tracer.startActiveSpan( - "onChatResume()", - async () => { - await onChatResume({ - phase: "preload", - ctx, - chatId: payload.chatId, - runId: currentRunId, - clientData: preloadClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.resume.phase": "preload", + await tracer.startActiveSpan( + "onChatResume()", + async () => { + await onChatResume({ + phase: "preload", + ctx, + chatId: payload.chatId, + runId: currentRunId, + clientData: preloadClientData, + }); }, - } - ); - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.resume.phase": "preload", + }, + } + ); + } : undefined, }); @@ -6279,10 +6246,7 @@ function chatAgent< // call entirely (the response is already complete). // `onTurnComplete` fires with the partial as // `responseMessage` so persistence works normally. - locals.set( - chatHandoverPartialKey, - handoverResult.output.partialAssistantMessage - ); + locals.set(chatHandoverPartialKey, handoverResult.output.partialAssistantMessage); // Stash the customer-side step-1 messageId. Turn-0 setup // uses it to seed the synthesized partial UIMessage with the // SAME id, so the agent's post-handover chunks merge into @@ -6338,341 +6302,340 @@ function chatAgent< if (bootInjectedQueue.length > 0) { currentWirePayload = bootInjectedQueue.shift()!; } else { - - const effectiveIdleTimeout = - idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const continuationResult = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for first message (continuation)", - onSuspend: onChatSuspend - ? async () => { - await tracer.startActiveSpan( - "onChatSuspend()", - async () => { - await onChatSuspend({ - phase: "continuation", - ctx, - chatId: payload.chatId, - runId: ctx.run.id, - clientData: continuationClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.suspend.phase": "continuation", + const effectiveIdleTimeout = idleTimeoutInSeconds ?? payload.idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; + + const continuationResult = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for first message (continuation)", + onSuspend: onChatSuspend + ? async () => { + await tracer.startActiveSpan( + "onChatSuspend()", + async () => { + await onChatSuspend({ + phase: "continuation", + ctx, + chatId: payload.chatId, + runId: ctx.run.id, + clientData: continuationClientData, + }); }, - } - ); - } - : undefined, - onResume: onChatResume - ? async () => { - await tracer.startActiveSpan( - "onChatResume()", - async () => { - await onChatResume({ - phase: "continuation", - ctx, - chatId: payload.chatId, - runId: ctx.run.id, - clientData: continuationClientData, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": payload.chatId, - "chat.resume.phase": "continuation", + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.suspend.phase": "continuation", + }, + } + ); + } + : undefined, + onResume: onChatResume + ? async () => { + await tracer.startActiveSpan( + "onChatResume()", + async () => { + await onChatResume({ + phase: "continuation", + ctx, + chatId: payload.chatId, + runId: ctx.run.id, + clientData: continuationClientData, + }); }, - } - ); - } - : undefined, - }); + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": payload.chatId, + "chat.resume.phase": "continuation", + }, + } + ); + } + : undefined, + }); - if (!continuationResult.ok) { - // Timed out waiting for the customer's next message — exit. - return; - } + if (!continuationResult.ok) { + // Timed out waiting for the customer's next message — exit. + return; + } - currentWirePayload = continuationResult.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; + currentWirePayload = continuationResult.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; - if (currentWirePayload.trigger === "close") { - return; - } + if (currentWirePayload.trigger === "close") { + return; + } } // end else (no boot-injected first turn) } for (let turn = 0; turn < maxTurns; turn++) { try { - // Extract turn-level context before entering the span. Slim - // wire: at most one delta message per record. `headStartMessages` - // is consumed at boot only (via `payload.headStartMessages`) - // and intentionally discarded here. - const { - metadata: wireMetadata, - message: incomingMessage, - headStartMessages: _hsm, - ...restWire - } = currentWirePayload; - void _hsm; - const incomingMessages: TUIMessage[] = incomingMessage - ? [incomingMessage as TUIMessage] - : []; - // Cleaning happens once here so `extractLastUserMessageText` and - // every downstream consumer see the same message shape — and - // `cleanupAbortedParts` no longer has to be re-applied below. - const cleanedIncomingMessages: TUIMessage[] = incomingMessages.map((msg) => - msg.role === "assistant" ? cleanupAbortedParts(msg) : msg - ); - const clientData = ( - parseClientData ? await parseClientData(wireMetadata) : wireMetadata - ) as inferSchemaOut; - const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages); - - // Actions are not turns. They use a different span name - // and don't carry a turn.number. Branched on at `isAction`. - const isAction = currentWirePayload.trigger === "action"; - const spanName = isAction ? "chat action" : `chat turn ${turn + 1}`; - - const turnAttributes: Attributes = { - ...(isAction ? {} : { "turn.number": turn + 1 }), - "gen_ai.conversation.id": currentWirePayload.chatId, - "gen_ai.operation.name": "chat", - "chat.trigger": currentWirePayload.trigger, - [SemanticInternalAttributes.STYLE_ICON]: isAction - ? "tabler-bolt" - : "tabler-message-chatbot", - [SemanticInternalAttributes.ENTITY_TYPE]: isAction ? "chat-action" : "chat-turn", - }; - - if (lastUserMessage) { - turnAttributes["chat.user_message"] = lastUserMessage; - - // Show a truncated preview of the user message as an accessory - const preview = - lastUserMessage.length > 80 ? lastUserMessage.slice(0, 80) + "..." : lastUserMessage; - Object.assign( - turnAttributes, - accessoryAttributes({ - items: [{ text: preview, variant: "normal" }], - style: "codepath", - }) - ); - } - - if (wireMetadata !== undefined) { - turnAttributes["chat.client_data"] = - typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata); - } + // Extract turn-level context before entering the span. Slim + // wire: at most one delta message per record. `headStartMessages` + // is consumed at boot only (via `payload.headStartMessages`) + // and intentionally discarded here. + const { + metadata: wireMetadata, + message: incomingMessage, + headStartMessages: _hsm, + ...restWire + } = currentWirePayload; + void _hsm; + const incomingMessages: TUIMessage[] = incomingMessage + ? [incomingMessage as TUIMessage] + : []; + // Cleaning happens once here so `extractLastUserMessageText` and + // every downstream consumer see the same message shape — and + // `cleanupAbortedParts` no longer has to be re-applied below. + const cleanedIncomingMessages: TUIMessage[] = incomingMessages.map((msg) => + msg.role === "assistant" ? cleanupAbortedParts(msg) : msg + ); + const clientData = ( + parseClientData ? await parseClientData(wireMetadata) : wireMetadata + ) as inferSchemaOut; + const lastUserMessage = extractLastUserMessageText(cleanedIncomingMessages); + + // Actions are not turns. They use a different span name + // and don't carry a turn.number. Branched on at `isAction`. + const isAction = currentWirePayload.trigger === "action"; + const spanName = isAction ? "chat action" : `chat turn ${turn + 1}`; + + const turnAttributes: Attributes = { + ...(isAction ? {} : { "turn.number": turn + 1 }), + "gen_ai.conversation.id": currentWirePayload.chatId, + "gen_ai.operation.name": "chat", + "chat.trigger": currentWirePayload.trigger, + [SemanticInternalAttributes.STYLE_ICON]: isAction + ? "tabler-bolt" + : "tabler-message-chatbot", + [SemanticInternalAttributes.ENTITY_TYPE]: isAction ? "chat-action" : "chat-turn", + }; - const turnResult = await tracer.startActiveSpan( - spanName, - async (turnSpan) => { - // (errors are caught by the outer try/catch which writes an error chunk) - locals.set(chatPipeCountKey, 0); - locals.set(chatDeferKey, new Set()); - locals.set(chatCompactionStateKey, undefined); - locals.set(chatSteeringQueueKey, []); - locals.set(chatResponsePartsKey, []); - // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected - // by deferred work from the previous turn's onTurnComplete need to - // survive into the next turn. The queue is drained before run(). - locals.set(chatInjectedMessageIdsKey, new Set()); + if (lastUserMessage) { + turnAttributes["chat.user_message"] = lastUserMessage; + + // Show a truncated preview of the user message as an accessory + const preview = + lastUserMessage.length > 80 + ? lastUserMessage.slice(0, 80) + "..." + : lastUserMessage; + Object.assign( + turnAttributes, + accessoryAttributes({ + items: [{ text: preview, variant: "normal" }], + style: "codepath", + }) + ); + } - // Store chat context for auto-detection by task-tool subtasks (ai.toolExecute / legacy ai.tool) - locals.set(chatTurnContextKey, { - chatId: currentWirePayload.chatId, - turn, - continuation, - clientData, - }); + if (wireMetadata !== undefined) { + turnAttributes["chat.client_data"] = + typeof wireMetadata === "string" ? wireMetadata : JSON.stringify(wireMetadata); + } - // Resolve the per-turn `tools` set now that turn context - // (incl. parsed clientData) exists, so every toModelMessages - // call this turn can re-apply tool `toModelOutput`. - await resolveTurnTools(); - - // Per-turn stop controller (reset each turn) - const stopController = new AbortController(); - currentStopController = stopController; - locals.set(chatStopControllerKey, stopController); - - // Three signals for the user's run function - const stopSignal = stopController.signal; - const cancelSignal = runSignal; - const combinedSignal = AbortSignal.any([runSignal, stopController.signal]); - - // Buffer messages that arrive during streaming - const pendingMessages: ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >[] = []; - const pmConfig = locals.get(chatPendingMessagesKey); - const msgSub = messagesInput.on(async (msg) => { - // If pendingMessages is configured, route to the steering queue - // instead of the wire buffer. The frontend handles re-sending - // non-injected messages via sendMessage on turn complete. - if (pmConfig) { - // Slim wire: at most one delta message per record. The - // pendingMessages handler reads `msg.message` directly - // instead of slicing an array — a wire record arrives - // with the new user message in `.message`, or no message - // at all (regenerate / preload / close / handover-prepare). - const lastUIMessage = msg.message as TUIMessage | undefined; - if (lastUIMessage) { - if (pmConfig.onReceived) { - try { - await pmConfig.onReceived({ - message: lastUIMessage as TUIMessage, - chatId: currentWirePayload.chatId, - turn, - }); - } catch { - /* non-fatal */ - } - } + const turnResult = await tracer.startActiveSpan( + spanName, + async (turnSpan) => { + // (errors are caught by the outer try/catch which writes an error chunk) + locals.set(chatPipeCountKey, 0); + locals.set(chatDeferKey, new Set()); + locals.set(chatCompactionStateKey, undefined); + locals.set(chatSteeringQueueKey, []); + locals.set(chatResponsePartsKey, []); + // NOTE: chatBackgroundQueueKey is NOT reset here — messages injected + // by deferred work from the previous turn's onTurnComplete need to + // survive into the next turn. The queue is drained before run(). + locals.set(chatInjectedMessageIdsKey, new Set()); + + // Store chat context for auto-detection by task-tool subtasks (ai.toolExecute / legacy ai.tool) + locals.set(chatTurnContextKey, { + chatId: currentWirePayload.chatId, + turn, + continuation, + clientData, + }); + // Resolve the per-turn `tools` set now that turn context + // (incl. parsed clientData) exists, so every toModelMessages + // call this turn can re-apply tool `toModelOutput`. + await resolveTurnTools(); + + // Per-turn stop controller (reset each turn) + const stopController = new AbortController(); + currentStopController = stopController; + locals.set(chatStopControllerKey, stopController); + + // Three signals for the user's run function + const stopSignal = stopController.signal; + const cancelSignal = runSignal; + const combinedSignal = AbortSignal.any([runSignal, stopController.signal]); + + // Buffer messages that arrive during streaming + const pendingMessages: ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >[] = []; + const pmConfig = locals.get(chatPendingMessagesKey); + const msgSub = messagesInput.on(async (msg) => { + // If pendingMessages is configured, route to the steering queue + // instead of the wire buffer. The frontend handles re-sending + // non-injected messages via sendMessage on turn complete. + if (pmConfig) { + // Slim wire: at most one delta message per record. The + // pendingMessages handler reads `msg.message` directly + // instead of slicing an array — a wire record arrives + // with the new user message in `.message`, or no message + // at all (regenerate / preload / close / handover-prepare). + const lastUIMessage = msg.message as TUIMessage | undefined; + if (lastUIMessage) { + if (pmConfig.onReceived) { try { - const queue = locals.get(chatSteeringQueueKey) ?? []; - // Deduplicate by message ID — guards against double-sends - if ( - lastUIMessage.id && - queue.some((e) => e.uiMessage.id === lastUIMessage.id) - ) { - return; - } - const modelMsgs = await toModelMessages([lastUIMessage]); - queue.push({ - uiMessage: lastUIMessage as UIMessage, - modelMessages: modelMsgs, + await pmConfig.onReceived({ + message: lastUIMessage as TUIMessage, + chatId: currentWirePayload.chatId, + turn, }); - locals.set(chatSteeringQueueKey, queue); } catch { - /* conversion failed — skip steering queue */ + /* non-fatal */ } } - return; // Don't add to wire buffer — frontend handles non-injected case - } - // No pendingMessages config — standard wire buffer for next turn - pendingMessages.push( - msg as ChatTaskWirePayload> - ); - }); - - // Track new messages for this turn (user input + assistant response). - const turnNewModelMessages: ModelMessage[] = []; - const turnNewUIMessages: TUIMessage[] = []; - - // ── Action handling ────────────────────────────────────── - // Actions arrive on the same input stream but with - // trigger === "action". They are NOT turns — only - // `hydrateMessages` and `onAction` fire. No turn lifecycle - // hooks (`onTurnStart` / `prepareMessages` / - // `onBeforeTurnComplete` / `onTurnComplete`) and no - // `run()` invocation. To produce a model response from - // an action, return a `StreamTextResult` (auto-piped), - // string, or UIMessage from `onAction`. Turn counter - // does not advance. - let actionStreamResult: unknown = undefined; - if (isAction) { - // Parse and validate the action payload - const parsedAction = parseAction - ? await parseAction(currentWirePayload.action) - : currentWirePayload.action; - - // Hydrate messages from backend if configured - if (hydrateMessages) { - const hydrated = await tracer.startActiveSpan( - "hydrateMessages()", - async () => { - return hydrateMessages({ - chatId: currentWirePayload.chatId, - turn, - trigger: "action", - incomingMessages: [] as TUIMessage[], - previousMessages: [...accumulatedUIMessages], - clientData, - continuation, - previousRunId, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.trigger": "action", - }, + try { + const queue = locals.get(chatSteeringQueueKey) ?? []; + // Deduplicate by message ID — guards against double-sends + if ( + lastUIMessage.id && + queue.some((e) => e.uiMessage.id === lastUIMessage.id) + ) { + return; } - ); - accumulatedUIMessages = [...hydrated] as TUIMessage[]; - accumulatedMessages = await toModelMessages(hydrated); - locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + const modelMsgs = await toModelMessages([lastUIMessage]); + queue.push({ + uiMessage: lastUIMessage as UIMessage, + modelMessages: modelMsgs, + }); + locals.set(chatSteeringQueueKey, queue); + } catch { + /* conversion failed — skip steering queue */ + } } + return; // Don't add to wire buffer — frontend handles non-injected case + } - // Fire onAction — handler may mutate state via - // `chat.history.*` and / or return a model response. - if (onAction) { - actionStreamResult = await tracer.startActiveSpan( - "onAction()", - async () => { - return await onAction({ - action: parsedAction as any, - chatId: currentWirePayload.chatId, - turn, - clientData, - uiMessages: accumulatedUIMessages, - messages: accumulatedMessages, - }); + // No pendingMessages config — standard wire buffer for next turn + pendingMessages.push( + msg as ChatTaskWirePayload> + ); + }); + + // Track new messages for this turn (user input + assistant response). + const turnNewModelMessages: ModelMessage[] = []; + const turnNewUIMessages: TUIMessage[] = []; + + // ── Action handling ────────────────────────────────────── + // Actions arrive on the same input stream but with + // trigger === "action". They are NOT turns — only + // `hydrateMessages` and `onAction` fire. No turn lifecycle + // hooks (`onTurnStart` / `prepareMessages` / + // `onBeforeTurnComplete` / `onTurnComplete`) and no + // `run()` invocation. To produce a model response from + // an action, return a `StreamTextResult` (auto-piped), + // string, or UIMessage from `onAction`. Turn counter + // does not advance. + let actionStreamResult: unknown = undefined; + if (isAction) { + // Parse and validate the action payload + const parsedAction = parseAction + ? await parseAction(currentWirePayload.action) + : currentWirePayload.action; + + // Hydrate messages from backend if configured + if (hydrateMessages) { + const hydrated = await tracer.startActiveSpan( + "hydrateMessages()", + async () => { + return hydrateMessages({ + chatId: currentWirePayload.chatId, + turn, + trigger: "action", + incomingMessages: [] as TUIMessage[], + previousMessages: [...accumulatedUIMessages], + clientData, + continuation, + previousRunId, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.trigger": "action", }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.action": - typeof parsedAction === "object" && parsedAction !== null - ? JSON.stringify(parsedAction) - : String(parsedAction), - }, - } - ); + } + ); + accumulatedUIMessages = [...hydrated] as TUIMessage[]; + accumulatedMessages = await toModelMessages(hydrated); + locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + } - // Apply chat.history mutations from onAction - const actionOverride = locals.get(chatOverrideMessagesKey); - if (actionOverride) { - locals.set(chatOverrideMessagesKey, undefined); - accumulatedUIMessages = [...actionOverride] as TUIMessage[]; - accumulatedMessages = await toModelMessages(actionOverride); - locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + // Fire onAction — handler may mutate state via + // `chat.history.*` and / or return a model response. + if (onAction) { + actionStreamResult = await tracer.startActiveSpan( + "onAction()", + async () => { + return await onAction({ + action: parsedAction as any, + chatId: currentWirePayload.chatId, + turn, + clientData, + uiMessages: accumulatedUIMessages, + messages: accumulatedMessages, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.action": + typeof parsedAction === "object" && parsedAction !== null + ? JSON.stringify(parsedAction) + : String(parsedAction), + }, } - } else { - warnMissingOnActionOnce(); + ); + + // Apply chat.history mutations from onAction + const actionOverride = locals.get(chatOverrideMessagesKey); + if (actionOverride) { + locals.set(chatOverrideMessagesKey, undefined); + accumulatedUIMessages = [...actionOverride] as TUIMessage[]; + accumulatedMessages = await toModelMessages(actionOverride); + locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); } + } else { + warnMissingOnActionOnce(); } + } - // ── Message handling (non-action turns) ─────────────────── - // - // Slim wire: at most one delta message arrives per record. - // The accumulator was already seeded at boot from a durable - // snapshot + `session.out` replay (or `hydrateMessages`, - // which also fires per-turn below). Per-turn handling is - // therefore a delta merge, not a full-history reset. - if (currentWirePayload.trigger !== "action") { - + // ── Message handling (non-action turns) ─────────────────── + // + // Slim wire: at most one delta message arrives per record. + // The accumulator was already seeded at boot from a durable + // snapshot + `session.out` replay (or `hydrateMessages`, + // which also fires per-turn below). Per-turn handling is + // therefore a delta merge, not a full-history reset. + if (currentWirePayload.trigger !== "action") { let cleanedUIMessages: TUIMessage[] = cleanedIncomingMessages; // Turn-0 head-start with hydrateMessages: the boot seeding from @@ -6809,8 +6772,7 @@ function chatAgent< ) { const lastUI = cleanedUIMessages[cleanedUIMessages.length - 1]!; const matchedExisting = - lastUI.id !== undefined && - previouslyKnownMessageIds.has(lastUI.id); + lastUI.id !== undefined && previouslyKnownMessageIds.has(lastUI.id); if (!matchedExisting) { turnNewUIMessages.push(lastUI); const lastModel = (await toModelMessages([lastUI]))[0]; @@ -6855,16 +6817,12 @@ function chatAgent< let replaced = false; for (const raw of cleanedUIMessages) { let incoming = raw; - let idx = accumulatedUIMessages.findIndex( - (m) => m.id === incoming.id - ); + let idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id); if (idx === -1) { const rewritten = rewriteIncomingIdViaToolCallMap(incoming); if (rewritten.id !== incoming.id) { incoming = rewritten as typeof raw; - idx = accumulatedUIMessages.findIndex( - (m) => m.id === incoming.id - ); + idx = accumulatedUIMessages.findIndex((m) => m.id === incoming.id); } } if (idx !== -1) { @@ -6888,9 +6846,7 @@ function chatAgent< accumulatedMessages.push(...incomingModelMessages); } if (turnNewUIMessages.length > 0) { - turnNewModelMessages.push( - ...(await toModelMessages(turnNewUIMessages)) - ); + turnNewModelMessages.push(...(await toModelMessages(turnNewUIMessages))); } } // `preload` / `close` / `handover-prepare` and submits @@ -6931,57 +6887,54 @@ function chatAgent< } locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages); + } // end if (trigger !== "action") + + // ── Action result handling ────────────────────────────── + // For action turns, skip the turn machinery entirely. + // If `onAction` returned a stream / string / UIMessage, + // pipe it as the response. Either way, emit + // `trigger:turn-complete` and then fall through to the + // wait-for-next-message logic (shared with message turns). + // The turn counter is decremented so the next iteration + // sees the same `turn` value — actions don't count. + if (isAction) { + msgSub.off(); - } // end if (trigger !== "action") - - // ── Action result handling ────────────────────────────── - // For action turns, skip the turn machinery entirely. - // If `onAction` returned a stream / string / UIMessage, - // pipe it as the response. Either way, emit - // `trigger:turn-complete` and then fall through to the - // wait-for-next-message logic (shared with message turns). - // The turn counter is decremented so the next iteration - // sees the same `turn` value — actions don't count. - if (isAction) { - msgSub.off(); - - if ( - (locals.get(chatPipeCountKey) ?? 0) === 0 && - isUIMessageStreamable(actionStreamResult) - ) { - try { - const resolvedOptions = resolveUIMessageStreamOptions(); - const uiStream = ( - actionStreamResult as UIMessageStreamable - ).toUIMessageStream({ - ...resolvedOptions, - generateMessageId: - resolvedOptions.generateMessageId ?? generateMessageId, - }); - await pipeChat(uiStream, { - signal: combinedSignal, - spanName: "stream response", - }); - } catch (error) { - if ( - error instanceof Error && - error.name === "AbortError" && - runSignal.aborted - ) { - return "exit"; - } - throw error; + if ( + (locals.get(chatPipeCountKey) ?? 0) === 0 && + isUIMessageStreamable(actionStreamResult) + ) { + try { + const resolvedOptions = resolveUIMessageStreamOptions(); + const uiStream = ( + actionStreamResult as UIMessageStreamable + ).toUIMessageStream({ + ...resolvedOptions, + generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId, + }); + await pipeChat(uiStream, { + signal: combinedSignal, + spanName: "stream response", + }); + } catch (error) { + if ( + error instanceof Error && + error.name === "AbortError" && + runSignal.aborted + ) { + return "exit"; } + throw error; } - - await writeTurnCompleteChunk(currentWirePayload.chatId); - - // Don't consume a turn iteration — actions aren't turns. - turn--; } - if (!isAction) { + await writeTurnCompleteChunk(currentWirePayload.chatId); + // Don't consume a turn iteration — actions aren't turns. + turn--; + } + + if (!isAction) { // Mint a scoped public access token once per turn, reused for // onChatStart, onTurnStart, onTurnComplete, and the turn-complete chunk. const currentRunId = ctx.run.id; @@ -7159,7 +7112,10 @@ function chatAgent< // Don't call userRun. Don't pipe. Skip directly // to the post-turn flow below. } else { - const preparedMessages = await applyPrepareMessages(accumulatedMessages, "run"); + const preparedMessages = await applyPrepareMessages( + accumulatedMessages, + "run" + ); runResult = await userRun({ ...restWire, messages: preparedMessages, @@ -7182,7 +7138,10 @@ function chatAgent< // We call toUIMessageStream ourselves to attach onFinish for response capture. // Pass originalMessages so the AI SDK reuses message IDs across turns // (e.g. for tool approval continuations / HITL flows). - if ((locals.get(chatPipeCountKey) ?? 0) === 0 && isUIMessageStreamable(runResult)) { + if ( + (locals.get(chatPipeCountKey) ?? 0) === 0 && + isUIMessageStreamable(runResult) + ) { onFinishAttached = true; const resolvedOptions = resolveUIMessageStreamOptions(); // For action turns, don't pass originalMessages: the response @@ -7195,9 +7154,7 @@ function chatAgent< // Pass originalMessages so the AI SDK reuses message IDs across // turns (e.g. for tool approval continuations / HITL flows). // Omit for action turns to force a fresh response ID. - ...(isActionTurn - ? {} - : { originalMessages: accumulatedUIMessages }), + ...(isActionTurn ? {} : { originalMessages: accumulatedUIMessages }), // Always provide generateMessageId so the start chunk carries a // messageId. Without this, the frontend and backend generate IDs // independently and they won't match for ID-based dedup. @@ -7214,7 +7171,10 @@ function chatAgent< resolveOnFinish!(); }, }); - await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" }); + await pipeChat(uiStream, { + signal: combinedSignal, + spanName: "stream response", + }); } } catch (error) { // Handle AbortError from streamText gracefully @@ -7248,7 +7208,10 @@ function chatAgent< // never reports final usage), which would block the turn loop // from ever firing onTurnComplete / writeTurnComplete. let turnUsage: LanguageModelUsage | undefined; - if (runResult != null && typeof (runResult as any).totalUsage?.then === "function") { + if ( + runResult != null && + typeof (runResult as any).totalUsage?.then === "function" + ) { try { turnUsage = (await Promise.race([ (runResult as any).totalUsage, @@ -7352,7 +7315,10 @@ function chatAgent< // may produce a message with an empty ID since IDs are normally // assigned by the frontend's useChat). if (!capturedResponseMessage.id) { - capturedResponseMessage = { ...capturedResponseMessage, id: generateMessageId() }; + capturedResponseMessage = { + ...capturedResponseMessage, + id: generateMessageId(), + }; } // Append any non-transient data parts queued via chat.response or writer.write() const queuedParts = locals.get(chatResponsePartsKey); @@ -7369,9 +7335,7 @@ function chatAgent< // instead of pushing a duplicate. For action turns this never // matches because originalMessages is omitted (fresh ID). const existingIdx = capturedResponseMessage.id - ? accumulatedUIMessages.findIndex( - (m) => m.id === capturedResponseMessage!.id - ) + ? accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage!.id) : -1; if (existingIdx !== -1) { accumulatedUIMessages[existingIdx] = capturedResponseMessage; @@ -7494,16 +7458,16 @@ function chatAgent< accumulatedMessages = outerCompaction.compactModelMessages ? await outerCompaction.compactModelMessages(outerCompactEvent) : [ - { - role: "assistant" as const, - content: [ - { - type: "text" as const, - text: `[Conversation summary]\n\n${summary}`, - }, - ], - }, - ]; + { + role: "assistant" as const, + content: [ + { + type: "text" as const, + text: `[Conversation summary]\n\n${summary}`, + }, + ], + }, + ]; // UI messages: callback or default (preserve all) if (outerCompaction.compactUIMessages) { @@ -7531,7 +7495,10 @@ function chatAgent< }); } - compactionSpan.setAttribute("compaction.summary_length", summary.length); + compactionSpan.setAttribute( + "compaction.summary_length", + summary.length + ); write({ type: "data-compaction", @@ -7626,7 +7593,9 @@ function chatAgent< // Drain any late response parts added during onBeforeTurnComplete const lateParts = locals.get(chatResponsePartsKey); if (lateParts && lateParts.length > 0 && capturedResponseMessage) { - const idx = accumulatedUIMessages.findIndex((m) => m.id === capturedResponseMessage!.id); + const idx = accumulatedUIMessages.findIndex( + (m) => m.id === capturedResponseMessage!.id + ); if (idx !== -1) { const msg = accumulatedUIMessages[idx]!; accumulatedUIMessages[idx] = { @@ -7688,7 +7657,9 @@ function chatAgent< ? { "gen_ai.usage.total_tokens": turnUsage.totalTokens } : {}), ...(cumulativeUsage.totalTokens != null - ? { "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens } + ? { + "gen_ai.usage.cumulative_total_tokens": cumulativeUsage.totalTokens, + } : {}), }, } @@ -7718,17 +7689,14 @@ function chatAgent< await tracer.startActiveSpan( "snapshot.write", async () => { - const snapshotInCursor = - getChatSession().in.lastDispatchedSeqNum(); + const snapshotInCursor = getChatSession().in.lastDispatchedSeqNum(); await writeChatSnapshot(sessionIdForSnapshot, { version: 1, savedAt: Date.now(), messages: accumulatedUIMessages, lastOutEventId: turnCompleteResult?.lastEventId, lastInEventId: - snapshotInCursor !== undefined - ? String(snapshotInCursor) - : undefined, + snapshotInCursor !== undefined ? String(snapshotInCursor) : undefined, }); }, { @@ -7751,54 +7719,50 @@ function chatAgent< ); } } + } // end if (!isAction) + + // NOTE: We intentionally do NOT await deferred work from onTurnComplete here. + // Promises deferred in onTurnComplete (e.g. background self-review via + // chat.defer + chat.inject) run during the idle wait. If they complete + // before the next message, their injected context is picked up in prepareStep. + // The pre-onBeforeTurnComplete drain handles promises from onTurnStart/run(). + + // Recovery-boot injection: drain remaining recovered turns + // before any other source. `onRecoveryBoot` (or its default) + // produced these from in-flight user messages on session.in + // that the dead predecessor never acknowledged. + if (bootInjectedQueue.length > 0) { + currentWirePayload = bootInjectedQueue.shift()!; + return "continue"; + } - } // end if (!isAction) - - // NOTE: We intentionally do NOT await deferred work from onTurnComplete here. - // Promises deferred in onTurnComplete (e.g. background self-review via - // chat.defer + chat.inject) run during the idle wait. If they complete - // before the next message, their injected context is picked up in prepareStep. - // The pre-onBeforeTurnComplete drain handles promises from onTurnStart/run(). - - // Recovery-boot injection: drain remaining recovered turns - // before any other source. `onRecoveryBoot` (or its default) - // produced these from in-flight user messages on session.in - // that the dead predecessor never acknowledged. - if (bootInjectedQueue.length > 0) { - currentWirePayload = bootInjectedQueue.shift()!; - return "continue"; - } - - // If messages arrived during streaming (without pendingMessages config), - // use the first one immediately as the next turn. - if (pendingMessages.length > 0) { - currentWirePayload = pendingMessages[0]!; - return "continue"; - } + // If messages arrived during streaming (without pendingMessages config), + // use the first one immediately as the next turn. + if (pendingMessages.length > 0) { + currentWirePayload = pendingMessages[0]!; + return "continue"; + } - // chat.requestUpgrade() was called — exit the loop so the - // transport triggers a new run on the latest version. - // chat.endRun() — same exit, no upgrade semantics. - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { - return "exit"; - } + // chat.requestUpgrade() was called — exit the loop so the + // transport triggers a new run on the latest version. + // chat.endRun() — same exit, no upgrade semantics. + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { + return "exit"; + } - // Wait for the next message — stay idle briefly, then suspend - const effectiveIdleTimeout = - (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? - idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const next = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for next message", - onSuspend: onChatSuspend - ? async () => { + // Wait for the next message — stay idle briefly, then suspend + const effectiveIdleTimeout = + (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? + idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; + + const next = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for next message", + onSuspend: onChatSuspend + ? async () => { await tracer.startActiveSpan( "onChatSuspend()", async () => { @@ -7824,9 +7788,9 @@ function chatAgent< } ); } - : undefined, - onResume: onChatResume - ? async () => { + : undefined, + onResume: onChatResume + ? async () => { await tracer.startActiveSpan( "onChatResume()", async () => { @@ -7852,204 +7816,202 @@ function chatAgent< } ); } - : undefined, - }); - - if (!next.ok) { - return "exit"; - } + : undefined, + }); - currentWirePayload = next.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; + if (!next.ok) { + return "exit"; + } - // Close signal — exit the loop gracefully - if (currentWirePayload.trigger === "close") { - return "exit"; - } + currentWirePayload = next.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; - return "continue"; - }, - { - attributes: turnAttributes, + // Close signal — exit the loop gracefully + if (currentWirePayload.trigger === "close") { + return "exit"; } - ); - if (turnResult === "exit") return; - // "continue" means proceed to next iteration - } catch (turnError) { - // Turn error handler: write an error chunk + turn-complete to the stream - // so the client sees the error, then wait for the next message instead - // of killing the entire run. This keeps the conversation alive. - if (turnError instanceof Error && turnError.name === "AbortError" && runSignal.aborted) { - // Full run cancellation — exit immediately - throw turnError; + return "continue"; + }, + { + attributes: turnAttributes, } + ); - // OOM errors must escape the turn loop so the task runtime can - // honor `retry.outOfMemory.machine` (set on chat.agent via - // `oomMachine`). Catching them here would keep the dead worker - // alive and defeat the machine swap. Re-throw and let the - // runtime dispatch the retry on a larger machine; recovery on - // attempt 2 picks up via the standard continuation path - // (same chatId / Session, accumulator rehydrates). - if (turnError instanceof OutOfMemoryError) { - throw turnError; - } + if (turnResult === "exit") return; + // "continue" means proceed to next iteration + } catch (turnError) { + // Turn error handler: write an error chunk + turn-complete to the stream + // so the client sees the error, then wait for the next message instead + // of killing the entire run. This keeps the conversation alive. + if ( + turnError instanceof Error && + turnError.name === "AbortError" && + runSignal.aborted + ) { + // Full run cancellation — exit immediately + throw turnError; + } - let errorTurnCompleteResult: Awaited< - ReturnType - > | undefined; - try { - await withChatWriter(async (writer) => { - const errorText = - turnError instanceof Error ? turnError.message : "An unexpected error occurred"; - writer.write({ type: "error", errorText } as any); - }); - // Signal turn complete so the client knows this turn is done - errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId); - } catch { - // Best-effort — if stream write fails, let the run continue anyway - } + // OOM errors must escape the turn loop so the task runtime can + // honor `retry.outOfMemory.machine` (set on chat.agent via + // `oomMachine`). Catching them here would keep the dead worker + // alive and defeat the machine swap. Re-throw and let the + // runtime dispatch the retry on a larger machine; recovery on + // attempt 2 picks up via the standard continuation path + // (same chatId / Session, accumulator rehydrates). + if (turnError instanceof OutOfMemoryError) { + throw turnError; + } - // The submit-message merge into the accumulator may not have run - // yet (a pre-run hook threw), so fold the wire message in for the - // error event + snapshot — the cursor has already advanced past it, - // so otherwise it survives in neither the snapshot nor the `.in` tail. - const erroredWireMessage = (currentWirePayload as { message?: TUIMessage }).message; - const erroredUIMessages = - erroredWireMessage && - !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id) - ? [...accumulatedUIMessages, erroredWireMessage] - : accumulatedUIMessages; - - // Fire onTurnComplete on the error path too — the docs promise it - // runs "after every turn, successful or errored" so customers can - // mark the turn failed. `responseMessage` is undefined/partial and - // `error` carries the thrown value. - if (onTurnComplete) { - try { - await tracer.startActiveSpan( - "onTurnComplete()", - async () => { - await onTurnComplete({ - ctx, - chatId: currentWirePayload.chatId, - messages: accumulatedMessages, - uiMessages: erroredUIMessages, - newMessages: [], - newUIMessages: erroredWireMessage ? [erroredWireMessage] : [], - responseMessage: undefined, - rawResponseMessage: undefined, - turn, - runId: ctx.run.id, - chatAccessToken: "", - // Parsed `clientData` isn't reliably in scope here (parsing - // may itself be the failure), and the raw metadata is the - // wrong shape — leave it undefined on the error path. - clientData: undefined, - stopped: false, - continuation, - previousRunId, - preloaded, - totalUsage: cumulativeUsage, - finishReason: "error", - error: turnError, - lastEventId: errorTurnCompleteResult?.lastEventId, - }); + let errorTurnCompleteResult: + | Awaited> + | undefined; + try { + await withChatWriter(async (writer) => { + const errorText = + turnError instanceof Error ? turnError.message : "An unexpected error occurred"; + writer.write({ type: "error", errorText } as any); + }); + // Signal turn complete so the client knows this turn is done + errorTurnCompleteResult = await writeTurnCompleteChunk(currentWirePayload.chatId); + } catch { + // Best-effort — if stream write fails, let the run continue anyway + } + + // The submit-message merge into the accumulator may not have run + // yet (a pre-run hook threw), so fold the wire message in for the + // error event + snapshot — the cursor has already advanced past it, + // so otherwise it survives in neither the snapshot nor the `.in` tail. + const erroredWireMessage = (currentWirePayload as { message?: TUIMessage }).message; + const erroredUIMessages = + erroredWireMessage && + !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id) + ? [...accumulatedUIMessages, erroredWireMessage] + : accumulatedUIMessages; + + // Fire onTurnComplete on the error path too — the docs promise it + // runs "after every turn, successful or errored" so customers can + // mark the turn failed. `responseMessage` is undefined/partial and + // `error` carries the thrown value. + if (onTurnComplete) { + try { + await tracer.startActiveSpan( + "onTurnComplete()", + async () => { + await onTurnComplete({ + ctx, + chatId: currentWirePayload.chatId, + messages: accumulatedMessages, + uiMessages: erroredUIMessages, + newMessages: [], + newUIMessages: erroredWireMessage ? [erroredWireMessage] : [], + responseMessage: undefined, + rawResponseMessage: undefined, + turn, + runId: ctx.run.id, + chatAccessToken: "", + // Parsed `clientData` isn't reliably in scope here (parsing + // may itself be the failure), and the raw metadata is the + // wrong shape — leave it undefined on the error path. + clientData: undefined, + stopped: false, + continuation, + previousRunId, + preloaded, + totalUsage: cumulativeUsage, + finishReason: "error", + error: turnError, + lastEventId: errorTurnCompleteResult?.lastEventId, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + "chat.id": currentWirePayload.chatId, + "chat.turn": turn + 1, + "chat.errored": true, }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", - [SemanticInternalAttributes.COLLAPSED]: true, - "chat.id": currentWirePayload.chatId, - "chat.turn": turn + 1, - "chat.errored": true, - }, - } - ); - } catch { - // A throwing onTurnComplete on the error path must not crash - // the run — keep the conversation alive for the next message. - } + } + ); + } catch { + // A throwing onTurnComplete on the error path must not crash + // the run — keep the conversation alive for the next message. } + } - // Persist a snapshot so the failed turn's user message isn't - // stranded. `writeTurnCompleteChunk` already advanced the `.in` - // cursor past it (via the session-in-event-id header), and the - // success-path snapshot write is skipped on error — without this - // the next boot would resume past a message that exists in - // neither the snapshot nor the replayable `.in` tail. - if (!hydrateMessages) { - try { - const errorSnapshotInCursor = - getChatSession().in.lastDispatchedSeqNum(); - await writeChatSnapshot(sessionIdForSnapshot, { - version: 1, - savedAt: Date.now(), - messages: erroredUIMessages, - lastOutEventId: errorTurnCompleteResult?.lastEventId, - lastInEventId: - errorSnapshotInCursor !== undefined - ? String(errorSnapshotInCursor) - : undefined, - }); - } catch (error) { - logger.warn("chat.agent: error-path snapshot write failed", { - error: error instanceof Error ? error.message : String(error), - sessionId: sessionIdForSnapshot, - }); - } + // Persist a snapshot so the failed turn's user message isn't + // stranded. `writeTurnCompleteChunk` already advanced the `.in` + // cursor past it (via the session-in-event-id header), and the + // success-path snapshot write is skipped on error — without this + // the next boot would resume past a message that exists in + // neither the snapshot nor the replayable `.in` tail. + if (!hydrateMessages) { + try { + const errorSnapshotInCursor = getChatSession().in.lastDispatchedSeqNum(); + await writeChatSnapshot(sessionIdForSnapshot, { + version: 1, + savedAt: Date.now(), + messages: erroredUIMessages, + lastOutEventId: errorTurnCompleteResult?.lastEventId, + lastInEventId: + errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined, + }); + } catch (error) { + logger.warn("chat.agent: error-path snapshot write failed", { + error: error instanceof Error ? error.message : String(error), + sessionId: sessionIdForSnapshot, + }); } + } - // chat.requestUpgrade() / chat.endRun() — exit after error turn too - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { - return; - } + // chat.requestUpgrade() / chat.endRun() — exit after error turn too + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { + return; + } - // Drain remaining recovered turns before idling — a thrown - // recovered turn shouldn't strand the rest of the boot queue - // until an unrelated live message arrives. - if (bootInjectedQueue.length > 0) { - currentWirePayload = bootInjectedQueue.shift()!; - continue; - } + // Drain remaining recovered turns before idling — a thrown + // recovered turn shouldn't strand the rest of the boot queue + // until an unrelated live message arrives. + if (bootInjectedQueue.length > 0) { + currentWirePayload = bootInjectedQueue.shift()!; + continue; + } - // Wait for the next message — same as after a successful turn - const effectiveIdleTimeout = - (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? - idleTimeoutInSeconds; - const effectiveTurnTimeout = - (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - - const next = await messagesInput.waitWithIdleTimeout({ - idleTimeoutInSeconds: effectiveIdleTimeout, - timeout: effectiveTurnTimeout, - spanName: "waiting for next message (after error)", - }); + // Wait for the next message — same as after a successful turn + const effectiveIdleTimeout = + (metadata.get(IDLE_TIMEOUT_METADATA_KEY) as number | undefined) ?? + idleTimeoutInSeconds; + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; - if (!next.ok) { - return; // Timed out — end run gracefully - } + const next = await messagesInput.waitWithIdleTimeout({ + idleTimeoutInSeconds: effectiveIdleTimeout, + timeout: effectiveTurnTimeout, + spanName: "waiting for next message (after error)", + }); - currentWirePayload = next.output as ChatTaskWirePayload< - TUIMessage, - inferSchemaIn - >; - // Continue to next iteration of the for loop + if (!next.ok) { + return; // Timed out — end run gracefully } + + currentWirePayload = next.output as ChatTaskWirePayload< + TUIMessage, + inferSchemaIn + >; + // Continue to next iteration of the for loop } - } finally { - // `stopSub` is registered post-preload so the close-during-preload - // early-return path may exit before it ever attached. Guard the - // cleanup so a missing subscription doesn't throw. - stopSub?.off(); } - } + } finally { + // `stopSub` is registered post-preload so the close-during-preload + // early-return path may exit before it ever attached. Guard the + // cleanup so a missing subscription doesn't throw. + stopSub?.off(); + } + }, }); // Register clientDataSchema so the CLI converts it to JSONSchema @@ -8145,7 +8107,9 @@ export interface ChatBuilder< ): ChatBuilder; /** Register a builder-level `onCompacted` hook. Runs before the task-level hook if both are set. */ - onCompacted(fn: (event: CompactedEvent) => Promise | void): ChatBuilder; + onCompacted( + fn: (event: CompactedEvent) => Promise | void + ): ChatBuilder; /** Register a builder-level `onChatSuspend` hook. Runs before the task-level hook if both are set. */ onChatSuspend( @@ -8169,12 +8133,24 @@ export interface ChatBuilder< * (backwards compatible). */ agent: [TClientDataSchema] extends [undefined] - ? ( - options: ChatAgentOptions - ) => Task>, unknown> - : ( - options: Omit, "clientDataSchema"> - ) => Task>, unknown>; + ? < + TId extends string, + TInfer extends TaskSchema | undefined = undefined, + TAction extends TaskSchema | undefined = undefined, + TTools extends ToolSet = ToolSet, + >( + options: ChatAgentOptions + ) => Task>, unknown> + : < + TId extends string, + TAction extends TaskSchema | undefined = undefined, + TTools extends ToolSet = ToolSet, + >( + options: Omit< + ChatAgentOptions, + "clientDataSchema" + > + ) => Task>, unknown>; /** * Create a custom agent with manual lifecycle control. @@ -8246,9 +8222,7 @@ function createChatBuilder< }); }, - onBoot( - fn: (event: BootEvent>) => Promise | void - ) { + onBoot(fn: (event: BootEvent>) => Promise | void) { return createChatBuilder({ ...config, hooks: { ...config.hooks, onBoot: fn }, @@ -8331,7 +8305,7 @@ function createChatBuilder< const mergedUiStream = config.uiStreamOptions && options.uiMessageStreamOptions ? { ...config.uiStreamOptions, ...options.uiMessageStreamOptions } - : options.uiMessageStreamOptions ?? config.uiStreamOptions; + : (options.uiMessageStreamOptions ?? config.uiStreamOptions); return chatAgent({ ...options, @@ -9075,9 +9049,9 @@ class ChatMessageAccumulator { */ prepareStep(): | ((args: { - messages: ModelMessage[]; - steps: CompactionStep[]; - }) => Promise<{ messages: ModelMessage[] } | undefined>) + messages: ModelMessage[]; + steps: CompactionStep[]; + }) => Promise<{ messages: ModelMessage[] } | undefined>) | undefined { if (!this._compaction && !this._pendingMessages) return undefined; const comp = this._compaction; @@ -9166,11 +9140,11 @@ class ChatMessageAccumulator { this.modelMessages = this._compaction.compactModelMessages ? await this._compaction.compactModelMessages(compactEvent) : [ - { - role: "assistant" as const, - content: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], - }, - ]; + { + role: "assistant" as const, + content: [{ type: "text" as const, text: `[Conversation summary]\n\n${summary}` }], + }, + ]; if (this._compaction.compactUIMessages) { this.uiMessages = await this._compaction.compactUIMessages(compactEvent); @@ -9265,9 +9239,9 @@ export type ChatTurn = { */ prepareStep(): | ((args: { - messages: ModelMessage[]; - steps: CompactionStep[]; - }) => Promise<{ messages: ModelMessage[] } | undefined>) + messages: ModelMessage[]; + steps: CompactionStep[]; + }) => Promise<{ messages: ModelMessage[] } | undefined>) | undefined; }; @@ -9355,7 +9329,9 @@ function createChatSession( const signal = await waitForHandover({ payload: currentPayload, idleTimeoutInSeconds: - sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? idleTimeoutInSeconds, + sessionIdleTimeoutOpt ?? + currentPayload.idleTimeoutInSeconds ?? + idleTimeoutInSeconds, timeout, }); if (!signal || signal.kind === "handover-skip" || runSignal.aborted) { @@ -9376,7 +9352,10 @@ function createChatSession( // a turn immediately would invoke the model with no user input. const isMessagelessContinuationBoot = currentPayload.continuation === true && !currentPayload.message; - if (turn === 0 && (currentPayload.trigger === "preload" || isMessagelessContinuationBoot)) { + if ( + turn === 0 && + (currentPayload.trigger === "preload" || isMessagelessContinuationBoot) + ) { const result = await messagesInput.waitWithIdleTimeout({ idleTimeoutInSeconds: sessionIdleTimeoutOpt ?? currentPayload.idleTimeoutInSeconds ?? 30, @@ -9403,10 +9382,7 @@ function createChatSession( // Subsequent turns: wait for the next message if (turn > 0) { // chat.requestUpgrade() / chat.endRun() — exit before waiting - if ( - locals.get(chatUpgradeRequestedKey) || - locals.get(chatEndRunRequestedKey) - ) { + if (locals.get(chatUpgradeRequestedKey) || locals.get(chatEndRunRequestedKey)) { stop.cleanup(); return { done: true, value: undefined }; } @@ -9549,9 +9525,7 @@ function createChatSession( } const response = accumulator.uiMessages.at(-1); if (!response || response.role !== "assistant") { - throw new Error( - "turn.complete() could not find the spliced handover response" - ); + throw new Error("turn.complete() could not find the spliced handover response"); } sessionMsgSub.off(); await chatWriteTurnComplete(); @@ -9667,18 +9641,17 @@ function createChatSession( accumulator.modelMessages = sessionCompaction.compactModelMessages ? await sessionCompaction.compactModelMessages(compactEvent) : [ - { - role: "assistant" as const, - content: [ - { type: "text" as const, text: `[Conversation summary]\n\n${summary}` }, - ], - }, - ]; + { + role: "assistant" as const, + content: [ + { type: "text" as const, text: `[Conversation summary]\n\n${summary}` }, + ], + }, + ]; if (sessionCompaction.compactUIMessages) { - accumulator.uiMessages = await sessionCompaction.compactUIMessages( - compactEvent - ); + accumulator.uiMessages = + await sessionCompaction.compactUIMessages(compactEvent); } } } @@ -9692,7 +9665,10 @@ function createChatSession( // Append any non-transient data parts queued via chat.response or writer.write() const queuedParts = locals.get(chatResponsePartsKey); if (queuedParts && queuedParts.length > 0) { - response = { ...response, parts: [...(response.parts ?? []), ...(queuedParts as UIMessage["parts"])] }; + response = { + ...response, + parts: [...(response.parts ?? []), ...(queuedParts as UIMessage["parts"])], + }; locals.set(chatResponsePartsKey, []); } await accumulator.addResponse(response); @@ -9929,8 +9905,8 @@ function chatLocal>(options: { id: string }): if (current === undefined) { throw new Error( "chat.local can only be modified after initialization. " + - "Call local.init() in onBoot (recommended — fires on every fresh worker including continuation runs) or run() first. " + - "If you previously initialized in onChatStart, move it to onBoot — onChatStart only fires on the chat's very first message and will not run on a continuation." + "Call local.init() in onBoot (recommended — fires on every fresh worker including continuation runs) or run() first. " + + "If you previously initialized in onChatStart, move it to onBoot — onChatStart only fires on the chat's very first message and will not run on a continuation." ); } locals.set(localKey, { ...current, [prop]: value }); @@ -10018,9 +9994,7 @@ export type ChatStartSessionEndpointContext = { chatId: string; }; -export type ChatStartSessionBaseURLResolver = ( - ctx: ChatStartSessionEndpointContext -) => string; +export type ChatStartSessionBaseURLResolver = (ctx: ChatStartSessionEndpointContext) => string; export type ChatStartSessionFetchOverride = ( url: string, @@ -10196,15 +10170,13 @@ function createChatStartSessionAction( ...(options?.triggerConfig?.maxAttempts !== undefined || params.triggerConfig?.maxAttempts !== undefined ? { - maxAttempts: - params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts!, + maxAttempts: params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts!, } : {}), ...(options?.triggerConfig?.maxDuration !== undefined || params.triggerConfig?.maxDuration !== undefined ? { - maxDuration: - params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration!, + maxDuration: params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration!, } : {}), ...(options?.triggerConfig?.region || params.triggerConfig?.region @@ -10283,10 +10255,7 @@ function resolveChatStartBaseURL( option: string | ChatStartSessionBaseURLResolver | undefined ): string { const fallback = apiClientManager.baseURL ?? "https://api.trigger.dev"; - const raw = - typeof option === "function" - ? option({ endpoint, chatId }) - : option ?? fallback; + const raw = typeof option === "function" ? option({ endpoint, chatId }) : (option ?? fallback); return raw.replace(/\/$/, ""); } @@ -10308,7 +10277,13 @@ function overrideRequestHeaders(accessToken: string): Record { async function callSessionsCreateWithOverride(args: { chatId: string; - body: { type: "chat.agent"; externalId: string; taskIdentifier: string; triggerConfig: SessionTriggerConfig; metadata?: Record }; + body: { + type: "chat.agent"; + externalId: string; + taskIdentifier: string; + triggerConfig: SessionTriggerConfig; + metadata?: Record; + }; baseURLOption: string | ChatStartSessionBaseURLResolver | undefined; fetchOverride: ChatStartSessionFetchOverride | undefined; }): Promise<{ id: string; runId: string; publicAccessToken: string }> { @@ -10344,9 +10319,7 @@ async function mintPublicTokenWithOverride(args: { }): Promise { const accessToken = apiClientManager.accessToken; if (!accessToken) { - throw new Error( - "chat.createStartSessionAction: no API access token configured for JWT mint." - ); + throw new Error("chat.createStartSessionAction: no API access token configured for JWT mint."); } const ctx: ChatStartSessionEndpointContext = { endpoint: "auth", chatId: args.chatId }; const url = `${resolveChatStartBaseURL("auth", args.chatId, args.baseURLOption)}/api/v1/auth/jwt/claims`; diff --git a/packages/trigger-sdk/src/v3/chat-client.ts b/packages/trigger-sdk/src/v3/chat-client.ts index 28d015a96fb..9836346fad7 100644 --- a/packages/trigger-sdk/src/v3/chat-client.ts +++ b/packages/trigger-sdk/src/v3/chat-client.ts @@ -43,9 +43,7 @@ export type InferChatClientData = /** Extract the UIMessage type from a chat agent task. */ export type InferChatUIMessage = - T extends Task, any> - ? TUIMessage - : UIMessage; + T extends Task, any> ? TUIMessage : UIMessage; // ─── Types ───────────────────────────────────────────────────────── @@ -100,10 +98,7 @@ export type AgentChatOptions = { * Called when a turn completes. Persist `lastEventId` for stream * resumption across requests. */ - onTurnComplete?: (event: { - chatId: string; - lastEventId?: string; - }) => void | Promise; + onTurnComplete?: (event: { chatId: string; lastEventId?: string }) => void | Promise; /** SSE timeout in seconds. @default 120 */ streamTimeoutSeconds?: number; /** @@ -328,9 +323,10 @@ export class AgentChat { this.onTriggered = options.onTriggered; this.onTurnComplete = options.onTurnComplete; const baseURLOption = options.baseURL; - this.baseURLResolver = typeof baseURLOption === "function" - ? baseURLOption - : () => baseURLOption ?? apiClientManager.baseURL ?? "https://api.trigger.dev"; + this.baseURLResolver = + typeof baseURLOption === "function" + ? baseURLOption + : () => baseURLOption ?? apiClientManager.baseURL ?? "https://api.trigger.dev"; this.fetchOverride = options.fetch; // Hydration: a non-empty `session` means the caller knows the @@ -372,10 +368,7 @@ export class AgentChat { * const text = await stream.text(); * ``` */ - async sendMessage( - text: string, - options?: { abortSignal?: AbortSignal } - ): Promise { + async sendMessage(text: string, options?: { abortSignal?: AbortSignal }): Promise { const msgId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const message: UIMessage = { id: msgId, @@ -389,12 +382,14 @@ export class AgentChat { /** Send raw UIMessage-like objects. Use `sendMessage()` for simple text. */ async sendRaw( - messages: UIMessage[] | Array<{ - id: string; - role: string; - parts?: unknown[]; - [key: string]: unknown; - }>, + messages: + | UIMessage[] + | Array<{ + id: string; + role: string; + parts?: unknown[]; + [key: string]: unknown; + }>, options?: { trigger?: "submit-message" | "regenerate-message"; abortSignal?: AbortSignal; @@ -416,9 +411,7 @@ export class AgentChat { // payload — reasoning blobs, text, and tool `input` come from the // agent's authoritative chain. `regenerate-message` omits `message`. if (triggerType === "submit-message" && messages.length === 0) { - throw new Error( - "AgentChat.sendRaw: 'submit-message' trigger requires at least one message" - ); + throw new Error("AgentChat.sendRaw: 'submit-message' trigger requires at least one message"); } const lastIfSubmit = triggerType === "submit-message" @@ -542,10 +535,7 @@ export class AgentChat { * } * ``` */ - async sendAction( - action: unknown, - options?: { abortSignal?: AbortSignal } - ): Promise { + async sendAction(action: unknown, options?: { abortSignal?: AbortSignal }): Promise { await this.ensureStarted(); const payload: ChatTaskWirePayload = { @@ -587,9 +577,7 @@ export class AgentChat { } /** Reconnect to the response stream (e.g. after a disconnect). */ - async reconnect( - abortSignal?: AbortSignal - ): Promise | null> { + async reconnect(abortSignal?: AbortSignal): Promise | null> { if (!this.state.started) return null; return this.subscribeToSessionStream(abortSignal, { sendStopOnAbort: false }); } @@ -662,15 +650,9 @@ export class AgentChat { chatId: this.chatId, ...(this.clientData ? { metadata: this.clientData } : {}), }, - ...(this.triggerConfigDefault?.machine - ? { machine: this.triggerConfigDefault.machine } - : {}), - ...(this.triggerConfigDefault?.queue - ? { queue: this.triggerConfigDefault.queue } - : {}), - ...(this.triggerConfigDefault?.tags - ? { tags: this.triggerConfigDefault.tags } - : {}), + ...(this.triggerConfigDefault?.machine ? { machine: this.triggerConfigDefault.machine } : {}), + ...(this.triggerConfigDefault?.queue ? { queue: this.triggerConfigDefault.queue } : {}), + ...(this.triggerConfigDefault?.tags ? { tags: this.triggerConfigDefault.tags } : {}), ...(this.triggerConfigDefault?.maxAttempts !== undefined ? { maxAttempts: this.triggerConfigDefault.maxAttempts } : {}), @@ -678,8 +660,7 @@ export class AgentChat { this.triggerConfigDefault?.idleTimeoutInSeconds !== undefined ? { idleTimeoutInSeconds: - options?.idleTimeoutInSeconds ?? - this.triggerConfigDefault?.idleTimeoutInSeconds!, + options?.idleTimeoutInSeconds ?? this.triggerConfigDefault?.idleTimeoutInSeconds!, } : {}), }; @@ -709,7 +690,7 @@ export class AgentChat { const sseCtx: AgentChatEndpointContext = { endpoint: "out", chatId }; const fetchOverride = this.fetchOverride; const sseFetchClient: typeof fetch | undefined = fetchOverride - ? ((input, init) => { + ? (((input, init) => { if (typeof input === "string") { return fetchOverride(input, init ?? {}, sseCtx); } @@ -728,7 +709,7 @@ export class AgentChat { }, sseCtx ); - }) as typeof fetch + }) as typeof fetch) : undefined; const internalAbort = new AbortController(); diff --git a/packages/trigger-sdk/src/v3/chat-react.ts b/packages/trigger-sdk/src/v3/chat-react.ts index 495e235083d..5995667730d 100644 --- a/packages/trigger-sdk/src/v3/chat-react.ts +++ b/packages/trigger-sdk/src/v3/chat-react.ts @@ -174,10 +174,7 @@ export function useMultiTabChat( useEffect(() => { if (!transport.hasClaim(chatId) && latestMessagesRef.current.length > 0) { if (idleRef.current !== null) { - const cancel = - typeof cancelIdleCallback === "function" - ? cancelIdleCallback - : clearTimeout; + const cancel = typeof cancelIdleCallback === "function" ? cancelIdleCallback : clearTimeout; cancel(idleRef.current as any); idleRef.current = null; } diff --git a/packages/trigger-sdk/src/v3/chat-server.test.ts b/packages/trigger-sdk/src/v3/chat-server.test.ts index e6caaa40b02..539fe0247ad 100644 --- a/packages/trigger-sdk/src/v3/chat-server.test.ts +++ b/packages/trigger-sdk/src/v3/chat-server.test.ts @@ -201,8 +201,8 @@ describe("chat.headStart (route handler)", () => { expect(res.headers.get("X-Trigger-Chat-Access-Token")).toBe(SESSION_PAT); expect(res.headers.get("Content-Type")).toMatch(/text\/event-stream/); - const sessionCreate = requests.find((r) => - r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") + const sessionCreate = requests.find( + (r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") ); expect(sessionCreate).toBeDefined(); const body = JSON.parse(sessionCreate!.init!.body as string); @@ -228,10 +228,17 @@ describe("chat.headStart (route handler)", () => { return appendOkResponse(); } if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -262,16 +269,12 @@ describe("chat.headStart (route handler)", () => { ) ); - const sessionCreate = requests.find((r) => - r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") + const sessionCreate = requests.find( + (r) => r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") ); expect(sessionCreate).toBeDefined(); const body = JSON.parse(sessionCreate!.init!.body as string); - expect(body.triggerConfig.tags).toEqual([ - "chat:chat-1", - "org:acme", - "agentic-run:xyz", - ]); + expect(body.triggerConfig.tags).toEqual(["chat:chat-1", "org:acme", "agentic-run:xyz"]); expect(body.triggerConfig.queue).toBe("my-queue"); expect(body.triggerConfig.basePayload.trigger).toBe("handover-prepare"); expect(body.triggerConfig.basePayload.chatId).toBe("chat-1"); @@ -290,10 +293,17 @@ describe("chat.headStart (route handler)", () => { } // Stitched response subscribes to `.out` after handover. if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -332,8 +342,7 @@ describe("chat.headStart (route handler)", () => { const handoverPost = requests.find( (r) => - r.url.includes("/realtime/v1/sessions/chat-final/in/append") && - r.init?.body !== undefined + r.url.includes("/realtime/v1/sessions/chat-final/in/append") && r.init?.body !== undefined ); expect(handoverPost).toBeDefined(); const body = JSON.parse(handoverPost!.init!.body as string); @@ -366,10 +375,17 @@ describe("chat.headStart (route handler)", () => { // closes immediately — this test validates dispatch only, not // the agent-side resume. if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { - return new Response(new ReadableStream({ start(c) { c.close(); } }), { - status: 200, - headers: { "content-type": "text/event-stream" }, - }); + return new Response( + new ReadableStream({ + start(c) { + c.close(); + }, + }), + { + status: 200, + headers: { "content-type": "text/event-stream" }, + } + ); } throw new Error(`Unexpected URL: ${urlStr}`); }); @@ -412,8 +428,7 @@ describe("chat.headStart (route handler)", () => { const handoverPost = requests.find( (r) => - r.url.includes("/realtime/v1/sessions/chat-tool/in/append") && - r.init?.body !== undefined + r.url.includes("/realtime/v1/sessions/chat-tool/in/append") && r.init?.body !== undefined ); expect(handoverPost).toBeDefined(); const body = JSON.parse(handoverPost!.init!.body as string); @@ -430,9 +445,7 @@ describe("chat.headStart (route handler)", () => { (m: { role: string }) => m.role === "assistant" ); expect(assistant).toBeDefined(); - const toolCallPart = assistant.content.find( - (p: { type: string }) => p.type === "tool-call" - ); + const toolCallPart = assistant.content.find((p: { type: string }) => p.type === "tool-call"); expect(toolCallPart).toBeDefined(); const approvalRequestPart = assistant.content.find( (p: { type: string }) => p.type === "tool-approval-request" diff --git a/packages/trigger-sdk/src/v3/chat-server.ts b/packages/trigger-sdk/src/v3/chat-server.ts index a20bc7ac22a..f0585933933 100644 --- a/packages/trigger-sdk/src/v3/chat-server.ts +++ b/packages/trigger-sdk/src/v3/chat-server.ts @@ -145,9 +145,7 @@ export type HeadStartSession = { * fire-and-forget. Returns a passthrough that the caller can use as * the HTTP response body. */ - tee( - stream: ReadableStream - ): ReadableStream; + tee(stream: ReadableStream): ReadableStream; /** * Awaits `result.finishReason` and dispatches `handover` (with the * partial assistant ModelMessages) or `handover-skip`. @@ -593,9 +591,9 @@ async function openHandoverSession(opts: { idleTimeoutInSeconds * 1000 ); - const buildStreamTextOptions = ( - spreadOpts?: { tools?: Record } - ): Record => { + const buildStreamTextOptions = (spreadOpts?: { + tools?: Record; + }): Record => { // The customer spreads this object into their `streamText` call // and then adds `model`, `system`, etc. on top. We set the four // keys handover correctness depends on: @@ -850,9 +848,7 @@ async function openHandoverSession(opts: { chatId, "out", { - ...(customerLastEventId != null - ? { lastEventId: customerLastEventId } - : {}), + ...(customerLastEventId != null ? { lastEventId: customerLastEventId } : {}), signal: AbortSignal.any([abortController.signal, subscriptionAbort.signal]), onPart: (part) => { if (part.id) latestEventId = part.id; @@ -1083,7 +1079,8 @@ function toNodeListener( res.setHeader(key, value); }); const setCookies = - typeof (webRes.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === "function" + typeof (webRes.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === + "function" ? (webRes.headers as Headers & { getSetCookie: () => string[] }).getSetCookie() : []; if (setCookies.length > 0) { diff --git a/packages/trigger-sdk/src/v3/chat.test.ts b/packages/trigger-sdk/src/v3/chat.test.ts index e4fc45eae4b..50bfb5e4095 100644 --- a/packages/trigger-sdk/src/v3/chat.test.ts +++ b/packages/trigger-sdk/src/v3/chat.test.ts @@ -173,22 +173,17 @@ function defaultSseResponse( } function authError(status = 401): Response { - return new Response( - JSON.stringify({ error: "Unauthorized", name: "TriggerApiError", status }), - { - status, - headers: { "content-type": "application/json" }, - } - ); + return new Response(JSON.stringify({ error: "Unauthorized", name: "TriggerApiError", status }), { + status, + headers: { "content-type": "application/json" }, + }); } /** * Drains a UIMessageChunk stream into an array. Used to assert what * the transport surfaced after filtering control chunks. */ -async function drainChunks( - stream: ReadableStream -): Promise { +async function drainChunks(stream: ReadableStream): Promise { const reader = stream.getReader(); const out: UIMessageChunk[] = []; try { @@ -331,9 +326,7 @@ describe("TriggerChatTransport", () => { }); it("is idempotent — second call returns the cached state without re-invoking startSession", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-2" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-2" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -369,9 +362,7 @@ describe("TriggerChatTransport", () => { }); it("preload() is an alias for start()", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-pre" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-pre" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -393,9 +384,7 @@ describe("TriggerChatTransport", () => { }); it("threads the transport's `clientData` through to startSession", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-cd" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-cd" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -414,9 +403,7 @@ describe("TriggerChatTransport", () => { }); it("setClientData updates the value passed to subsequent startSession calls", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "session-pat-set" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "session-pat-set" }); const transport = new TriggerChatTransport({ task: "my-chat-task", @@ -438,9 +425,7 @@ describe("TriggerChatTransport", () => { describe("ensureSessionState (lazy start on first sendMessage)", () => { it("calls startSession lazily on first sendMessage when no PAT is hydrated", async () => { - const startSession = vi - .fn() - .mockResolvedValue({ publicAccessToken: "lazy-session-pat" }); + const startSession = vi.fn().mockResolvedValue({ publicAccessToken: "lazy-session-pat" }); global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { const urlStr = typeof url === "string" ? url : url.toString(); @@ -565,8 +550,8 @@ describe("TriggerChatTransport", () => { expect(chunks).toHaveLength(sampleChunks.length); expect(chunks[0]).toEqual(sampleChunks[0]); - const append = requests.find((r) => - isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append") + const append = requests.find( + (r) => isSessionStreamAppendUrl(r.url) && r.url.endsWith("/in/append") ); expect(append).toBeDefined(); expect(chatIdFromUrl(append!.url)).toBe("chat-1"); @@ -620,9 +605,7 @@ describe("TriggerChatTransport", () => { }); const baseURLFn = vi.fn(({ endpoint }: { endpoint: "in" | "out"; chatId: string }) => - endpoint === "out" - ? "https://stream.example.com" - : "https://api.example.com" + endpoint === "out" ? "https://stream.example.com" : "https://api.example.com" ); const transport = new TriggerChatTransport({ @@ -658,11 +641,7 @@ describe("TriggerChatTransport", () => { const fetchCalls: Array<{ url: string; endpoint: string; chatId: string }> = []; const customFetch = vi.fn( - async ( - url: string, - init: RequestInit, - ctx: { endpoint: "in" | "out"; chatId: string } - ) => { + async (url: string, init: RequestInit, ctx: { endpoint: "in" | "out"; chatId: string }) => { fetchCalls.push({ url, endpoint: ctx.endpoint, chatId: ctx.chatId }); if (isSessionStreamAppendUrl(url)) return defaultAppendResponse(); if (isSessionOutSubscribeUrl(url)) return defaultSseResponse(); @@ -1194,9 +1173,7 @@ describe("TriggerChatTransport", () => { // Only the endpoint was called — no /api/v1/sessions, no .in/append, // no .out subscribe. The handler owns first-turn end-to-end. - const endpointPosts = requests.filter( - (r) => r.url === "https://my-app.example/api/chat" - ); + const endpointPosts = requests.filter((r) => r.url === "https://my-app.example/api/chat"); expect(endpointPosts).toHaveLength(1); expect(requests.some((r) => isSessionCreateUrl(r.url))).toBe(false); expect(requests.some((r) => isSessionStreamAppendUrl(r.url))).toBe(false); diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index e0d37ad7d47..d7a71ee8421 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -462,9 +462,10 @@ export class TriggerChatTransport implements ChatTransport { | undefined; const baseURLOption = options.baseURL ?? DEFAULT_BASE_URL; const streamOverride = options.streamBaseURL; - this.resolveBaseURLFn = typeof baseURLOption === "function" - ? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx)) - : (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption); + this.resolveBaseURLFn = + typeof baseURLOption === "function" + ? (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption(ctx)) + : (ctx) => (ctx.endpoint === "out" && streamOverride ? streamOverride : baseURLOption); this.fetchOverride = options.fetch; this.extraHeaders = options.headers ?? {}; this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS; @@ -687,9 +688,7 @@ export class TriggerChatTransport implements ChatTransport { }); if (!response.ok) { - throw new Error( - `chat.handover endpoint returned ${response.status} ${response.statusText}` - ); + throw new Error(`chat.handover endpoint returned ${response.status} ${response.statusText}`); } if (!response.body) { throw new Error("chat.handover endpoint returned no response body"); @@ -906,10 +905,7 @@ export class TriggerChatTransport implements ChatTransport { * `StreamTextResult`); for `void`-returning side-effect-only actions * the stream completes immediately with `trigger:turn-complete`. */ - sendAction = async ( - chatId: string, - action: unknown - ): Promise> => { + sendAction = async (chatId: string, action: unknown): Promise> => { if (this.coordinator) { if (this.coordinator.isReadOnly(chatId)) { throw new Error("This chat is active in another tab"); @@ -1320,7 +1316,7 @@ export class TriggerChatTransport implements ChatTransport { const sseCtx: ChatTransportEndpointContext = { endpoint: "out", chatId }; const fetchOverride = this.fetchOverride; const sseFetchClient: typeof fetch | undefined = fetchOverride - ? ((input, init) => { + ? (((input, init) => { if (typeof input === "string") { return fetchOverride(input, init ?? {}, sseCtx); } @@ -1340,7 +1336,7 @@ export class TriggerChatTransport implements ChatTransport { }, sseCtx ); - }) as typeof fetch + }) as typeof fetch) : undefined; const connectSseOnce = async (token: string) => { const subscription = new SSEStreamSubscription(streamUrl, { @@ -1448,9 +1444,7 @@ export class TriggerChatTransport implements ChatTransport { // when no header is present so the deploy-skew window // closes turns correctly. let controlValue = controlSubtype(value.headers); - let legacyChunk: - | { type?: string; publicAccessToken?: string } - | undefined; + let legacyChunk: { type?: string; publicAccessToken?: string } | undefined; if (!controlValue && value.chunk && typeof value.chunk === "object") { const chunk = value.chunk as { type?: unknown; publicAccessToken?: unknown }; if (chunk.type === "trigger:turn-complete") { diff --git a/packages/trigger-sdk/src/v3/idempotencyKeys.ts b/packages/trigger-sdk/src/v3/idempotencyKeys.ts index 0030dbf3aaf..1b0e31ec3a7 100644 --- a/packages/trigger-sdk/src/v3/idempotencyKeys.ts +++ b/packages/trigger-sdk/src/v3/idempotencyKeys.ts @@ -1,4 +1,8 @@ -import { createIdempotencyKey, resetIdempotencyKey, type IdempotencyKey } from "@trigger.dev/core/v3"; +import { + createIdempotencyKey, + resetIdempotencyKey, + type IdempotencyKey, +} from "@trigger.dev/core/v3"; export const idempotencyKeys = { create: createIdempotencyKey, diff --git a/packages/trigger-sdk/src/v3/prompt.ts b/packages/trigger-sdk/src/v3/prompt.ts index 1c465b1ee46..2b5892a5f85 100644 --- a/packages/trigger-sdk/src/v3/prompt.ts +++ b/packages/trigger-sdk/src/v3/prompt.ts @@ -55,34 +55,28 @@ export type PromptHandle< export type AnyPromptHandle = PromptHandle; /** Extract the identifier (id literal type) from a PromptHandle */ -export type PromptIdentifier = T extends PromptHandle - ? TId - : string; +export type PromptIdentifier = + T extends PromptHandle ? TId : string; /** Extract the variables input type from a PromptHandle */ -export type PromptVariables = T extends PromptHandle - ? inferSchemaIn - : Record; +export type PromptVariables = + T extends PromptHandle + ? inferSchemaIn + : Record; /** * Compile a Mustache-style template by substituting `{{variable}}` placeholders. */ -function compileTemplate( - template: string, - variables: Record -): string { +function compileTemplate(template: string, variables: Record): string { // Handle conditional sections: {{#key}}...{{/key}} - let result = template.replace( - /\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, - (_match, key, content) => { - const value = variables[key]; - return value - ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { - return String(variables[k] ?? ""); - }) - : ""; - } - ); + let result = template.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_match, key, content) => { + const value = variables[key]; + return value + ? content.replace(/\{\{(\w+)\}\}/g, (_m: string, k: string) => { + return String(variables[k] ?? ""); + }) + : ""; + }); // Handle simple substitutions: {{key}} result = result.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => { @@ -124,7 +118,14 @@ function resolveLocally( variables: Record ): ResolvedPrompt { const inputJson = Object.keys(variables).length > 0 ? JSON.stringify(variables) : undefined; - const telemetryFn = makeToAISDKTelemetry(options.id, options.id, 0, ["local"], options.model, inputJson); + const telemetryFn = makeToAISDKTelemetry( + options.id, + options.id, + 0, + ["local"], + options.model, + inputJson + ); return { promptId: options.id, @@ -140,12 +141,8 @@ function resolveLocally( export function definePrompt< TIdentifier extends string, TVariables extends TaskSchema | undefined = undefined, ->( - options: PromptOptions -): PromptHandle { - const parseVariables = options.variables - ? getSchemaParseFn(options.variables) - : undefined; +>(options: PromptOptions): PromptHandle { + const parseVariables = options.variables ? getSchemaParseFn(options.variables) : undefined; // Register with resource catalog const metadata: PromptMetadataWithFunctions = { @@ -170,9 +167,7 @@ export function definePrompt< id: options.id, resolve: async (variables, resolveOptions) => { // Validate variables if schema provided - const validated = parseVariables - ? await parseVariables(variables) - : variables; + const validated = parseVariables ? await parseVariables(variables) : variables; const vars = validated as Record; const apiClient = apiClientManager.client; diff --git a/packages/trigger-sdk/src/v3/promptManagement.ts b/packages/trigger-sdk/src/v3/promptManagement.ts index 480676f445e..b9876d70e44 100644 --- a/packages/trigger-sdk/src/v3/promptManagement.ts +++ b/packages/trigger-sdk/src/v3/promptManagement.ts @@ -10,7 +10,12 @@ import { type PromptOverrideCreatedResponseBody, type UpdatePromptOverrideRequestBody, } from "@trigger.dev/core/v3"; -import type { AnyPromptHandle, PromptIdentifier, PromptVariables, ResolvedPrompt } from "./prompt.js"; +import type { + AnyPromptHandle, + PromptIdentifier, + PromptVariables, + ResolvedPrompt, +} from "./prompt.js"; import { tracer } from "./tracer.js"; function promptSpanOptions(name: string, slug: string) { @@ -119,9 +124,7 @@ export async function resolvePrompt { +export function listPrompts(requestOptions?: ApiRequestOptions): Promise { const apiClient = apiClientManager.clientOrThrow(); return apiClient.listPrompts({ tracer, @@ -158,14 +161,18 @@ export async function promotePromptVersion( requestOptions?: ApiRequestOptions ): Promise { const apiClient = apiClientManager.clientOrThrow(); - return apiClient.promotePromptVersion(slug, { version }, { - ...promptSpanOptions("prompts.promote()", slug), - attributes: { - ...promptSpanOptions("prompts.promote()", slug).attributes, - "prompt.slug": slug, - "prompt.version": version, - }, - }); + return apiClient.promotePromptVersion( + slug, + { version }, + { + ...promptSpanOptions("prompts.promote()", slug), + attributes: { + ...promptSpanOptions("prompts.promote()", slug).attributes, + "prompt.slug": slug, + "prompt.version": version, + }, + } + ); } /** Create an override — a dashboard/API edit that takes priority over the deployed version. */ @@ -226,12 +233,16 @@ export async function reactivatePromptOverride( requestOptions?: ApiRequestOptions ): Promise { const apiClient = apiClientManager.clientOrThrow(); - return apiClient.reactivatePromptOverride(slug, { version }, { - ...promptSpanOptions("prompts.reactivateOverride()", slug), - attributes: { - ...promptSpanOptions("prompts.reactivateOverride()", slug).attributes, - "prompt.slug": slug, - "prompt.version": version, - }, - }); + return apiClient.reactivatePromptOverride( + slug, + { version }, + { + ...promptSpanOptions("prompts.reactivateOverride()", slug), + attributes: { + ...promptSpanOptions("prompts.reactivateOverride()", slug).attributes, + "prompt.slug": slug, + "prompt.version": version, + }, + } + ); } diff --git a/packages/trigger-sdk/src/v3/retry.ts b/packages/trigger-sdk/src/v3/retry.ts index 65e3203f37d..c70794c21f7 100644 --- a/packages/trigger-sdk/src/v3/retry.ts +++ b/packages/trigger-sdk/src/v3/retry.ts @@ -164,12 +164,9 @@ async function retryFetch( const abortController = new AbortController(); const timeoutId = init?.timeoutInMs - ? setTimeout( - () => { - abortController.abort(); - }, - init?.timeoutInMs - ) + ? setTimeout(() => { + abortController.abort(); + }, init?.timeoutInMs) : undefined; init?.signal?.addEventListener("abort", () => { diff --git a/packages/trigger-sdk/src/v3/runs.ts b/packages/trigger-sdk/src/v3/runs.ts index 88e6d2b701c..db949c876bb 100644 --- a/packages/trigger-sdk/src/v3/runs.ts +++ b/packages/trigger-sdk/src/v3/runs.ts @@ -160,10 +160,10 @@ function listRunsRequestOptions( type RunId = TRunId extends AnyRunHandle | AnyBatchedRunHandle ? TRunId : TRunId extends AnyTask - ? string - : TRunId extends string - ? TRunId - : never; + ? string + : TRunId extends string + ? TRunId + : never; function retrieveRun( runId: RunId, diff --git a/packages/trigger-sdk/src/v3/schedules/index.ts b/packages/trigger-sdk/src/v3/schedules/index.ts index 1f87ed83e27..cd7bd691417 100644 --- a/packages/trigger-sdk/src/v3/schedules/index.ts +++ b/packages/trigger-sdk/src/v3/schedules/index.ts @@ -46,13 +46,13 @@ export type ScheduleOptions< timezone?: string; /** You can optionally specify which environments this schedule should run in. * When not specified, the schedule will run in all environments. - * + * * @example * ```ts * environments: ["PRODUCTION", "STAGING"] * ``` - * - * @example + * + * @example * ```ts * environments: ["PRODUCTION"] // Only run in production * ``` diff --git a/packages/trigger-sdk/src/v3/schemas.ts b/packages/trigger-sdk/src/v3/schemas.ts index 65be53024e5..44898a17c29 100644 --- a/packages/trigger-sdk/src/v3/schemas.ts +++ b/packages/trigger-sdk/src/v3/schemas.ts @@ -1,2 +1,2 @@ // Re-export JSON Schema types for user convenience -export type { JSONSchema } from "@trigger.dev/core/v3"; \ No newline at end of file +export type { JSONSchema } from "@trigger.dev/core/v3"; diff --git a/packages/trigger-sdk/src/v3/sessions.ts b/packages/trigger-sdk/src/v3/sessions.ts index d2a33c09fed..1a7c7716c95 100644 --- a/packages/trigger-sdk/src/v3/sessions.ts +++ b/packages/trigger-sdk/src/v3/sessions.ts @@ -409,16 +409,13 @@ export class SessionOutputChannel { * shared {@link SSEStreamSubscription} plumbing used by run-scoped * realtime streams. */ - async read( - options?: SessionSubscribeOptions - ): Promise> { + async read(options?: SessionSubscribeOptions): Promise> { const apiClient = apiClientManager.clientOrThrow(); return apiClient.subscribeToSessionStream(this.sessionId, "out", { signal: options?.signal, timeoutInSeconds: options?.timeoutInSeconds, - lastEventId: - options?.lastEventId != null ? String(options.lastEventId) : undefined, + lastEventId: options?.lastEventId != null ? String(options.lastEventId) : undefined, onPart: options?.onPart, onControl: options?.onControl, onComplete: options?.onComplete, @@ -827,9 +824,7 @@ export class SessionInputChannel { span.setAttribute("wait.resolved", "skipped"); return { ok: false as const, - error: new WaitpointTimeoutError( - "Idle timeout elapsed and skipSuspend is set" - ), + error: new WaitpointTimeoutError("Idle timeout elapsed and skipSuspend is set"), }; } diff --git a/packages/trigger-sdk/src/v3/shared.test.ts b/packages/trigger-sdk/src/v3/shared.test.ts index 4a0509b6b3b..16191e9826b 100644 --- a/packages/trigger-sdk/src/v3/shared.test.ts +++ b/packages/trigger-sdk/src/v3/shared.test.ts @@ -216,4 +216,3 @@ describe("readableStreamToAsyncIterable", () => { expect(producedValues.length).toBeLessThanOrEqual(3); }); }); - diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index fba990949b3..592a4b62dfe 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -1038,7 +1038,10 @@ export async function batchTriggerByIdAndWait( ctx, }); - const runs = await handleBatchTaskRunExecutionResultV2(result.items, response.taskIdentifiers); + const runs = await handleBatchTaskRunExecutionResultV2( + result.items, + response.taskIdentifiers + ); return { id: result.id, @@ -1082,7 +1085,10 @@ export async function batchTriggerByIdAndWait( ctx, }); - const runs = await handleBatchTaskRunExecutionResultV2(result.items, response.taskIdentifiers); + const runs = await handleBatchTaskRunExecutionResultV2( + result.items, + response.taskIdentifiers + ); return { id: result.id, @@ -1559,7 +1565,10 @@ export async function batchTriggerAndWaitTasks( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2181,8 +2193,8 @@ async function* transformSingleTaskBatchItemsStreamForWait( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2314,8 +2326,8 @@ async function batchTrigger_internal( queue: item.options?.queue ? { name: item.options.queue } : queue - ? { name: queue } - : undefined, + ? { name: queue } + : undefined, concurrencyKey: item.options?.concurrencyKey, test: taskContext.ctx?.run.isTest, payloadType: payloadPacket.dataType, @@ -2736,8 +2748,8 @@ async function batchTriggerAndWait_internal = { export type AnySkillHandle = SkillHandle; /** Extract the id literal type from a SkillHandle. */ -export type SkillIdentifier = T extends SkillHandle - ? TId - : string; +export type SkillIdentifier = + T extends SkillHandle ? TId : string; /** * Bundled skills are copied to `${cwd}/.trigger/skills/{id}/` by the CLI at diff --git a/packages/trigger-sdk/src/v3/streams.test.ts b/packages/trigger-sdk/src/v3/streams.test.ts index 67ee00fd261..5c22330f24d 100644 --- a/packages/trigger-sdk/src/v3/streams.test.ts +++ b/packages/trigger-sdk/src/v3/streams.test.ts @@ -3,62 +3,62 @@ import { streams } from "./streams.js"; import { taskContext, realtimeStreams } from "@trigger.dev/core/v3"; vi.mock("@trigger.dev/core/v3", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - taskContext: { - ctx: { - run: { - id: "run_123", - // parentTaskRunId and rootTaskRunId are undefined for root tasks - }, - }, + const original = await importOriginal(); + return { + ...original, + taskContext: { + ctx: { + run: { + id: "run_123", + // parentTaskRunId and rootTaskRunId are undefined for root tasks }, - realtimeStreams: { - pipe: vi.fn().mockReturnValue({ - wait: () => Promise.resolve(), - stream: new ReadableStream(), - }), - }, - }; + }, + }, + realtimeStreams: { + pipe: vi.fn().mockReturnValue({ + wait: () => Promise.resolve(), + stream: new ReadableStream(), + }), + }, + }; }); describe("streams.pipe consistency", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); + beforeEach(() => { + vi.clearAllMocks(); + }); - it("should not throw and should use self runId when target is 'root' in a root task", async () => { - const mockStream = new ReadableStream(); + it("should not throw and should use self runId when target is 'root' in a root task", async () => { + const mockStream = new ReadableStream(); - // This should not throw anymore - const { waitUntilComplete } = streams.pipe("test-key", mockStream, { - target: "root", - }); - - expect(realtimeStreams.pipe).toHaveBeenCalledWith( - "test-key", - mockStream, - expect.objectContaining({ - target: "run_123", - }) - ); + // This should not throw anymore + const { waitUntilComplete } = streams.pipe("test-key", mockStream, { + target: "root", }); - it("should not throw and should use self runId when target is 'parent' in a root task", async () => { - const mockStream = new ReadableStream(); + expect(realtimeStreams.pipe).toHaveBeenCalledWith( + "test-key", + mockStream, + expect.objectContaining({ + target: "run_123", + }) + ); + }); - // This should not throw anymore - const { waitUntilComplete } = streams.pipe("test-key", mockStream, { - target: "parent", - }); + it("should not throw and should use self runId when target is 'parent' in a root task", async () => { + const mockStream = new ReadableStream(); - expect(realtimeStreams.pipe).toHaveBeenCalledWith( - "test-key", - mockStream, - expect.objectContaining({ - target: "run_123", - }) - ); + // This should not throw anymore + const { waitUntilComplete } = streams.pipe("test-key", mockStream, { + target: "parent", }); + + expect(realtimeStreams.pipe).toHaveBeenCalledWith( + "test-key", + mockStream, + expect.objectContaining({ + target: "run_123", + }) + ); + }); }); diff --git a/packages/trigger-sdk/src/v3/streams.ts b/packages/trigger-sdk/src/v3/streams.ts index f987872d80a..57d08e80551 100644 --- a/packages/trigger-sdk/src/v3/streams.ts +++ b/packages/trigger-sdk/src/v3/streams.ts @@ -751,10 +751,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { return { id: opts.id, on(handler) { - return inputStreams.on( - opts.id, - handler as (data: unknown) => void | Promise - ); + return inputStreams.on(opts.id, handler as (data: unknown) => void | Promise); }, once(options) { const ctx = taskContext.ctx; @@ -774,9 +771,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { attributes: { [SemanticInternalAttributes.STYLE_ICON]: "streams", [SemanticInternalAttributes.ENTITY_TYPE]: "input-stream", - ...(runId - ? { [SemanticInternalAttributes.ENTITY_ID]: `${runId}:${opts.id}` } - : {}), + ...(runId ? { [SemanticInternalAttributes.ENTITY_ID]: `${runId}:${opts.id}` } : {}), streamId: opts.id, ...accessoryAttributes({ items: [{ text: opts.id, variant: "normal" }], @@ -815,7 +810,6 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { const result = await tracer.startActiveSpan( options?.spanName ?? `inputStream.wait()`, async (span) => { - // 1. Block the run on the waitpoint const waitResponse = await apiClient.waitForWaitpointToken({ runFriendlyId: ctx.run.id, @@ -839,12 +833,12 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { const data = waitResult.output !== undefined ? await conditionallyImportAndParsePacket( - { - data: waitResult.output, - dataType: waitResult.outputType ?? "application/json", - }, - apiClient - ) + { + data: waitResult.output, + dataType: waitResult.outputType ?? "application/json", + }, + apiClient + ) : undefined; if (waitResult.ok) { @@ -916,9 +910,7 @@ function input(opts: { id: string }): RealtimeDefinedInputStream { span.setAttribute("wait.resolved", "skipped"); return { ok: false as const, - error: new WaitpointTimeoutError( - "Idle timeout elapsed and skipSuspend is set" - ), + error: new WaitpointTimeoutError("Idle timeout elapsed and skipSuspend is set"), }; } diff --git a/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts b/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts index 70eae4696d9..a4cbe7b33a0 100644 --- a/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts +++ b/packages/trigger-sdk/src/v3/test/mock-chat-agent.ts @@ -1,14 +1,8 @@ import type { UIMessage, UIMessageChunk } from "ai"; import { resourceCatalog } from "@trigger.dev/core/v3"; import type { LocalsKey } from "@trigger.dev/core/v3"; -import { - runInMockTaskContext, - type MockTaskContextOptions, -} from "@trigger.dev/core/v3/test"; -import { - __setSessionOpenImplForTests, - __setSessionStartImplForTests, -} from "../sessions.js"; +import { runInMockTaskContext, type MockTaskContextOptions } from "@trigger.dev/core/v3/test"; +import { __setSessionOpenImplForTests, __setSessionStartImplForTests } from "../sessions.js"; import { __setReadChatSnapshotImplForTests, __setReplaySessionInTailImplForTests, @@ -16,10 +10,7 @@ import { __setWriteChatSnapshotImplForTests, type ChatSnapshotV1, } from "../ai.js"; -import { - createTestSessionHandle, - type TestSessionOutState, -} from "./test-session-handle.js"; +import { createTestSessionHandle, type TestSessionOutState } from "./test-session-handle.js"; /** Pre-seed locals before the agent's `run()` starts. */ export type SetupLocals = (locals: { @@ -287,10 +278,7 @@ export type MockChatAgentHarness = { readonly allRawChunks: unknown[]; }; -const CONTROL_CHUNK_TYPES = new Set([ - "trigger:turn-complete", - "trigger:upgrade-required", -]); +const CONTROL_CHUNK_TYPES = new Set(["trigger:turn-complete", "trigger:upgrade-required"]); function isControlChunk(chunk: unknown): boolean { if (typeof chunk !== "object" || chunk === null) return false; @@ -407,9 +395,11 @@ export function mockChatAgent( __setReadChatSnapshotImplForTests((_id: string) => { return seededSnapshot as ChatSnapshotV1 | undefined; }); - __setWriteChatSnapshotImplForTests((_id: string, snapshot: ChatSnapshotV1) => { - lastWrittenSnapshot = snapshot as ChatSnapshotV1; - }); + __setWriteChatSnapshotImplForTests( + (_id: string, snapshot: ChatSnapshotV1) => { + lastWrittenSnapshot = snapshot as ChatSnapshotV1; + } + ); // Replay override: install a default that returns whatever // `seededReplayChunks` reduces to. `mockChatAgent` doesn't model the @@ -487,83 +477,80 @@ export function mockChatAgent( }; }); - taskFinished = runInMockTaskContext( - async (drivers) => { - runSignal = new AbortController(); - - // For `mode: "continuation"`, omit `trigger` from the wire payload — - // mirrors what the server's `ensureRunForSession` / `swapSessionRun` - // produces (the continuation overrides clear `trigger` so the SDK - // boot path falls into the continuation-wait branch instead of - // re-firing the basePayload's stale first-run trigger). `continuation: - // true` is set unconditionally for this mode so the boot path's - // continuation-wait condition matches. - const isContinuationMode = mode === "continuation"; - const initialPayload: ChatWirePayload = { - chatId, - ...(isContinuationMode - ? { trigger: undefined as never, continuation: true } - : { trigger: mode }), - metadata: clientData, - ...(!isContinuationMode && options.continuation ? { continuation: true } : {}), - ...(options.previousRunId ? { previousRunId: options.previousRunId } : {}), - ...(options.headStartMessages ? { headStartMessages: options.headStartMessages } : {}), - }; - - sendSessionInput = drivers.sessions.in.send; - closeSessionInput = drivers.sessions.in.close; - - // Record every chunk written to session.out, detect turn-complete. - const listener = (chunk: unknown) => { - allRawChunks.push(chunk); - if (!isControlChunk(chunk)) { - allChunks.push(chunk as UIMessageChunk); - } - if ( - typeof chunk === "object" && - chunk !== null && - (chunk as { type?: string }).type === "trigger:turn-complete" - ) { - const resolvers = turnCompleteResolvers; - turnCompleteResolvers = []; - for (const resolve of resolvers) resolve(); - } - }; - sessionOutState.listeners.add(listener); - const unsubscribe = () => sessionOutState.listeners.delete(listener); - - if (options.setupLocals) { - await options.setupLocals({ set: drivers.locals.set }); - } + taskFinished = runInMockTaskContext(async (drivers) => { + runSignal = new AbortController(); + + // For `mode: "continuation"`, omit `trigger` from the wire payload — + // mirrors what the server's `ensureRunForSession` / `swapSessionRun` + // produces (the continuation overrides clear `trigger` so the SDK + // boot path falls into the continuation-wait branch instead of + // re-firing the basePayload's stale first-run trigger). `continuation: + // true` is set unconditionally for this mode so the boot path's + // continuation-wait condition matches. + const isContinuationMode = mode === "continuation"; + const initialPayload: ChatWirePayload = { + chatId, + ...(isContinuationMode + ? { trigger: undefined as never, continuation: true } + : { trigger: mode }), + metadata: clientData, + ...(!isContinuationMode && options.continuation ? { continuation: true } : {}), + ...(options.previousRunId ? { previousRunId: options.previousRunId } : {}), + ...(options.headStartMessages ? { headStartMessages: options.headStartMessages } : {}), + }; - harnessReadyResolve(); + sendSessionInput = drivers.sessions.in.send; + closeSessionInput = drivers.sessions.in.close; - try { - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] Starting runFn with payload:", initialPayload); - } - await runFn(initialPayload, { - ctx: drivers.ctx, - signal: runSignal.signal, - }); - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] runFn returned"); - } - } catch (err) { - if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { - console.log("[mockChatAgent] runFn threw:", err); - } - throw err; - } finally { - unsubscribe(); - // Resolve any outstanding turn-complete waiters so callers don't hang + // Record every chunk written to session.out, detect turn-complete. + const listener = (chunk: unknown) => { + allRawChunks.push(chunk); + if (!isControlChunk(chunk)) { + allChunks.push(chunk as UIMessageChunk); + } + if ( + typeof chunk === "object" && + chunk !== null && + (chunk as { type?: string }).type === "trigger:turn-complete" + ) { const resolvers = turnCompleteResolvers; turnCompleteResolvers = []; for (const resolve of resolvers) resolve(); } - }, - options.taskContext - ) + }; + sessionOutState.listeners.add(listener); + const unsubscribe = () => sessionOutState.listeners.delete(listener); + + if (options.setupLocals) { + await options.setupLocals({ set: drivers.locals.set }); + } + + harnessReadyResolve(); + + try { + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] Starting runFn with payload:", initialPayload); + } + await runFn(initialPayload, { + ctx: drivers.ctx, + signal: runSignal.signal, + }); + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] runFn returned"); + } + } catch (err) { + if (process.env.TRIGGER_CHAT_TEST_DEBUG === "1") { + console.log("[mockChatAgent] runFn threw:", err); + } + throw err; + } finally { + unsubscribe(); + // Resolve any outstanding turn-complete waiters so callers don't hang + const resolvers = turnCompleteResolvers; + turnCompleteResolvers = []; + for (const resolve of resolvers) resolve(); + } + }, options.taskContext) .catch((err) => { // Propagate errors to pending turn waiters instead of dropping them const resolvers = turnCompleteResolvers; @@ -581,18 +568,14 @@ export function mockChatAgent( __setReplaySessionInTailImplForTests(undefined); }); - const sendPayloadAndWait = async ( - payload: ChatWirePayload - ): Promise => { + const sendPayloadAndWait = async (payload: ChatWirePayload): Promise => { await harnessReady; const before = allRawChunks.length; const turnComplete = waitForTurnComplete(); await sendSessionInput(sessionId, { kind: "message", payload }); await turnComplete; const rawChunks = allRawChunks.slice(before); - const chunks = rawChunks.filter( - (c) => !isControlChunk(c) - ) as UIMessageChunk[]; + const chunks = rawChunks.filter((c) => !isControlChunk(c)) as UIMessageChunk[]; return { chunks, rawChunks }; }; @@ -745,7 +728,9 @@ export function mockChatAgent( async function reduceChunksToMessages(chunks: UIMessageChunk[]): Promise { if (chunks.length === 0) return []; const aiModule = (await import("ai")) as { - readUIMessageStream?: (args: { stream: ReadableStream }) => AsyncIterable; + readUIMessageStream?: (args: { + stream: ReadableStream; + }) => AsyncIterable; cleanupAbortedParts?: (msg: UIMessage) => UIMessage; }; const readUIMessageStream = aiModule.readUIMessageStream; diff --git a/packages/trigger-sdk/src/v3/test/test-session-handle.ts b/packages/trigger-sdk/src/v3/test/test-session-handle.ts index 93d6146ddd5..3e36f402a8c 100644 --- a/packages/trigger-sdk/src/v3/test/test-session-handle.ts +++ b/packages/trigger-sdk/src/v3/test/test-session-handle.ts @@ -27,7 +27,10 @@ import { * network call. */ class TestSessionInputChannel extends SessionInputChannel { - constructor(sessionId: string, private readonly getAbortSignal: () => AbortSignal | undefined) { + constructor( + sessionId: string, + private readonly getAbortSignal: () => AbortSignal | undefined + ) { super(sessionId); } @@ -35,26 +38,28 @@ class TestSessionInputChannel extends SessionInputChannel { // continue to flow through the real `sessionStreams` global, which // the mock task context installs as a `TestSessionStreamManager`. wait(): ManualWaitpointPromise { - return new ManualWaitpointPromise((resolve: (value: { ok: false; error: Error }) => void) => { - const signal = this.getAbortSignal(); - if (!signal) { - // Harness hasn't wired up its run signal yet — nothing to abort - // on. Stay pending; the run loop should never reach this state - // in practice but we don't want to throw here either. - return; - } - const onAbort = () => { - resolve({ - ok: false, - error: new Error("session.in.wait() aborted by test harness"), - }); - }; - if (signal.aborted) { - onAbort(); - return; + return new ManualWaitpointPromise( + (resolve: (value: { ok: false; error: Error }) => void) => { + const signal = this.getAbortSignal(); + if (!signal) { + // Harness hasn't wired up its run signal yet — nothing to abort + // on. Stay pending; the run loop should never reach this state + // in practice but we don't want to throw here either. + return; + } + const onAbort = () => { + resolve({ + ok: false, + error: new Error("session.in.wait() aborted by test harness"), + }); + }; + if (signal.aborted) { + onAbort(); + return; + } + signal.addEventListener("abort", onAbort, { once: true }); } - signal.addEventListener("abort", onAbort, { once: true }); - }); + ); } } @@ -198,9 +203,7 @@ export class TestSessionOutputChannel extends SessionOutputChannel { notify(state, part); }, merge(streamArg) { - ongoing.push( - drainInto(streamArg, state).catch(() => {}) - ); + ongoing.push(drainInto(streamArg, state).catch(() => {})); }, }); diff --git a/packages/trigger-sdk/src/v3/triggerClient.test.ts b/packages/trigger-sdk/src/v3/triggerClient.test.ts index f5374171ed7..a19502a08f4 100644 --- a/packages/trigger-sdk/src/v3/triggerClient.test.ts +++ b/packages/trigger-sdk/src/v3/triggerClient.test.ts @@ -15,7 +15,7 @@ function installFetchSpy() { const originalFetch = globalThis.fetch; globalThis.fetch = (async (input: any, init?: RequestInit) => { - const url = typeof input === "string" ? input : input?.url ?? String(input); + const url = typeof input === "string" ? input : (input?.url ?? String(input)); const headers = new Headers(init?.headers); captured.push({ url, diff --git a/packages/trigger-sdk/test/chat-snapshot.test.ts b/packages/trigger-sdk/test/chat-snapshot.test.ts index 85e1fe8adef..6ae6c3891f3 100644 --- a/packages/trigger-sdk/test/chat-snapshot.test.ts +++ b/packages/trigger-sdk/test/chat-snapshot.test.ts @@ -88,11 +88,12 @@ describe("chat snapshot helpers", () => { it("returns the snapshot on a successful GET", async () => { const { getChatSnapshotUrl } = stubApiClient({}); const snapshot = buildSnapshot(2); - stubFetch(async () => - new Response(JSON.stringify(snapshot), { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response(JSON.stringify(snapshot), { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("session-1"); @@ -122,11 +123,12 @@ describe("chat snapshot helpers", () => { it("returns undefined when the response body is malformed JSON", async () => { stubApiClient({}); - stubFetch(async () => - new Response("not-json-{[", { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response("not-json-{[", { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("malformed-session"); @@ -141,11 +143,12 @@ describe("chat snapshot helpers", () => { savedAt: Date.now(), messages: [], }; - stubFetch(async () => - new Response(JSON.stringify(futureSnapshot), { - status: 200, - headers: { "content-type": "application/json" }, - }) + stubFetch( + async () => + new Response(JSON.stringify(futureSnapshot), { + status: 200, + headers: { "content-type": "application/json" }, + }) ); const result = await readChatSnapshot("v99-session"); @@ -154,10 +157,11 @@ describe("chat snapshot helpers", () => { it("returns undefined when `messages` field is missing or wrong type", async () => { stubApiClient({}); - stubFetch(async () => - new Response(JSON.stringify({ version: 1, savedAt: 1, messages: "not-an-array" }), { - status: 200, - }) + stubFetch( + async () => + new Response(JSON.stringify({ version: 1, savedAt: 1, messages: "not-an-array" }), { + status: 200, + }) ); const result = await readChatSnapshot("bad-shape-session"); @@ -190,9 +194,7 @@ describe("chat snapshot helpers", () => { it("returns undefined when the response is not an object", async () => { stubApiClient({}); - stubFetch(async () => - new Response(JSON.stringify("just-a-string"), { status: 200 }) - ); + stubFetch(async () => new Response(JSON.stringify("just-a-string"), { status: 200 })); const result = await readChatSnapshot("string-response"); expect(result).toBeUndefined(); @@ -224,7 +226,9 @@ describe("chat snapshot helpers", () => { stubApiClient({}); stubFetch(async () => new Response("forbidden", { status: 403 })); - await expect(writeChatSnapshot("forbidden-session", buildSnapshot())).resolves.toBeUndefined(); + await expect( + writeChatSnapshot("forbidden-session", buildSnapshot()) + ).resolves.toBeUndefined(); }); it("returns without throwing on a fetch network error (warns)", async () => { diff --git a/packages/trigger-sdk/test/chatHandover.test.ts b/packages/trigger-sdk/test/chatHandover.test.ts index 72b3fa7e448..a101b91494f 100644 --- a/packages/trigger-sdk/test/chatHandover.test.ts +++ b/packages/trigger-sdk/test/chatHandover.test.ts @@ -98,8 +98,12 @@ describe("chat.handover", () => { const agent = chat.agent({ id: "chat.handover.pure-text", - onChatStart: () => { order.push("onChatStart"); }, - onTurnStart: () => { order.push("onTurnStart"); }, + onChatStart: () => { + order.push("onChatStart"); + }, + onTurnStart: () => { + order.push("onTurnStart"); + }, onTurnComplete: ({ responseMessage }) => { order.push("onTurnComplete"); capturedResponse = { @@ -265,9 +269,7 @@ describe("chat.handover", () => { // synthesized partial must carry it (with provider metadata, so an // Anthropic signature survives a UIMessage -> ModelMessage round // trip) or the durable history loses the step-1 thinking. - let captured: - | { partTypes?: string[]; reasoningText?: string; meta?: unknown } - | undefined; + let captured: { partTypes?: string[]; reasoningText?: string; meta?: unknown } | undefined; const agent = chat.agent({ id: "chat.handover.reasoning", @@ -279,9 +281,9 @@ describe("chat.handover", () => { .filter((p) => p.type === "reasoning") .map((p) => (p as { text?: string }).text || "") .join(""), - meta: (parts.find((p) => p.type === "reasoning") as - | { providerMetadata?: unknown } - | undefined)?.providerMetadata, + meta: ( + parts.find((p) => p.type === "reasoning") as { providerMetadata?: unknown } | undefined + )?.providerMetadata, }; }, run: async ({ messages, signal }) => { @@ -338,9 +340,7 @@ describe("chat.handover", () => { const runFn = vi.fn(); const stored: { id: string; role: string; parts: unknown[] }[] = []; const hydrateIncomingRoles: string[] = []; - let captured: - | { responseId?: string; responseText?: string; roles?: string[] } - | undefined; + let captured: { responseId?: string; responseText?: string; roles?: string[] } | undefined; const agent = chat.agent({ id: "chat.handover.hydrate-pure-text", diff --git a/packages/trigger-sdk/test/chatHandoverBackends.test.ts b/packages/trigger-sdk/test/chatHandoverBackends.test.ts index 29b45172e9e..66583586679 100644 --- a/packages/trigger-sdk/test/chatHandoverBackends.test.ts +++ b/packages/trigger-sdk/test/chatHandoverBackends.test.ts @@ -57,13 +57,21 @@ const TOOL_CALL_PARTIAL: ModelMessage[] = [ content: [ { type: "text", text: "let me check the weather" }, { type: "tool-call", toolCallId: "tc-1", toolName: "weather", input: { city: "tokyo" } }, - { type: "tool-approval-request", approvalId: "handover-approval-1", toolCallId: "tc-1" } as never, + { + type: "tool-approval-request", + approvalId: "handover-approval-1", + toolCallId: "tc-1", + } as never, ], }, { role: "tool", content: [ - { type: "tool-approval-response", approvalId: "handover-approval-1", approved: true } as never, + { + type: "tool-approval-response", + approvalId: "handover-approval-1", + approved: true, + } as never, ], }, ]; @@ -293,7 +301,11 @@ describe("chat.customAgent + headStart handover", () => { const prior: UIMessage[] = [ { id: "u-1", role: "user", parts: [{ type: "text", text: "hello" }] }, // Already-persisted partial under the same id the handover uses. - { id: "asst-dup", role: "assistant", parts: [{ type: "text", text: "Hi there, hope you're well." }] }, + { + id: "asst-dup", + role: "assistant", + parts: [{ type: "text", text: "Hi there, hope you're well." }], + }, ]; const agent = chat.customAgent({ diff --git a/packages/trigger-sdk/test/merge-by-id.test.ts b/packages/trigger-sdk/test/merge-by-id.test.ts index 1c0091273cc..7de51e2351e 100644 --- a/packages/trigger-sdk/test/merge-by-id.test.ts +++ b/packages/trigger-sdk/test/merge-by-id.test.ts @@ -62,10 +62,7 @@ describe("mergeByIdReplaceWins", () => { }); it("replaces by id when `b` has a colliding entry — replay wins", () => { - const a = [ - userMessage("u-1", "hi"), - assistantMessage("a-1", "stale-version"), - ]; + const a = [userMessage("u-1", "hi"), assistantMessage("a-1", "stale-version")]; const b = [assistantMessage("a-1", "fresh-version")]; const result = mergeByIdReplaceWins(a, b); expect(result).toHaveLength(2); @@ -80,10 +77,7 @@ describe("mergeByIdReplaceWins", () => { userMessage("u-2", "second"), assistantMessage("a-2", "also-stale"), ]; - const b = [ - assistantMessage("a-1", "fresh-1"), - assistantMessage("a-2", "fresh-2"), - ]; + const b = [assistantMessage("a-1", "fresh-1"), assistantMessage("a-2", "fresh-2")]; const result = mergeByIdReplaceWins(a, b); expect(result.map((m) => m.id)).toEqual(["u-1", "a-1", "u-2", "a-2"]); expect((result[1]!.parts[0] as { text: string }).text).toBe("fresh-1"); @@ -105,10 +99,18 @@ describe("mergeByIdReplaceWins", () => { const a = [ userMessage("u-1", "first"), // Synthetic message missing the id field — should append, never replace. - { id: "" as string, role: "assistant", parts: [{ type: "text", text: "no-id-a" }] } as UIMessage, + { + id: "" as string, + role: "assistant", + parts: [{ type: "text", text: "no-id-a" }], + } as UIMessage, ]; const b = [ - { id: "" as string, role: "assistant", parts: [{ type: "text", text: "no-id-b" }] } as UIMessage, + { + id: "" as string, + role: "assistant", + parts: [{ type: "text", text: "no-id-b" }], + } as UIMessage, ]; const result = mergeByIdReplaceWins(a, b); expect(result).toHaveLength(3); diff --git a/packages/trigger-sdk/test/mockChatAgent.test.ts b/packages/trigger-sdk/test/mockChatAgent.test.ts index c2001de723f..222d5d2ba09 100644 --- a/packages/trigger-sdk/test/mockChatAgent.test.ts +++ b/packages/trigger-sdk/test/mockChatAgent.test.ts @@ -205,9 +205,7 @@ describe("mockChatAgent", () => { // `upsertIncomingMessage` does. if (trigger === "submit-message" && incomingMessages.length > 0) { const newMsg = incomingMessages[incomingMessages.length - 1]!; - const exists = newMsg.id - ? stored.some((m) => m.id === newMsg.id) - : false; + const exists = newMsg.id ? stored.some((m) => m.id === newMsg.id) : false; if (!exists) stored.push(newMsg); } return [...stored]; @@ -308,7 +306,12 @@ describe("mockChatAgent", () => { }); }, run: async ({ messages, signal }) => { - return streamText({ model, messages, tools: { askUser: askUserTool }, abortSignal: signal }); + return streamText({ + model, + messages, + tools: { askUser: askUserTool }, + abortSignal: signal, + }); }, }); @@ -404,9 +407,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -482,9 +483,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -560,9 +559,7 @@ describe("mockChatAgent", () => { hydrateMessages: async () => [dbAssistant as any], onTurnComplete: async ({ uiMessages }) => { const head = uiMessages.find((m: any) => m.id === HEAD_ID); - mergedToolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + mergedToolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); }, run: async ({ messages, signal }) => { return streamText({ @@ -675,9 +672,7 @@ describe("mockChatAgent", () => { }); // Recommended pattern: validate only user messages, since HITL // continuations carry slim assistants the AI SDK schema rejects. - const userMessages = messages.filter( - (m: any) => m.role === "user" - ); + const userMessages = messages.filter((m: any) => m.role === "user"); if (userMessages.length > 0) { await validateUIMessages({ messages: userMessages, @@ -824,9 +819,7 @@ describe("mockChatAgent", () => { await harness.sendMessage(userMessage("hi")); await new Promise((r) => setTimeout(r, 50)); - const turn1Assistant = turnsSeen.at(-1)?.uiMessages.find( - (m: any) => m.role === "assistant" - ); + const turn1Assistant = turnsSeen.at(-1)?.uiMessages.find((m: any) => m.role === "assistant"); expect(turn1Assistant).toBeTruthy(); const HEAD_ID = turn1Assistant!.id; @@ -848,9 +841,7 @@ describe("mockChatAgent", () => { const turn2 = turnsSeen.at(-1); const head = turn2!.uiMessages.find((m: any) => m.id === HEAD_ID); - const toolPart = (head?.parts ?? []).find( - (p: any) => p?.toolCallId === TC - ); + const toolPart = (head?.parts ?? []).find((p: any) => p?.toolCallId === TC); expect(toolPart?.state).toBe("output-available"); expect(toolPart?.output).toEqual({ color: "blue" }); // Snapshot's `input` survived the merge. @@ -1038,9 +1029,7 @@ describe("mockChatAgent", () => { // No additional model call; console.warn fired with our marker text. expect(runSpy.mock.calls.length).toBe(baselineRun); expect( - warnSpy.mock.calls.some((args) => - (args[0] as string).includes("no `onAction` handler") - ) + warnSpy.mock.calls.some((args) => (args[0] as string).includes("no `onAction` handler")) ).toBe(true); const sawTurnComplete = actionTurn.rawChunks.some( @@ -2061,8 +2050,7 @@ describe("mockChatAgent", () => { id: "onChatStart-gate.fresh-baseline", onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "fresh-baseline" }); try { @@ -2089,8 +2077,7 @@ describe("mockChatAgent", () => { hydrateMessages: async ({ incomingMessages }) => incomingMessages, onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "continuation-skip", @@ -2124,8 +2111,7 @@ describe("mockChatAgent", () => { hydrateMessages: async ({ incomingMessages }) => incomingMessages, onChatStart, onTurnStart, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "oom-retry-skip", diff --git a/packages/trigger-sdk/test/promptCaching.test.ts b/packages/trigger-sdk/test/promptCaching.test.ts index b20bf012855..6834596b7c5 100644 --- a/packages/trigger-sdk/test/promptCaching.test.ts +++ b/packages/trigger-sdk/test/promptCaching.test.ts @@ -126,7 +126,9 @@ describe("chat prompt caching — system providerOptions", () => { messages, abortSignal: signal, ...chat.toStreamTextOptions({ - systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } }, + systemProviderOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } }, + }, }), }), }); @@ -186,7 +188,9 @@ describe("chat prompt caching — system providerOptions", () => { messages, abortSignal: signal, ...chat.toStreamTextOptions({ - systemProviderOptions: { anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } } }, + systemProviderOptions: { + anthropic: { cacheControl: { type: "ephemeral", ttl: "1h" } }, + }, }), }), }); diff --git a/packages/trigger-sdk/test/recovery-boot.test.ts b/packages/trigger-sdk/test/recovery-boot.test.ts index 5d7b4cd2213..0b11567fb9d 100644 --- a/packages/trigger-sdk/test/recovery-boot.test.ts +++ b/packages/trigger-sdk/test/recovery-boot.test.ts @@ -71,8 +71,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.no-state", onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "no-state", @@ -102,8 +101,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { captured.event = event as never; return {}; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "partial-fires-hook", @@ -162,8 +160,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { captured.event = event; return {}; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "pending-tool-from-raw", @@ -173,12 +170,13 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { harness.seedSessionInTail([u1 as never]); // Install AFTER mockChatAgent — its constructor sets its own default // override that we want to replace for this test. - __setReplaySessionOutTailImplForTests(async () => - ({ - settled: [], - partial: cleanedPartial, - partialRaw: rawPartial, - }) as never + __setReplaySessionOutTailImplForTests( + async () => + ({ + settled: [], + partial: cleanedPartial, + partialRaw: rawPartial, + }) as never ); try { await new Promise((r) => setTimeout(r, 50)); @@ -207,8 +205,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.inflight-users-no-partial", onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "inflight-users-no-partial", @@ -237,8 +234,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.default-dispatch", // NO onRecoveryBoot — exercise the default path - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "default-dispatch", @@ -277,8 +273,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { })); } }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "smart-default", @@ -292,11 +287,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { // Turn 1 fires with the follow-up user (u2). Its chain should // include [u1 (original), a-partial, u2 (follow-up)]. expect(turnCount).toBe(1); - expect(observedChain.map((m) => m.role)).toEqual([ - "user", - "assistant", - "user", - ]); + expect(observedChain.map((m) => m.role)).toEqual(["user", "assistant", "user"]); expect(observedChain[0]!.idHead).toBe("u-1"); expect(observedChain[1]!.idHead).toBe("a-partial"); expect(observedChain[2]!.idHead).toBe("u-2"); @@ -318,8 +309,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { const agent = chat.agent({ id: "recovery-boot.suppress-dispatch", onRecoveryBoot: async (): Promise => ({ recoveredTurns: [] }), - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "suppress-dispatch", @@ -356,8 +346,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { onTurnStart: async ({ uiMessages }) => { observedMessageCount = uiMessages.length; }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "chain-override", @@ -387,8 +376,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { id: "recovery-boot.hydrate-skips", hydrateMessages: async ({ incomingMessages }) => incomingMessages, onRecoveryBoot, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "hydrate-skips", @@ -423,8 +411,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { order.push("beforeBoot"); }, }), - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "before-boot", @@ -459,8 +446,7 @@ describe("onRecoveryBoot — chat.agent recovery hook", () => { onRecoveryBoot: async () => { throw new Error("kaboom"); }, - run: async ({ messages, signal }) => - streamText({ model, messages, abortSignal: signal }), + run: async ({ messages, signal }) => streamText({ model, messages, abortSignal: signal }), }); const harness = mockChatAgent(agent, { chatId: "hook-throws", diff --git a/packages/trigger-sdk/test/replay-session-in.test.ts b/packages/trigger-sdk/test/replay-session-in.test.ts index a90ecc15396..3d04974a9b6 100644 --- a/packages/trigger-sdk/test/replay-session-in.test.ts +++ b/packages/trigger-sdk/test/replay-session-in.test.ts @@ -48,11 +48,21 @@ describe("replaySessionInTail", () => { stubReadRecords([ { kind: "message", - payload: { chatId: "c1", trigger: "submit-message", message: u1, metadata: { userId: "a" } }, + payload: { + chatId: "c1", + trigger: "submit-message", + message: u1, + metadata: { userId: "a" }, + }, }, { kind: "message", - payload: { chatId: "c1", trigger: "submit-message", message: u2, metadata: { userId: "b" } }, + payload: { + chatId: "c1", + trigger: "submit-message", + message: u2, + metadata: { userId: "b" }, + }, }, ]); diff --git a/packages/trigger-sdk/test/replay-session-out.test.ts b/packages/trigger-sdk/test/replay-session-out.test.ts index ed78ecec146..0111ec200b3 100644 --- a/packages/trigger-sdk/test/replay-session-out.test.ts +++ b/packages/trigger-sdk/test/replay-session-out.test.ts @@ -179,7 +179,11 @@ describe("replaySessionOutTail", () => { // Trailing turn: starts a tool call but never resolves it. { type: "start", messageId: "a-2", messageMetadata: { role: "assistant" } } as UIMessageChunk, { type: "tool-input-start", toolCallId: "tc-cut", toolName: "search" } as UIMessageChunk, - { type: "tool-input-delta", toolCallId: "tc-cut", inputTextDelta: '{"q":"x"}' } as UIMessageChunk, + { + type: "tool-input-delta", + toolCallId: "tc-cut", + inputTextDelta: '{"q":"x"}', + } as UIMessageChunk, // No tool-input-end, no tool-call, no finish → orphaned. ]); @@ -192,9 +196,9 @@ describe("replaySessionOutTail", () => { // would represent a tool the next turn would re-process. const trailing = result.find((m) => m.id === "a-2"); if (trailing) { - const orphanedToolPart = (trailing.parts as Array<{ type: string; toolCallId?: string; state?: string }>).find( - (p) => p.toolCallId === "tc-cut" && p.state === "input-streaming" - ); + const orphanedToolPart = ( + trailing.parts as Array<{ type: string; toolCallId?: string; state?: string }> + ).find((p) => p.toolCallId === "tc-cut" && p.state === "input-streaming"); expect(orphanedToolPart).toBeUndefined(); } }); @@ -205,7 +209,11 @@ describe("replaySessionOutTail", () => { // entirely (it never reached the next turn's accumulator). stubReadRecordsWithChunks([ ...textTurn("a-1", "complete"), - { type: "start", messageId: "a-orphan", messageMetadata: { role: "assistant" } } as UIMessageChunk, + { + type: "start", + messageId: "a-orphan", + messageMetadata: { role: "assistant" }, + } as UIMessageChunk, { type: "tool-input-start", toolCallId: "tc-orph", toolName: "search" } as UIMessageChunk, // No tool-input-end, no tool-call, no finish. ]); @@ -233,10 +241,7 @@ describe("replaySessionOutTail", () => { // The writer puts chunk objects directly into the record envelope; // the route forwards them as-is. A string body is malformed — the // consumer drops it defensively rather than JSON.parsing. - stubReadRecordsWithChunks([ - "not-an-object", - ...textTurn("a-1", "survived"), - ]); + stubReadRecordsWithChunks(["not-an-object", ...textTurn("a-1", "survived")]); const result = await replaySessionOutTail("garbage-session"); expect(result).toHaveLength(1); @@ -247,12 +252,7 @@ describe("replaySessionOutTail", () => { // The consumer requires `chunk` to be a non-null object with a // string `type` field. Records that arrive as primitives // (number, null, string) are dropped silently. - stubReadRecordsWithChunks([ - 42, - null, - "just-a-string", - ...textTurn("a-1", "survived"), - ]); + stubReadRecordsWithChunks([42, null, "just-a-string", ...textTurn("a-1", "survived")]); const result = await replaySessionOutTail("primitive-data-session"); expect(result).toHaveLength(1); @@ -260,11 +260,7 @@ describe("replaySessionOutTail", () => { }); it("ignores chunks missing a `type` field", async () => { - stubReadRecordsWithChunks([ - { foo: "bar" }, - { type: 42 }, - ...textTurn("a-1", "valid"), - ]); + stubReadRecordsWithChunks([{ foo: "bar" }, { type: 42 }, ...textTurn("a-1", "valid")]); const result = await replaySessionOutTail("typeless-session"); expect(result).toHaveLength(1); @@ -277,7 +273,11 @@ describe("replaySessionOutTail", () => { // a single corrupt segment shouldn't sink the entire replay. stubReadRecordsWithChunks([ // Malformed: text-end with no preceding text-start. - { type: "start", messageId: "bad-1", messageMetadata: { role: "assistant" } } as UIMessageChunk, + { + type: "start", + messageId: "bad-1", + messageMetadata: { role: "assistant" }, + } as UIMessageChunk, { type: "text-end", id: "no-such-text" } as UIMessageChunk, { type: "finish" } as UIMessageChunk, ...textTurn("a-1", "after-bad"), diff --git a/packages/trigger-sdk/test/wire-shape.test.ts b/packages/trigger-sdk/test/wire-shape.test.ts index 6214f3ca01e..54a5074b0b3 100644 --- a/packages/trigger-sdk/test/wire-shape.test.ts +++ b/packages/trigger-sdk/test/wire-shape.test.ts @@ -180,13 +180,17 @@ describe("upsertIncomingMessage", () => { const head = { id: "asst-1", role: "assistant" as const, - parts: [{ type: "tool-search", toolCallId: "tc-1", state: "input-available", input: {} } as never], + parts: [ + { type: "tool-search", toolCallId: "tc-1", state: "input-available", input: {} } as never, + ], }; const stored: UIMessage[] = [userMsg("u-1", "hi"), head]; const slim = { id: "asst-1", role: "assistant" as const, - parts: [{ type: "tool-search", toolCallId: "tc-1", state: "output-available", output: {} } as never], + parts: [ + { type: "tool-search", toolCallId: "tc-1", state: "output-available", output: {} } as never, + ], }; const mutated = upsertIncomingMessage(stored, { trigger: "submit-message", @@ -243,7 +247,10 @@ describe("upsertIncomingMessage", () => { it("pushes when newMsg has no id (no dedup possible)", () => { const stored: UIMessage[] = [userMsg("u-1", "hi")]; - const incoming = { role: "user", parts: [{ type: "text", text: "no id" }] } as unknown as UIMessage; + const incoming = { + role: "user", + parts: [{ type: "text", text: "no id" }], + } as unknown as UIMessage; const mutated = upsertIncomingMessage(stored, { trigger: "submit-message", incomingMessages: [incoming], @@ -455,7 +462,9 @@ describe("ChatTaskWirePayload (compile-time shape)", () => { // forces a compile error rather than letting the wire silently grow // back. type WirePayloadKeys = keyof ChatTaskWirePayload; - expectTypeOf().not.toEqualTypeOf<"messages" | Exclude>(); + expectTypeOf().not.toEqualTypeOf< + "messages" | Exclude + >(); // Also confirm the absence at the value level — a payload literal // with `messages` would be a TS error if uncommented: // @@ -478,18 +487,13 @@ describe("ChatTaskWirePayload (compile-time shape)", () => { it("requires `chatId: string` and `trigger: `", () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf< - | "submit-message" - | "regenerate-message" - | "preload" - | "close" - | "action" - | "handover-prepare" + "submit-message" | "regenerate-message" | "preload" | "close" | "action" | "handover-prepare" >(); }); }); describe("ChatInputChunk envelope", () => { - it("wraps a wire payload in `kind: \"message\"` shape", () => { + it('wraps a wire payload in `kind: "message"` shape', () => { const userMsg: UIMessage = { id: "u-1", role: "user", @@ -511,7 +515,7 @@ describe("ChatInputChunk envelope", () => { } }); - it("supports `kind: \"stop\"` records (no payload)", () => { + it('supports `kind: "stop"` records (no payload)', () => { const chunk: ChatInputChunk = { kind: "stop", message: "user-canceled" }; const decoded = JSON.parse(JSON.stringify(chunk)) as ChatInputChunk; expect(decoded.kind).toBe("stop"); @@ -520,7 +524,7 @@ describe("ChatInputChunk envelope", () => { } }); - it("supports `kind: \"handover\"` records (with partialAssistantMessage)", () => { + it('supports `kind: "handover"` records (with partialAssistantMessage)', () => { const chunk: ChatInputChunk = { kind: "handover", partialAssistantMessage: [ @@ -533,7 +537,7 @@ describe("ChatInputChunk envelope", () => { expect(decoded.kind).toBe("handover"); }); - it("supports `kind: \"handover-skip\"` records", () => { + it('supports `kind: "handover-skip"` records', () => { const chunk: ChatInputChunk = { kind: "handover-skip" }; const decoded = JSON.parse(JSON.stringify(chunk)) as ChatInputChunk; expect(decoded.kind).toBe("handover-skip"); diff --git a/packages/trigger-sdk/vitest.config.ts b/packages/trigger-sdk/vitest.config.ts index 8387992eaae..8db9c42ae0c 100644 --- a/packages/trigger-sdk/vitest.config.ts +++ b/packages/trigger-sdk/vitest.config.ts @@ -6,4 +6,3 @@ export default defineConfig({ globals: true, }, }); - diff --git a/rules/manifest.json b/rules/manifest.json index 64d9a86139e..d7bc0a9168f 100644 --- a/rules/manifest.json +++ b/rules/manifest.json @@ -151,4 +151,4 @@ ] } } -} \ No newline at end of file +} diff --git a/scripts/bundleSdkDocs.ts b/scripts/bundleSdkDocs.ts index 80baf7ef0ea..bcbeb10723e 100644 --- a/scripts/bundleSdkDocs.ts +++ b/scripts/bundleSdkDocs.ts @@ -114,7 +114,9 @@ async function bundleSdkDocs() { if (copied === 0) { // Every nav page was missing on disk; refuse to ship the SDK with an empty docs bundle. - throw new Error(`[bundleSdkDocs] 0 docs copied from the "${DROPDOWN}" nav; refusing empty docs bundle`); + throw new Error( + `[bundleSdkDocs] 0 docs copied from the "${DROPDOWN}" nav; refusing empty docs bundle` + ); } console.log( diff --git a/scripts/generate-github-release.mjs b/scripts/generate-github-release.mjs index fda2f3cb8c6..8fc528df639 100755 --- a/scripts/generate-github-release.mjs +++ b/scripts/generate-github-release.mjs @@ -88,9 +88,7 @@ function extractChangesFromPrBody(body) { function getContributors(previousVersion) { try { - const range = previousVersion - ? `v${previousVersion}...HEAD` - : "HEAD~50..HEAD"; + const range = previousVersion ? `v${previousVersion}...HEAD` : "HEAD~50..HEAD"; const log = execSync(`git log ${range} --format="%aN|%aE" --no-merges`, { cwd: ROOT_DIR, encoding: "utf-8", @@ -111,9 +109,7 @@ function getContributors(previousVersion) { contributors.set(name, (contributors.get(name) || 0) + 1); } - return [...contributors.entries()] - .sort((a, b) => b[1] - a[1]) - .map(([name]) => name); + return [...contributors.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name); } catch { return []; } @@ -128,9 +124,7 @@ function getPublishedPackages() { for (const dir of readdirSync(packagesDir, { withFileTypes: true })) { if (!dir.isDirectory()) continue; try { - const pkg = JSON.parse( - readFileSync(join(packagesDir, dir.name, "package.json"), "utf-8") - ); + const pkg = JSON.parse(readFileSync(join(packagesDir, dir.name, "package.json"), "utf-8")); if (pkg.name && !pkg.private) { names.push(pkg.name); } @@ -217,9 +211,7 @@ function formatRelease({ version, changesContent, contributors, packages }) { lines.push("## Contributors"); lines.push(""); lines.push( - contributors - .map((c) => (/^[A-Za-z0-9][-A-Za-z0-9]*$/.test(c) ? `@${c}` : c)) - .join(", ") + contributors.map((c) => (/^[A-Za-z0-9][-A-Za-z0-9]*$/.test(c) ? `@${c}` : c)).join(", ") ); lines.push(""); } diff --git a/scripts/recover-stuck-runs.ts b/scripts/recover-stuck-runs.ts index 28bb4e85e46..e107cc38540 100755 --- a/scripts/recover-stuck-runs.ts +++ b/scripts/recover-stuck-runs.ts @@ -71,18 +71,20 @@ async function main() { const [environmentId, postgresUrl, redisReadUrl, redisWriteUrl] = process.argv.slice(2); if (!environmentId || !postgresUrl || !redisReadUrl) { - console.error("Usage: tsx scripts/recover-stuck-runs.ts [redisWriteUrl]"); + console.error( + "Usage: tsx scripts/recover-stuck-runs.ts [redisWriteUrl]" + ); console.error(""); console.error("Dry-run mode when no redisWriteUrl is provided (read-only)."); console.error("Execute mode when redisWriteUrl is provided (makes actual changes)."); console.error(""); console.error("Example (dry-run):"); - console.error(' tsx scripts/recover-stuck-runs.ts env_1234567890 \\'); + console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\"); console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\'); console.error(' "redis://readonly.example.com:6379"'); console.error(""); console.error("Example (execute):"); - console.error(' tsx scripts/recover-stuck-runs.ts env_1234567890 \\'); + console.error(" tsx scripts/recover-stuck-runs.ts env_1234567890 \\"); console.error(' "postgresql://user:pass@localhost:5432/triggerdev" \\'); console.error(' "redis://readonly.example.com:6379" \\'); console.error(' "redis://writeonly.example.com:6379"'); @@ -261,7 +263,9 @@ async function main() { } // Prepare recovery operations - console.log(`\n⚔ ${executeMode ? "Executing" : "Planning"} recovery for ${stuckRuns.length} stuck runs`); + console.log( + `\n⚔ ${executeMode ? "Executing" : "Planning"} recovery for ${stuckRuns.length} stuck runs` + ); console.log(`This will:`); console.log(` 1. Add each run back to its specific queue sorted set`); console.log(` 2. Remove each run from the queue-specific currentConcurrency set`); diff --git a/scripts/stamp-preview-version.mjs b/scripts/stamp-preview-version.mjs index a6b21d10777..36a025668fd 100644 --- a/scripts/stamp-preview-version.mjs +++ b/scripts/stamp-preview-version.mjs @@ -55,9 +55,7 @@ function resolveSha() { const sha = resolveSha().slice(0, 7); const previewVersion = `0.0.0-preview-${sha}`; -const dirs = readdirSync(PACKAGES_DIR, { withFileTypes: true }).filter((e) => - e.isDirectory() -); +const dirs = readdirSync(PACKAGES_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()); // First pass: collect every public package name so we know which workspace // specifiers point at a sibling whose version we are about to change. diff --git a/server.json b/server.json index 57b89ff193a..b0b552e33a4 100644 --- a/server.json +++ b/server.json @@ -28,4 +28,4 @@ "environment_variables": [] } ] -} \ No newline at end of file +}