mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 09:56:24 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54504ab3a5 | ||
|
|
cc5086d127 | ||
|
|
b20482461c | ||
|
|
5b5368fe98 | ||
|
|
582a2108ce | ||
|
|
9c65a69937 | ||
|
|
1d391908f4 | ||
|
|
596dca4dee | ||
|
|
1827832775 | ||
|
|
898692af26 | ||
|
|
5c3f2ddf8c | ||
|
|
1382cebe10 | ||
|
|
0732cdd8e1 | ||
|
|
2bf6a9813a | ||
|
|
f094a10645 | ||
|
|
89f1943d2d | ||
|
|
fc4f319676 | ||
|
|
b51fdb331c | ||
|
|
8a1a6b8fb4 | ||
|
|
1a2b985777 | ||
|
|
00067d23a0 | ||
|
|
3ebc4b01b9 | ||
|
|
17f362e0fe | ||
|
|
5b83c15fb5 | ||
|
|
d86c3041c9 |
@@ -63,6 +63,7 @@
|
||||
"@ibm/plex": "6.4.1",
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/session-ui": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -357,6 +358,7 @@
|
||||
"@opencode-ai/ai": "workspace:*",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -415,6 +417,7 @@
|
||||
"electron-store": "11.0.2",
|
||||
"electron-updater": "6.8.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"lighthouse": "13.4.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@brendonovich/vite-plugin-opencode": "0.1.3",
|
||||
@@ -422,6 +425,8 @@
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@sentry/solid": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
@@ -432,12 +437,14 @@
|
||||
"@types/node": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"app-builder-lib": "26.15.7",
|
||||
"devtools-protocol": "0.0.1687809",
|
||||
"drizzle-kit": "catalog:",
|
||||
"drizzle-orm": "catalog:",
|
||||
"effect": "catalog:",
|
||||
"electron": "42.10.1",
|
||||
"electron-builder": "26.15.7",
|
||||
"electron-vite": "6.0.0-beta.1",
|
||||
"puppeteer-core": "25.9.0",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "8.2.2",
|
||||
@@ -605,6 +612,21 @@
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
"packages/plugin-browser": {
|
||||
"name": "@opencode-ai/plugin-browser",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/posts": {
|
||||
"name": "@opencode-ai/posts",
|
||||
"dependencies": {
|
||||
@@ -1824,6 +1846,16 @@
|
||||
|
||||
"@fontsource/inter": ["@fontsource/inter@5.3.0", "", {}, "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g=="],
|
||||
|
||||
"@formatjs/ecma402-abstract": ["@formatjs/ecma402-abstract@2.3.6", "", { "dependencies": { "@formatjs/fast-memoize": "2.2.7", "@formatjs/intl-localematcher": "0.6.2", "decimal.js": "^10.4.3", "tslib": "^2.8.0" } }, "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw=="],
|
||||
|
||||
"@formatjs/fast-memoize": ["@formatjs/fast-memoize@2.2.7", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ=="],
|
||||
|
||||
"@formatjs/icu-messageformat-parser": ["@formatjs/icu-messageformat-parser@2.11.4", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/icu-skeleton-parser": "1.8.16", "tslib": "^2.8.0" } }, "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw=="],
|
||||
|
||||
"@formatjs/icu-skeleton-parser": ["@formatjs/icu-skeleton-parser@1.8.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "tslib": "^2.8.0" } }, "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ=="],
|
||||
|
||||
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="],
|
||||
|
||||
"@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
|
||||
|
||||
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
||||
@@ -2144,6 +2176,8 @@
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||
|
||||
"@opencode-ai/plugin-browser": ["@opencode-ai/plugin-browser@workspace:packages/plugin-browser"],
|
||||
|
||||
"@opencode-ai/posts": ["@opencode-ai/posts@workspace:packages/posts"],
|
||||
|
||||
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||
@@ -2208,6 +2242,8 @@
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="],
|
||||
|
||||
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.220.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
@@ -2218,6 +2254,8 @@
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="],
|
||||
|
||||
"@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="],
|
||||
@@ -2490,6 +2528,8 @@
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||
|
||||
"@paulirish/trace_engine": ["@paulirish/trace_engine@0.0.65", "", { "dependencies": { "legacy-javascript": "latest", "third-party-web": "latest" } }, "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg=="],
|
||||
|
||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-AKvPMOM7LfK0uFe1m7o7+veOa8xQGPqsqOrKi3QKgCElzwjGp39mbhr2g7mt/v/mXQHiIvJmDj5cJS173x8Q9Q=="],
|
||||
|
||||
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
|
||||
@@ -2546,6 +2586,8 @@
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="],
|
||||
|
||||
"@radix-ui/colors": ["@radix-ui/colors@1.0.1", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.0.1", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
|
||||
@@ -2744,10 +2786,18 @@
|
||||
|
||||
"@sentry/feedback": ["@sentry/feedback@10.71.0", "", { "dependencies": { "@sentry/core": "10.71.0" } }, "sha512-4+zMAmn1DcbYBtFE0pWt6zo8vWoBHTuP/UhtCMTCoGB4sVYvmwRxv9Hc9OpRHSzE9kSl8beD0zKFxuoVHizKQQ=="],
|
||||
|
||||
"@sentry/node": ["@sentry/node@10.73.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.1", "@opentelemetry/instrumentation": "^0.220.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.73.0", "@sentry/node-core": "10.73.0", "@sentry/opentelemetry": "10.73.0", "@sentry/server-utils": "10.73.0", "import-in-the-middle": "^3.0.0" } }, "sha512-jiMJ6GgXDw6UMGzJY+o0c8OoeA9OfHqZ/xEpHfDqy75hn+9CEkRkbNGCIdMvsc7wW/Se1Os394hHfTpU+YEskg=="],
|
||||
|
||||
"@sentry/node-core": ["@sentry/node-core@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.73.0", "@sentry/opentelemetry": "10.73.0", "import-in-the-middle": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/core", "@opentelemetry/exporter-trace-otlp-http", "@opentelemetry/instrumentation", "@opentelemetry/sdk-trace-base"] }, "sha512-GHAGUmZPmm6FKfxfv2maVVJ/A99YAmd7oFOuKMDmITI6O/Msl6JB3vPTEpopVAHLW0LYJQBOjddL9C7o+Jt44g=="],
|
||||
|
||||
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.73.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" } }, "sha512-fQouPQKsH0CQrw6oAn1k0Z2I+tgyochCovifr5qNS69i0OzjknLa03WJyiZ/IuzXc4AVa5jAKfOeE9slABz8Qw=="],
|
||||
|
||||
"@sentry/replay": ["@sentry/replay@10.71.0", "", { "dependencies": { "@sentry/browser-utils": "10.71.0", "@sentry/core": "10.71.0" } }, "sha512-ytEgVg7isvavQL6hgsgYeuSCkcA3EyOKm9lMLQOZTLkiUCtFT/ItWwGNhzS46vQ6wsTv3Uc0Y1JlcJHSFKe/Kg=="],
|
||||
|
||||
"@sentry/replay-canvas": ["@sentry/replay-canvas@10.71.0", "", { "dependencies": { "@sentry/core": "10.71.0", "@sentry/replay": "10.71.0" } }, "sha512-YTqesxBh9arExI49LrTm4Y2/xPX40q2GWi0gWRw6lNfYtyNz7/ZfHi8/1N6JEc8dldTslqFhII6ZFecUyU9iaA=="],
|
||||
|
||||
"@sentry/server-utils": ["@sentry/server-utils@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0", "@sentry/core": "10.73.0" } }, "sha512-QskripdKFbM/+gipC6mpa2crLwL7+VbkX84IpHg2z9UlYQ1kNKd3aMT+Qk9NLRSw/zu1rSIAvlbWfx4D3rgNAA=="],
|
||||
|
||||
"@sentry/solid": ["@sentry/solid@10.71.0", "", { "dependencies": { "@sentry/browser": "10.71.0", "@sentry/conventions": "^0.16.0", "@sentry/core": "10.71.0" }, "peerDependencies": { "@solidjs/router": ">=0.13.4 <2.0.0-0", "@tanstack/solid-router": "^1.132.27", "solid-js": "^1.8.4" }, "optionalPeers": ["@solidjs/router", "@tanstack/solid-router"] }, "sha512-/vVy9GmB/RejmomWEMZ+P3xlwrhfpIYJ3F0+UcTE836uuH92r8CSnmucdGQqke8c/6ullQFjZrLd/dTDPXj1aA=="],
|
||||
|
||||
"@sentry/vite-plugin": ["@sentry/vite-plugin@5.4.0", "", { "dependencies": { "@sentry/bundler-plugins": "^10.64.0" } }, "sha512-fFJgCxs5hDyAm9BbZJ+LbA+LK2tjX5OoD0v0ARU4StR6KQmGUduoPs69yJ9AfqZ0om3Rlp5JDliiwFcNkasORA=="],
|
||||
@@ -3314,6 +3364,8 @@
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
@@ -3542,10 +3594,16 @@
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"chrome-launcher": ["chrome-launcher@1.2.1", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A=="],
|
||||
|
||||
"chromium-bidi": ["chromium-bidi@17.0.2", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg=="],
|
||||
|
||||
"chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="],
|
||||
|
||||
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
|
||||
|
||||
"cjs-module-lexer": ["cjs-module-lexer@2.2.1", "", {}, "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q=="],
|
||||
|
||||
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
|
||||
|
||||
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
|
||||
@@ -3602,6 +3660,8 @@
|
||||
|
||||
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
|
||||
|
||||
"configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
@@ -3638,6 +3698,8 @@
|
||||
|
||||
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
|
||||
|
||||
"csp_evaluator": ["csp_evaluator@1.1.8", "", {}, "sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
|
||||
@@ -3742,6 +3804,8 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||
@@ -3790,6 +3854,8 @@
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"devtools-protocol": ["devtools-protocol@0.0.1687809", "", {}, "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g=="],
|
||||
|
||||
"dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="],
|
||||
|
||||
"diacritics": ["diacritics@1.3.0", "", {}, "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA=="],
|
||||
@@ -3894,6 +3960,8 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
|
||||
@@ -3938,7 +4006,7 @@
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
@@ -4240,6 +4308,8 @@
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||
|
||||
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
|
||||
@@ -4264,8 +4334,12 @@
|
||||
|
||||
"ignore-walk": ["ignore-walk@8.0.0", "", { "dependencies": { "minimatch": "^10.0.3" } }, "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A=="],
|
||||
|
||||
"image-ssim": ["image-ssim@0.2.0", "", {}, "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg=="],
|
||||
|
||||
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||
|
||||
"import-in-the-middle": ["import-in-the-middle@3.4.0", "", { "dependencies": { "cjs-module-lexer": "^2.2.0", "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-Xfjwfarhe+LGmoaof+sexeNo3sGRysb5x56WrZLwHtmqT0OilSKtVWkv0lrCcMgSKyKP9oBogBAisHZvtJH0aw=="],
|
||||
|
||||
"import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
@@ -4284,6 +4358,8 @@
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="],
|
||||
|
||||
"ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
@@ -4402,12 +4478,16 @@
|
||||
|
||||
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
||||
|
||||
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
|
||||
|
||||
"js-beautify": ["js-beautify@1.15.4", "", { "dependencies": { "config-chain": "^1.1.13", "editorconfig": "^1.0.4", "glob": "^10.4.2", "js-cookie": "^3.0.5", "nopt": "^7.2.1" }, "bin": { "js-beautify": "js/bin/js-beautify.js", "css-beautify": "js/bin/css-beautify.js", "html-beautify": "js/bin/html-beautify.js" } }, "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.8", "", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="],
|
||||
|
||||
"js-library-detector": ["js-library-detector@6.7.0", "", {}, "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.15.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag=="],
|
||||
@@ -4480,8 +4560,16 @@
|
||||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
||||
"legacy-javascript": ["legacy-javascript@0.0.1", "", {}, "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg=="],
|
||||
|
||||
"leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
|
||||
|
||||
"lighthouse": ["lighthouse@13.4.1", "", { "dependencies": { "@paulirish/trace_engine": "0.0.65", "@sentry/node": "^10.0.0", "axe-core": "^4.12.1", "chrome-launcher": "^1.2.1", "configstore": "^7.0.0", "csp_evaluator": "1.1.8", "devtools-protocol": "0.0.1663043", "enquirer": "^2.3.6", "http-link-header": "^1.1.1", "intl-messageformat": "^10.5.3", "jpeg-js": "^0.4.4", "js-library-detector": "^6.7.0", "lighthouse-logger": "^2.0.2", "lighthouse-stack-packs": "1.12.3", "lodash-es": "^4.17.21", "lookup-closest-locale": "6.2.0", "open": "^8.4.0", "puppeteer-core": "^25.3.0", "robots-parser": "^3.0.1", "speedline-core": "^1.4.3", "third-party-web": "^0.29.2", "tldts-icann": "^7.4.9", "web-features": "^3.34.0", "ws": "^7.0.0", "yargs": "^17.3.1", "yargs-parser": "^21.0.0" }, "bin": { "lighthouse": "cli/index.js", "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js", "chrome-debug": "core/scripts/manual-chrome-launcher.js" } }, "sha512-fDu8lt3QLK/lTqIxtp1HkzQNJ32rsFHhbadYOepcMZFLgA8oINhxutMbMv8XXnpTOvZ0TXCo4JCk1LDTWaRLnA=="],
|
||||
|
||||
"lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="],
|
||||
|
||||
"lighthouse-stack-packs": ["lighthouse-stack-packs@1.12.3", "", {}, "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||
@@ -4528,6 +4616,8 @@
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lookup-closest-locale": ["lookup-closest-locale@6.2.0", "", {}, "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||
@@ -4560,6 +4650,8 @@
|
||||
|
||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||
|
||||
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -4720,8 +4812,14 @@
|
||||
|
||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
|
||||
|
||||
"modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="],
|
||||
|
||||
"module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
|
||||
|
||||
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="],
|
||||
@@ -5032,6 +5130,8 @@
|
||||
|
||||
"pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
|
||||
|
||||
"puppeteer-core": ["puppeteer-core@25.9.0", "", { "dependencies": { "@puppeteer/browsers": "3.2.1", "chromium-bidi": "17.0.2", "devtools-protocol": "0.0.1666840", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.3" } }, "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
|
||||
|
||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||
@@ -5158,6 +5258,8 @@
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
|
||||
|
||||
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
|
||||
|
||||
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||
@@ -5192,6 +5294,8 @@
|
||||
|
||||
"roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
|
||||
|
||||
"robots-parser": ["robots-parser@3.0.1", "", {}, "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="],
|
||||
@@ -5350,6 +5454,8 @@
|
||||
|
||||
"spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="],
|
||||
|
||||
"speedline-core": ["speedline-core@1.4.3", "", { "dependencies": { "@types/node": "*", "image-ssim": "^0.2.0", "jpeg-js": "^0.4.1" } }, "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||
@@ -5484,6 +5590,8 @@
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"third-party-web": ["third-party-web@0.29.2", "", {}, "sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g=="],
|
||||
|
||||
"tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
@@ -5506,6 +5614,10 @@
|
||||
|
||||
"titleize": ["titleize@4.0.0", "", {}, "sha512-ZgUJ1K83rhdu7uh7EHAC2BgY5DzoX8V5rTvoWI4vFysggi6YjLe5gUXABPWAU7VkvGP7P/0YiWq+dcPeYDsf1g=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.4.11", "", {}, "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg=="],
|
||||
|
||||
"tldts-icann": ["tldts-icann@7.4.11", "", { "dependencies": { "tldts-core": "^7.4.11" } }, "sha512-1p+NDJ7FUYCliESmsQl9EW5Um8JIyFheiy6Y6ZoHho27d+TLAGFa0gMf1VOVy02vNY1aQo6xeKiaGj45+7P+PA=="],
|
||||
|
||||
"tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="],
|
||||
|
||||
"tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="],
|
||||
@@ -5568,6 +5680,8 @@
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
|
||||
|
||||
"typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
|
||||
|
||||
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
@@ -5728,6 +5842,8 @@
|
||||
|
||||
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
|
||||
|
||||
"web-features": ["web-features@3.37.0", "", {}, "sha512-47x5zchtpsvvH/EEQ0h3d3E9kZyFxFyk6LkmYHXDZjLF0RAa3OV2s8dMRqphp/PTxxrd/iZwz37dte6xWgV7mQ=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
@@ -5736,6 +5852,8 @@
|
||||
|
||||
"webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="],
|
||||
|
||||
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
@@ -5812,6 +5930,8 @@
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||
|
||||
"xml-naming": ["xml-naming@0.3.0", "", {}, "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
|
||||
@@ -6284,6 +6404,12 @@
|
||||
|
||||
"@opencode-ai/www/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="],
|
||||
|
||||
"@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="],
|
||||
|
||||
"@opentelemetry/sdk-trace/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="],
|
||||
|
||||
"@opentelemetry/sdk-trace/@opentelemetry/resources": ["@opentelemetry/resources@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA=="],
|
||||
|
||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"@opentui/core/marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
@@ -6316,6 +6442,8 @@
|
||||
|
||||
"@protobuf-ts/plugin/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
|
||||
|
||||
"@puppeteer/browsers/yargs": ["yargs@18.1.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@sentry/bundler-plugins/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
@@ -6326,6 +6454,18 @@
|
||||
|
||||
"@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A=="],
|
||||
|
||||
"@sentry/node/@sentry/core": ["@sentry/core@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA=="],
|
||||
|
||||
"@sentry/node-core/@sentry/core": ["@sentry/core@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA=="],
|
||||
|
||||
"@sentry/opentelemetry/@sentry/core": ["@sentry/core@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA=="],
|
||||
|
||||
"@sentry/server-utils/@sentry/core": ["@sentry/core@10.73.0", "", { "dependencies": { "@sentry/conventions": "^0.16.0" } }, "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA=="],
|
||||
|
||||
"@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
|
||||
|
||||
"@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
|
||||
@@ -6428,6 +6568,10 @@
|
||||
|
||||
"builder-util/js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
|
||||
|
||||
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"clean-css/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -6446,6 +6590,8 @@
|
||||
|
||||
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"configstore/dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
@@ -6490,6 +6636,8 @@
|
||||
|
||||
"electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
|
||||
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"esast-util-from-js/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
@@ -6534,6 +6682,8 @@
|
||||
|
||||
"htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"import-in-the-middle/es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"js-beautify/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"js-beautify/nopt": ["nopt@7.2.1", "", { "dependencies": { "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w=="],
|
||||
@@ -6542,10 +6692,16 @@
|
||||
|
||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||
|
||||
"matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
"lighthouse/devtools-protocol": ["devtools-protocol@0.0.1663043", "", {}, "sha512-33aOY3ZnBP1dgZsshgaL+/XlsQleiFZgyUaDtdZkEa1nbZhVY1MoDeWjk+wxg25fU924l1ZJfoGNmjjeA/5s1w=="],
|
||||
|
||||
"lighthouse/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"lighthouse/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
||||
|
||||
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"micromark-extension-mdxjs/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
@@ -6610,6 +6766,10 @@
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1666840", "", {}, "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg=="],
|
||||
|
||||
"puppeteer-core/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@@ -6678,6 +6838,8 @@
|
||||
|
||||
"unplugin/acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||
|
||||
"unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="],
|
||||
@@ -7194,6 +7356,8 @@
|
||||
|
||||
"@opencode-ai/www/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="],
|
||||
|
||||
"@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||
|
||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
@@ -7202,10 +7366,20 @@
|
||||
|
||||
"@pierre/trees/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||
|
||||
"@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"@sentry/cli/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@1.29.2", "", { "dependencies": { "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "oniguruma-to-es": "^2.2.0" } }, "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A=="],
|
||||
@@ -7370,6 +7544,8 @@
|
||||
|
||||
"builder-util/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"chrome-launcher/is-wsl/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -7406,6 +7582,8 @@
|
||||
|
||||
"electron/@electron/get/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"esbuild-plugin-copy/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
@@ -7428,6 +7606,12 @@
|
||||
|
||||
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
|
||||
|
||||
"lighthouse/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"lighthouse/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"lighthouse/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
@@ -8172,6 +8356,8 @@
|
||||
|
||||
"@opencode-ai/www/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
|
||||
|
||||
"@puppeteer/browsers/yargs/cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-T1JUv8gUrXavDI1HdVGqNRal4y2Bh5QavpR4DV+bJdA=",
|
||||
"aarch64-linux": "sha256-wlbWdEUVWAafgxaZis0Q+SAas8k6hu3s0C39CnMZ2gw=",
|
||||
"aarch64-darwin": "sha256-74rRue8KQWKzgMMb540beqMzp1DUShlPte6qklYCVTs=",
|
||||
"x86_64-darwin": "sha256-ihr3DoLd/4Lw6WbyXzSQXMP4hrVGk/UiRkO2aoQn8vg="
|
||||
"x86_64-linux": "sha256-7NBjLAaZRbirLuBJdQO9iwlonqUFKOkU8u6rh/e8Ij0=",
|
||||
"aarch64-linux": "sha256-877mqEw+JGTTftYSWvHOsKnFRQ0B5umxY5xW31Rs+ms=",
|
||||
"aarch64-darwin": "sha256-eaWQZfyQMefy5kn+Q9dAzOnXcwjwx7EEtDuzpocgHOQ=",
|
||||
"x86_64-darwin": "sha256-mg+Sr8h7d2dmrnOfjiX/ktmA3wuBg/iGZv9ooFvlERc="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ export const route = Route.make({
|
||||
|
||||
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
|
||||
|
||||
LLM `ProviderMetadata` is indexed by the selected `LanguageModel.route.id`. Use that same route ID when emitting usage, reasoning signatures, item IDs, and hosted-tool state and when reading them for continuation. There is no independently configurable metadata namespace. Reassigning a provider does not rename the route or its metadata; changing the route ID changes both. Provider-specific APIs with different replay formats must have distinct route IDs.
|
||||
|
||||
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider owns a small `Route.make(...)` composition instead of a protocol clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
|
||||
|
||||
When a provider supports multiple physical transports, selection remains execution policy below its semantic route. `OpenResponsesChannel.transport(...)` owns the provider-neutral Responses WebSocket concept: it prepares one final request, executes HTTP by default, strips WebSocket-disallowed fields, and passes a generic channel exchange to a per-call `WebSocketChannelExecutor` when supplied. Provider-specific Responses routes opt in with handshake and connection-age policy. `Route.streamPrepared` owns decoding and acknowledges channel completion only after successful full consumption.
|
||||
|
||||
@@ -534,35 +534,6 @@ Additional provider entrypoints include:
|
||||
- `@opencode-ai/ai/providers/cloudflare-ai-gateway`
|
||||
- `@opencode-ai/ai/providers/cloudflare-workers-ai`
|
||||
|
||||
### Route-keyed metadata
|
||||
|
||||
LLM messages, events, and usage retain provider-specific information under `providerMetadata[model.route.id]`. The provider ID identifies the provider; the route ID identifies its API and owns the metadata needed to replay that API's responses.
|
||||
|
||||
```ts
|
||||
const model = GoogleVertexMessages.configure({ project, accessToken }).model("claude-sonnet-4-5")
|
||||
const response = yield * LLM.generate(LLM.request({ model, prompt: "Think through this problem." }))
|
||||
const reasoning = response.message.content.find((part) => part.type === "reasoning")
|
||||
const signature = reasoning?.providerMetadata?.[model.route.id]?.signature
|
||||
// model.route.id === "google-vertex-messages"
|
||||
```
|
||||
|
||||
The former `providerMetadataKey` route option has been removed. Existing application-owned LLM metadata must use route keys instead of provider names, for example:
|
||||
|
||||
| API | Metadata key |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| OpenAI Chat / Responses | `openai-chat` / `openai-responses` |
|
||||
| Azure Chat / Responses | `azure-openai-chat` / `azure-openai-responses` |
|
||||
| Anthropic Messages | `anthropic-messages` |
|
||||
| Google Gemini | `gemini` |
|
||||
| Vertex Gemini / Chat / Responses / Messages | `google-vertex-gemini` / `google-vertex-chat` / `google-vertex-responses` / `google-vertex-messages` |
|
||||
| Bedrock Converse / Mantle Chat / Mantle Responses | `bedrock-converse` / `bedrock-mantle-chat` / `bedrock-mantle-responses` |
|
||||
| Groq / Mistral | `groq-chat` / `mistral-chat` |
|
||||
| xAI Chat / Responses | `xai-chat` / `xai-responses` |
|
||||
| Baseten / Cerebras / DeepInfra / DeepSeek / Fireworks / TogetherAI | `baseten-chat` / `cerebras-chat` / `deepinfra-chat` / `deepseek-chat` / `fireworks-chat` / `togetherai-chat` |
|
||||
| Generic Chat / Responses | `openai-compatible-chat` / `openai-compatible-responses` |
|
||||
|
||||
OpenRouter and Cloudflare already use matching provider and route IDs. Generic Messages uses `anthropic-messages`. Custom routes use their own `id`.
|
||||
|
||||
## Provider options & HTTP overlays
|
||||
|
||||
Request options in order of stability:
|
||||
|
||||
@@ -455,7 +455,7 @@ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
||||
interface ParserState {
|
||||
readonly provider: LLMRequest["model"]["provider"]
|
||||
readonly compactions: Readonly<Record<number, string | null>>
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
readonly reasoningSignatures: Readonly<Record<number, string>>
|
||||
readonly usage?: Usage
|
||||
@@ -563,14 +563,14 @@ const serverToolResultType = (name: string): AnthropicServerToolResultType | und
|
||||
|
||||
const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (
|
||||
part: ToolResultPart,
|
||||
routeID: string,
|
||||
providerMetadataKey: string,
|
||||
) {
|
||||
const wireType = serverToolResultType(part.name)
|
||||
if (!wireType)
|
||||
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
|
||||
// Prefer the provider-owned replay payload; fall back to the result value for
|
||||
// histories constructed directly from provider events.
|
||||
const payload = part.providerMetadata?.[routeID]?.["result"] ?? part.result.value
|
||||
const payload = part.providerMetadata?.[providerMetadataKey]?.["result"] ?? part.result.value
|
||||
return {
|
||||
type: wireType,
|
||||
tool_use_id: scrubToolCallID(part.id),
|
||||
@@ -857,7 +857,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
breakpoints: Cache.Breakpoints,
|
||||
) {
|
||||
const messages: AnthropicMessage[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
|
||||
for (const [index, message] of request.messages.entries()) {
|
||||
if (message.role === "system") {
|
||||
@@ -911,8 +911,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
if (part.type === "reasoning") {
|
||||
// A signature marks visible thinking; only signature-less parts carrying
|
||||
// redactedData round-trip as opaque redacted_thinking blocks.
|
||||
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, routeID)
|
||||
const redactedData = redactedDataFromMetadata(part.providerMetadata, routeID)
|
||||
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, providerMetadataKey)
|
||||
const redactedData = redactedDataFromMetadata(part.providerMetadata, providerMetadataKey)
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ type: "redacted_thinking", data: redactedData })
|
||||
continue
|
||||
@@ -941,7 +941,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-result" && part.providerExecuted) {
|
||||
content.push(yield* lowerServerToolResult(part, routeID))
|
||||
content.push(yield* lowerServerToolResult(part, providerMetadataKey))
|
||||
continue
|
||||
}
|
||||
return yield* invalid(
|
||||
@@ -1148,7 +1148,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
|
||||
// inclusive `inputTokens` the rest of the contract expects. Extended
|
||||
// thinking tokens are included in `output_tokens`; newer responses also
|
||||
// expose that subset through `output_tokens_details.thinking_tokens`.
|
||||
const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: AnthropicUsage | undefined, providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const iterations = usage.iterations?.length ? usage.iterations : [usage]
|
||||
const last = usage.iterations?.at(-1)
|
||||
@@ -1175,7 +1175,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | u
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: ProviderShared.sumTokens(...iterations.map((item) => item.output_tokens_details?.thinking_tokens)),
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1184,7 +1184,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | u
|
||||
// field prefers `right` when defined, falls back to `left`. `inputTokens` is
|
||||
// recomputed from the merged breakdown so the inclusive total stays
|
||||
// consistent with `nonCached + cacheRead + cacheWrite`.
|
||||
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, routeID: string) => {
|
||||
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, providerMetadataKey: string) => {
|
||||
if (!left) return right
|
||||
if (!right) return left
|
||||
const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
|
||||
@@ -1203,7 +1203,9 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined, routeID:
|
||||
reasoningTokens,
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
|
||||
providerMetadata: {
|
||||
[routeID]: mergeJsonRecords(left.providerMetadata?.[routeID], right.providerMetadata?.[routeID]) ?? {},
|
||||
[providerMetadataKey]:
|
||||
mergeJsonRecords(left.providerMetadata?.[providerMetadataKey], right.providerMetadata?.[providerMetadataKey]) ??
|
||||
{},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1221,7 +1223,7 @@ const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> =
|
||||
|
||||
const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
|
||||
|
||||
const serverToolResultEvent = (block: AnthropicStreamBlock, routeID: string): LLMEvent | undefined => {
|
||||
const serverToolResultEvent = (block: AnthropicStreamBlock, providerMetadataKey: string): LLMEvent | undefined => {
|
||||
if (!block.type || !isServerToolResultType(block.type)) return undefined
|
||||
const errorPayload =
|
||||
typeof block.content === "object" && block.content !== null && "type" in block.content
|
||||
@@ -1235,7 +1237,7 @@ const serverToolResultEvent = (block: AnthropicStreamBlock, routeID: string): LL
|
||||
providerExecuted: true,
|
||||
// The complete payload is irreducible provider replay state: subsequent
|
||||
// stateless requests must round-trip the typed result block verbatim.
|
||||
providerMetadata: providerMetadata(routeID, { blockType: block.type, result: block.content }),
|
||||
providerMetadata: providerMetadata(providerMetadataKey, { blockType: block.type, result: block.content }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1244,8 +1246,8 @@ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
const NO_EVENTS: StepResult["1"] = []
|
||||
|
||||
const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
|
||||
const usage = mapUsage(event.message?.usage, state.routeID)
|
||||
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.routeID) } : state, NO_EVENTS]
|
||||
const usage = mapUsage(event.message?.usage, state.providerMetadataKey)
|
||||
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.providerMetadataKey) } : state, NO_EVENTS]
|
||||
}
|
||||
|
||||
const onContentBlockStart = (
|
||||
@@ -1297,7 +1299,9 @@ const onContentBlockStart = (
|
||||
const events: LLMEvent[] = []
|
||||
const id = `reasoning-${event.index ?? 0}`
|
||||
const metadata =
|
||||
block.signature === undefined ? undefined : providerMetadata(state.routeID, { signature: block.signature })
|
||||
block.signature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { signature: block.signature })
|
||||
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, metadata)
|
||||
return [
|
||||
{
|
||||
@@ -1326,14 +1330,14 @@ const onContentBlockStart = (
|
||||
state.lifecycle,
|
||||
events,
|
||||
`reasoning-${event.index ?? 0}`,
|
||||
providerMetadata(state.routeID, { redactedData: block.data }),
|
||||
providerMetadata(state.providerMetadataKey, { redactedData: block.data }),
|
||||
),
|
||||
},
|
||||
events,
|
||||
]
|
||||
}
|
||||
|
||||
const result = serverToolResultEvent(block, state.routeID)
|
||||
const result = serverToolResultEvent(block, state.providerMetadataKey)
|
||||
if (!result) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
|
||||
@@ -1434,7 +1438,7 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
|
||||
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
|
||||
events,
|
||||
`reasoning-${event.index}`,
|
||||
signature === undefined ? undefined : providerMetadata(state.routeID, { signature }),
|
||||
signature === undefined ? undefined : providerMetadata(state.providerMetadataKey, { signature }),
|
||||
)
|
||||
events.push(...resultEvents)
|
||||
const reasoningSignatures = { ...state.reasoningSignatures }
|
||||
@@ -1446,7 +1450,7 @@ const onMessageDelta = (
|
||||
state: ParserState,
|
||||
event: AnthropicEvent & { readonly delta?: AnthropicStreamDelta },
|
||||
): StepResult => {
|
||||
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.routeID), state.routeID)
|
||||
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.providerMetadataKey), state.providerMetadataKey)
|
||||
const pendingFinish = (() => {
|
||||
const stopReason = event.delta?.stop_reason
|
||||
if (stopReason === null || stopReason === undefined) return state.pendingFinish
|
||||
@@ -1455,7 +1459,7 @@ const onMessageDelta = (
|
||||
const finishMetadata =
|
||||
stopSequence === null || stopSequence === undefined
|
||||
? state.pendingFinish?.providerMetadata
|
||||
: providerMetadata(state.routeID, { stopSequence })
|
||||
: providerMetadata(state.providerMetadataKey, { stopSequence })
|
||||
return {
|
||||
reason: {
|
||||
normalized: mapFinishReason(stopReason),
|
||||
@@ -1483,7 +1487,12 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
|
||||
events.push(...result.events)
|
||||
const closed = Object.entries(state.reasoningSignatures).reduce(
|
||||
(current, [index, signature]) =>
|
||||
Lifecycle.reasoningEnd(current, events, `reasoning-${index}`, providerMetadata(state.routeID, { signature })),
|
||||
Lifecycle.reasoningEnd(
|
||||
current,
|
||||
events,
|
||||
`reasoning-${index}`,
|
||||
providerMetadata(state.providerMetadataKey, { signature }),
|
||||
),
|
||||
lifecycle,
|
||||
)
|
||||
const finished = Lifecycle.finish(closed, events, {
|
||||
@@ -1618,7 +1627,7 @@ export const protocol = Protocol.make({
|
||||
initial: (request) => ({
|
||||
provider: request.model.provider,
|
||||
compactions: {},
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
reasoningSignatures: {},
|
||||
lifecycle: Lifecycle.initial(),
|
||||
@@ -1661,6 +1670,7 @@ export const transport = <Body extends Pick<AnthropicMessagesBody, "messages" |
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "anthropic",
|
||||
providerMetadataKey: "anthropic",
|
||||
protocol,
|
||||
endpoint: Endpoint.path((input) => (input.request.model.provider === "anthropic" ? `${PATH}?beta=true` : PATH), {
|
||||
baseURL: DEFAULT_BASE_URL,
|
||||
|
||||
@@ -260,14 +260,14 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
|
||||
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
|
||||
|
||||
const reasoningSignature = (part: ReasoningPart, routeID: string) => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const reasoningSignature = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (part.encrypted !== undefined) return part.encrypted
|
||||
if (ProviderShared.isRecord(metadata) && typeof metadata.signature === "string") return metadata.signature
|
||||
}
|
||||
|
||||
const reasoningRedactedData = (part: ReasoningPart, routeID: string) => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const reasoningRedactedData = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string") return metadata.redactedData
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
breakpoints: BedrockCache.Breakpoints,
|
||||
) {
|
||||
const messages: BedrockMessage[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
|
||||
for (const message of request.messages) {
|
||||
if (message.role === "system") {
|
||||
@@ -372,8 +372,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const signature = reasoningSignature(part, routeID)
|
||||
const redactedData = reasoningRedactedData(part, routeID)
|
||||
const signature = reasoningSignature(part, providerMetadataKey)
|
||||
const redactedData = reasoningRedactedData(part, providerMetadataKey)
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ reasoningContent: { redactedContent: redactedData } })
|
||||
continue
|
||||
@@ -485,7 +485,7 @@ const mapFinishReason = (reason: string): FinishReason => {
|
||||
|
||||
// AWS reports inputTokens separately from cache reads and writes.
|
||||
// Bedrock does not break reasoning out of outputTokens for current models.
|
||||
const mapUsage = (usage: BedrockUsageSchema | undefined, routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: BedrockUsageSchema | undefined, providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const inputTokens = ProviderShared.sumTokens(
|
||||
usage.inputTokens,
|
||||
@@ -499,12 +499,12 @@ const mapUsage = (usage: BedrockUsageSchema | undefined, routeID: string): Usage
|
||||
cacheReadInputTokens: usage.cacheReadInputTokens,
|
||||
cacheWriteInputTokens: usage.cacheWriteInputTokens,
|
||||
totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
// Bedrock splits the finish into `messageStop` (carries `stopReason`) and
|
||||
// `metadata` (carries usage). Hold both in state so `onHalt` can emit exactly
|
||||
@@ -587,8 +587,8 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
})()
|
||||
const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks)
|
||||
const metadata = (() => {
|
||||
if (reasoning.signature) return providerMetadata(state.routeID, { signature: reasoning.signature })
|
||||
if (redactedData !== undefined) return providerMetadata(state.routeID, { redactedData })
|
||||
if (reasoning.signature) return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
|
||||
if (redactedData !== undefined) return providerMetadata(state.providerMetadataKey, { redactedData })
|
||||
})()
|
||||
const lifecycle = (() => {
|
||||
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
|
||||
@@ -640,10 +640,10 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
if (resultEvents.length) return Lifecycle.stepStart(state.lifecycle, events)
|
||||
const metadata = (() => {
|
||||
const signature = state.reasoningSignatures[index]
|
||||
if (signature) return providerMetadata(state.routeID, { signature })
|
||||
if (signature) return providerMetadata(state.providerMetadataKey, { signature })
|
||||
const redactedContent = state.reasoningRedactedContent[index]
|
||||
if (redactedContent)
|
||||
return providerMetadata(state.routeID, {
|
||||
return providerMetadata(state.providerMetadataKey, {
|
||||
redactedData: encodeRedactedContent(redactedContent),
|
||||
})
|
||||
})()
|
||||
@@ -697,7 +697,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
}
|
||||
|
||||
if (event.metadata) {
|
||||
const usage = mapUsage(event.metadata.usage, state.routeID) ?? state.usage
|
||||
const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.usage
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
@@ -757,7 +757,7 @@ export const protocol = Protocol.make({
|
||||
stream: {
|
||||
event: BedrockEvent,
|
||||
initial: (request) => ({
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
finishReason: undefined,
|
||||
usage: undefined,
|
||||
@@ -774,6 +774,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "bedrock",
|
||||
providerMetadataKey: "bedrock",
|
||||
protocol,
|
||||
// Bedrock's URL embeds the region in the route endpoint host and the
|
||||
// validated modelId in the path. We read the validated body so the URL
|
||||
|
||||
@@ -232,7 +232,7 @@ type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent>
|
||||
|
||||
interface ParserState {
|
||||
readonly route: string
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly finishReason?: string
|
||||
readonly hasToolCalls: boolean
|
||||
readonly promptFeedback?: GeminiPromptFeedback
|
||||
@@ -309,7 +309,7 @@ const lowerToolCall = (part: ToolCallPart, omitIds: boolean, metadataKey: string
|
||||
|
||||
const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) {
|
||||
const contents: GeminiContent[] = []
|
||||
const metadataKey = request.model.route.id
|
||||
const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const omitCallIds = omitsFunctionCallIds(request.model.id)
|
||||
const legacyToolMedia = routesLegacyToolMedia(request.model.id)
|
||||
let pendingMedia: GeminiInlineDataPart[] | undefined
|
||||
@@ -583,7 +583,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.reasoningId,
|
||||
state.reasoningSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { thoughtSignature: state.reasoningSignature }),
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
|
||||
)
|
||||
if (state.textId !== undefined)
|
||||
lifecycle = Lifecycle.textEnd(
|
||||
@@ -592,7 +592,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.textId,
|
||||
state.textSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { thoughtSignature: state.textSignature }),
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
|
||||
)
|
||||
Lifecycle.finish(lifecycle, events, {
|
||||
reason: {
|
||||
@@ -604,7 +604,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
providerMetadata:
|
||||
state.promptFeedback === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.routeID, { promptFeedback: state.promptFeedback }),
|
||||
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
||||
})
|
||||
return events
|
||||
}
|
||||
@@ -634,7 +634,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
const nextState = {
|
||||
...state,
|
||||
promptFeedback: event.promptFeedback ?? state.promptFeedback,
|
||||
usage: event.usageMetadata ? (mapUsage(event.usageMetadata, state.routeID) ?? state.usage) : state.usage,
|
||||
usage: event.usageMetadata
|
||||
? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
|
||||
: state.usage,
|
||||
}
|
||||
const candidate = event.candidates?.[0]
|
||||
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
|
||||
@@ -690,7 +692,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
textId,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
: undefined,
|
||||
)
|
||||
textId = undefined
|
||||
textSignature = undefined
|
||||
@@ -704,7 +708,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
reasoningId,
|
||||
part.text,
|
||||
signature ? providerMetadata(state.routeID, { thoughtSignature: signature }) : undefined,
|
||||
signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined,
|
||||
)
|
||||
continue
|
||||
}
|
||||
@@ -713,7 +717,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
reasoningSignature = undefined
|
||||
@@ -727,7 +733,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
textId,
|
||||
part.text,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
)
|
||||
textSignature = undefined
|
||||
continue
|
||||
@@ -748,7 +754,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
reasoningSignature = undefined
|
||||
@@ -758,7 +766,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
textId,
|
||||
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
)
|
||||
textId = undefined
|
||||
textSignature = undefined
|
||||
@@ -770,7 +778,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
name: part.functionCall.name,
|
||||
input,
|
||||
providerMetadata: part.thoughtSignature
|
||||
? providerMetadata(state.routeID, { thoughtSignature: part.thoughtSignature })
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
@@ -813,7 +821,7 @@ export const protocol = Protocol.make({
|
||||
event: Protocol.jsonEvent(GeminiEvent),
|
||||
initial: (request) => ({
|
||||
route: `${request.model.provider}/${request.model.route.id}`,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
hasToolCalls: false,
|
||||
lifecycle: Lifecycle.initial(),
|
||||
nextReasoningId: 0,
|
||||
@@ -827,6 +835,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "google",
|
||||
providerMetadataKey: "google",
|
||||
protocol,
|
||||
// Gemini's path embeds the model id and pins SSE framing at the URL level.
|
||||
endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, {
|
||||
|
||||
@@ -287,10 +287,9 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
message: LLMRequest["messages"][number],
|
||||
normalizeID: (id: string) => string,
|
||||
prefix: boolean,
|
||||
routeID: string,
|
||||
) {
|
||||
const structured = message.content.some(
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.[routeID]?.thinking),
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
|
||||
)
|
||||
const content: Array<Schema.Schema.Type<typeof MistralTextContent> | MistralThinkingContent> = []
|
||||
const text: string[] = []
|
||||
@@ -302,7 +301,7 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const native = part.providerMetadata?.[routeID]?.thinking
|
||||
const native = part.providerMetadata?.mistral?.thinking
|
||||
if (structured && isMistralThinkingContent(native)) content.push(native)
|
||||
else if (structured) content.push({ type: "text", text: part.text })
|
||||
else text.push(part.text)
|
||||
@@ -379,22 +378,13 @@ const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request:
|
||||
if (message.role === "assistant") {
|
||||
const hasToolCalls = message.content.some((part) => part.type === "tool-call")
|
||||
const hasNativeThinking = message.content.some(
|
||||
(part) =>
|
||||
part.type === "reasoning" &&
|
||||
isMistralThinkingContent(part.providerMetadata?.[request.model.route.id]?.thinking),
|
||||
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
|
||||
)
|
||||
const text = message.content
|
||||
.flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : []))
|
||||
.join("")
|
||||
if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue
|
||||
messages.push(
|
||||
yield* lowerAssistant(
|
||||
message,
|
||||
normalizeID,
|
||||
!hasToolCalls && message === request.messages.at(-1),
|
||||
request.model.route.id,
|
||||
),
|
||||
)
|
||||
messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1)))
|
||||
continue
|
||||
}
|
||||
messages.push(...(yield* lowerToolResults(message, normalizeID)))
|
||||
@@ -465,7 +455,6 @@ interface ActiveContent {
|
||||
}
|
||||
|
||||
export interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly tools: ToolStream.State<ToolKey>
|
||||
readonly pendingTools: Partial<Record<ToolKey, PendingTool>>
|
||||
readonly toolIDs: ReadonlyMap<string, string>
|
||||
@@ -480,7 +469,7 @@ export interface ParserState {
|
||||
readonly finishReason?: FinishReasonDetails
|
||||
}
|
||||
|
||||
const mapUsage = (usage: MistralEvent["usage"], routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: MistralEvent["usage"]): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const input = usage.prompt_tokens ?? undefined
|
||||
const reported =
|
||||
@@ -496,7 +485,7 @@ const mapUsage = (usage: MistralEvent["usage"], routeID: string): Usage | undefi
|
||||
nonCachedInputTokens: ProviderShared.subtractTokens(input, cached),
|
||||
cacheReadInputTokens: cached,
|
||||
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { mistral: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -528,7 +517,7 @@ const thinkingUnits = (value: unknown): ReadonlyArray<MistralThinkingUnit> => {
|
||||
const thinkingText = (thinking: ReadonlyArray<MistralThinkingUnit>) =>
|
||||
thinking.flatMap((unit) => (typeof unit.text === "string" ? [unit.text] : [])).join("")
|
||||
|
||||
const thinkingMetadata = (thinking: MistralThinkingContent, routeID: string) => ({ [routeID]: { thinking } })
|
||||
const thinkingMetadata = (thinking: MistralThinkingContent) => ({ mistral: { thinking } })
|
||||
|
||||
const closeActive = (state: ParserState, events: LLMEvent[]) => {
|
||||
if (!state.active) return state
|
||||
@@ -539,7 +528,7 @@ const closeActive = (state: ParserState, events: LLMEvent[]) => {
|
||||
state.lifecycle,
|
||||
events,
|
||||
state.active.id,
|
||||
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }, state.routeID),
|
||||
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }),
|
||||
thinkingText(state.active.thinking?.thinking ?? []),
|
||||
)
|
||||
return { ...state, lifecycle, active: undefined }
|
||||
@@ -572,14 +561,8 @@ const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOut
|
||||
...current,
|
||||
lifecycle:
|
||||
text.length > 0
|
||||
? Lifecycle.reasoningDelta(
|
||||
current.lifecycle,
|
||||
events,
|
||||
active.id,
|
||||
text,
|
||||
thinkingMetadata(thinking, state.routeID),
|
||||
)
|
||||
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking, state.routeID)),
|
||||
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking))
|
||||
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)),
|
||||
active: { ...active, thinking },
|
||||
nextContent: current.active ? current.nextContent : current.nextContent + 1,
|
||||
}
|
||||
@@ -700,7 +683,7 @@ const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event:
|
||||
})
|
||||
}
|
||||
const events: LLMEvent[] = []
|
||||
const usage = mapUsage(event.usage, state.routeID) ?? state.usage
|
||||
const usage = mapUsage(event.usage) ?? state.usage
|
||||
if (state.finishReason) {
|
||||
if (hasLateContent(event))
|
||||
return yield* ProviderShared.eventError(
|
||||
@@ -778,8 +761,7 @@ export const protocol = Protocol.make({
|
||||
body: { schema: MistralBody, from: fromRequest },
|
||||
stream: {
|
||||
event: MistralStreamEvent,
|
||||
initial: (request): ParserState => ({
|
||||
routeID: request.model.route.id,
|
||||
initial: (): ParserState => ({
|
||||
tools: ToolStream.empty<ToolKey>(),
|
||||
pendingTools: {},
|
||||
toolIDs: new Map(),
|
||||
@@ -801,6 +783,7 @@ export const httpTransport = HttpTransport.sseJson.with<MistralBody>().with({ fr
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "mistral",
|
||||
providerMetadataKey: "mistral",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
|
||||
@@ -423,7 +423,7 @@ export interface ParserState {
|
||||
readonly completedCompactions: ReadonlySet<string>
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<string>
|
||||
readonly hasFunctionCall: boolean
|
||||
readonly lifecycle: Lifecycle.State
|
||||
@@ -476,15 +476,15 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
|
||||
|
||||
// Server-issued item ids need a nonempty prefix and suffix, but the prefix is
|
||||
// provider-defined and does not necessarily identify the item's semantic type.
|
||||
const itemID = (providerMetadata: ProviderMetadata | undefined, routeID: string) => {
|
||||
const metadata = providerMetadata?.[routeID]
|
||||
const itemID = (providerMetadata: ProviderMetadata | undefined, providerMetadataKey: string) => {
|
||||
const metadata = providerMetadata?.[providerMetadataKey]
|
||||
if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string") return undefined
|
||||
const separator = metadata.itemId.indexOf("_")
|
||||
return separator > 0 && separator < metadata.itemId.length - 1 ? metadata.itemId : undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart, routeID: string): OpenResponsesInputItem => {
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => {
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
return {
|
||||
type: "function_call",
|
||||
...(id === undefined ? {} : { id }),
|
||||
@@ -495,10 +495,10 @@ const lowerToolCall = (part: ToolCallPart, routeID: string): OpenResponsesInputI
|
||||
}
|
||||
}
|
||||
|
||||
const lowerReasoning = (part: ReasoningPart, routeID: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[routeID]
|
||||
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (!ProviderShared.isRecord(metadata)) return undefined
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const encryptedContent =
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
@@ -521,7 +521,7 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
|
||||
const providerMedia = adapter.lowerMedia?.({ part, media, request })
|
||||
if (providerMedia) return providerMedia
|
||||
const detail = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))(
|
||||
part.providerMetadata?.[request.model.route.id]?.detail,
|
||||
part.providerMetadata?.[metadataKey(request.model)]?.detail,
|
||||
)
|
||||
const url =
|
||||
typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
|
||||
@@ -608,12 +608,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
const input: LoweredInputItem[] = []
|
||||
const routeID = request.model.route.id
|
||||
const providerMetadataKey = metadataKey(request.model)
|
||||
|
||||
for (const message of request.messages) {
|
||||
const metadata = yield* ProviderShared.validateWith(
|
||||
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
|
||||
)(message.providerMetadata?.[routeID])
|
||||
)(message.providerMetadata?.[providerMetadataKey])
|
||||
if (message.role === "system") {
|
||||
input.push({
|
||||
role: "developer",
|
||||
@@ -638,8 +638,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
const groups = content.reduce<
|
||||
Array<{ id: string | undefined; phase: MessagePhase | null | undefined; parts: TextPart[] }>
|
||||
>((groups, part) => {
|
||||
const partMetadata = part.providerMetadata?.[routeID]
|
||||
const id = itemID(part.providerMetadata, routeID) ?? metadata?.itemId
|
||||
const partMetadata = part.providerMetadata?.[providerMetadataKey]
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey) ?? metadata?.itemId
|
||||
const partPhase = messagePhase(partMetadata?.phase)
|
||||
const phase = partPhase === undefined ? metadata?.phase : partPhase
|
||||
const group = groups.at(-1)
|
||||
@@ -675,7 +675,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
flushText()
|
||||
const reasoning = lowerReasoning(part, routeID)
|
||||
const reasoning = lowerReasoning(part, providerMetadataKey)
|
||||
if (!reasoning) continue
|
||||
const existing = reasoning.id === undefined ? undefined : reasoningItems[reasoning.id]
|
||||
if (existing) {
|
||||
@@ -691,12 +691,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
if (part.type === "tool-call") {
|
||||
flushText()
|
||||
if (part.providerExecuted === true) continue
|
||||
input.push(lowerToolCall(part, routeID))
|
||||
input.push(lowerToolCall(part, providerMetadataKey))
|
||||
continue
|
||||
}
|
||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||
flushText()
|
||||
const id = itemID(part.providerMetadata, routeID)
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const hosted =
|
||||
part.result.type !== "json"
|
||||
? undefined
|
||||
@@ -844,7 +844,7 @@ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (req
|
||||
// cached-read and cache-write subsets, and `output_tokens` (inclusive total)
|
||||
// with a `reasoning_tokens` subset. Pass the totals through and derive the
|
||||
// non-cached breakdown.
|
||||
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, routeID: string) => {
|
||||
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => {
|
||||
if (!usage) return undefined
|
||||
const cached = usage.input_tokens_details?.cached_tokens
|
||||
const cacheWrite = usage.input_tokens_details?.cache_write_tokens
|
||||
@@ -858,7 +858,7 @@ export const mapUsage = (usage: OpenResponsesUsage | null | undefined, routeID:
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: reasoning,
|
||||
totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -874,8 +874,10 @@ const mapFinishReason = (event: Event, hasFunctionCall: boolean): FinishReason =
|
||||
return hasFunctionCall ? "tool-calls" : "unknown"
|
||||
}
|
||||
|
||||
export const metadataKey = (model: LLMRequest["model"]) => model.route.providerMetadataKey ?? "openresponses"
|
||||
|
||||
export const providerMetadata = (state: ParserState, metadata: Record<string, unknown>): ProviderMetadata => ({
|
||||
[state.routeID]: metadata,
|
||||
[state.providerMetadataKey]: metadata,
|
||||
})
|
||||
|
||||
export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
@@ -1347,7 +1349,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
|
||||
normalized: mapFinishReason(event, current.hasFunctionCall),
|
||||
raw: event.response?.incomplete_details?.reason,
|
||||
},
|
||||
usage: mapUsage(event.response?.usage, current.routeID),
|
||||
usage: mapUsage(event.response?.usage, current.providerMetadataKey),
|
||||
providerMetadata:
|
||||
event.response?.id || event.response?.service_tier
|
||||
? providerMetadata(current, {
|
||||
@@ -1484,7 +1486,7 @@ export const initial = (request: LLMRequest, adapter: ProviderAdapter = BASE_ADA
|
||||
completedCompactions: new Set<string>(),
|
||||
id: adapter.id,
|
||||
name: adapter.name,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: metadataKey(request.model),
|
||||
hasFunctionCall: false,
|
||||
tools: ToolStream.empty<string>(),
|
||||
lifecycle: Lifecycle.initial(),
|
||||
|
||||
@@ -257,7 +257,7 @@ interface PendingToolDelta {
|
||||
}
|
||||
|
||||
export interface ParserState {
|
||||
readonly routeID: string
|
||||
readonly providerMetadataKey: string
|
||||
readonly tools: ToolStream.State<number>
|
||||
readonly pendingTools: Partial<Record<number, PendingToolDelta>>
|
||||
readonly toolCallEvents: ReadonlyArray<LLMEvent>
|
||||
@@ -329,17 +329,18 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
|
||||
const openAICompatibleReasoningContent = (native: unknown) =>
|
||||
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
|
||||
|
||||
const reasoningField = (part: ReasoningPart, routeID: string) => {
|
||||
const field = part.providerMetadata?.[routeID]?.reasoningField
|
||||
const reasoningField = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const field = part.providerMetadata?.[providerMetadataKey]?.reasoningField
|
||||
return typeof field === "string" ? field : undefined
|
||||
}
|
||||
|
||||
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, routeID: string) => {
|
||||
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, providerMetadataKey: string) => {
|
||||
const observed = parts.flatMap((part) => {
|
||||
const details = part.providerMetadata?.[routeID]?.reasoningDetails
|
||||
const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails
|
||||
return Array.isArray(details) ? details : []
|
||||
})
|
||||
if (parts.some((part) => Array.isArray(part.providerMetadata?.[routeID]?.reasoningDetails))) return observed
|
||||
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
|
||||
return observed
|
||||
if (isRecord(native) && Array.isArray(native.reasoning_details)) return native.reasoning_details
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
message: OpenAIChatRequestMessage,
|
||||
configuredField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
options: LoweringOptions & { readonly routeID: string },
|
||||
options: LoweringOptions & { readonly providerMetadataKey: string },
|
||||
) {
|
||||
const content: TextPart[] = []
|
||||
const reasoning: ReasoningPart[] = []
|
||||
@@ -393,13 +394,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
|
||||
}
|
||||
}
|
||||
const text = reasoning.map((part) => part.text).join("")
|
||||
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.routeID)
|
||||
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.providerMetadataKey)
|
||||
const observedField = reasoning
|
||||
.map((part) => reasoningField(part, options.routeID))
|
||||
.map((part) => reasoningField(part, options.providerMetadataKey))
|
||||
.find((value) => value !== undefined)
|
||||
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible)
|
||||
const fullyStructured = reasoning.every((part) =>
|
||||
Array.isArray(part.providerMetadata?.[options.routeID]?.reasoningDetails),
|
||||
Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails),
|
||||
)
|
||||
const field = (() => {
|
||||
if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
|
||||
@@ -468,7 +469,7 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (
|
||||
message: OpenAIChatRequestMessage,
|
||||
reasoningField: string | undefined,
|
||||
requireReasoning: boolean,
|
||||
options: LoweringOptions & { readonly routeID: string },
|
||||
options: LoweringOptions & { readonly providerMetadataKey: string },
|
||||
) {
|
||||
if (message.role === "user") return [yield* lowerUserMessage(message, options)]
|
||||
if (message.role === "assistant")
|
||||
@@ -504,7 +505,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
|
||||
const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family))
|
||||
const lowering = {
|
||||
...options,
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
toolCallID: (id: string) => {
|
||||
if (mistral)
|
||||
return id
|
||||
@@ -832,7 +833,7 @@ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event
|
||||
// Providers differ on cache-hit location: OpenAI uses
|
||||
// `prompt_tokens_details.cached_tokens`, DeepSeek uses
|
||||
// `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"], routeID: string): Usage | undefined => {
|
||||
const mapUsage = (usage: OpenAIChatEvent["usage"], providerMetadataKey: string): Usage | undefined => {
|
||||
if (!usage) return undefined
|
||||
const input = usage.prompt_tokens ?? undefined
|
||||
const output = usage.completion_tokens ?? undefined
|
||||
@@ -851,7 +852,7 @@ const mapUsage = (usage: OpenAIChatEvent["usage"], routeID: string): Usage | und
|
||||
cacheWriteInputTokens: cacheWrite,
|
||||
reasoningTokens: reasoning,
|
||||
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
|
||||
providerMetadata: { [routeID]: usage },
|
||||
providerMetadata: { [providerMetadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -926,11 +927,11 @@ const conflictingDetailValue = (previous: unknown, current: unknown) =>
|
||||
previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current
|
||||
|
||||
const reasoningMetadata = (
|
||||
routeID: string,
|
||||
providerMetadataKey: string,
|
||||
field: ParserState["reasoningField"],
|
||||
details?: ReadonlyArray<unknown>,
|
||||
) => ({
|
||||
[routeID]: {
|
||||
[providerMetadataKey]: {
|
||||
...(field ? { reasoningField: field } : {}),
|
||||
...(details ? { reasoningDetails: details } : {}),
|
||||
},
|
||||
@@ -954,8 +955,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
// `choice.usage` instead of the top-level `usage` field.
|
||||
const choiceUsage = (choice as unknown as { usage?: OpenAIChatEvent["usage"] })?.usage
|
||||
const usage =
|
||||
mapUsage(event.usage, state.routeID) ??
|
||||
(choiceUsage ? mapUsage(choiceUsage, state.routeID) : undefined) ??
|
||||
mapUsage(event.usage, state.providerMetadataKey) ??
|
||||
(choiceUsage ? mapUsage(choiceUsage, state.providerMetadataKey) : undefined) ??
|
||||
state.usage
|
||||
const rawFinishReason = choice?.finish_reason
|
||||
const finishReason = rawFinishReason
|
||||
@@ -994,7 +995,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
|
||||
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
|
||||
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
|
||||
const deltaMetadata = reasoningMetadata(state.routeID, reasoningField)
|
||||
const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField)
|
||||
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
|
||||
if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
|
||||
else if (
|
||||
@@ -1083,7 +1084,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
|
||||
return [
|
||||
{
|
||||
routeID: state.routeID,
|
||||
providerMetadataKey: state.providerMetadataKey,
|
||||
tools: finished?.tools ?? tools,
|
||||
pendingTools,
|
||||
toolCallEvents: finished?.events ?? state.toolCallEvents,
|
||||
@@ -1127,7 +1128,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
|
||||
// Snapshot details at publish time so the emitted event never observes later
|
||||
// mutation of the accumulated `reasoningDetails` array.
|
||||
const metadata = reasoningMetadata(
|
||||
state.routeID,
|
||||
state.providerMetadataKey,
|
||||
state.reasoningField,
|
||||
state.reasoningDetailsObserved ? [...state.reasoningDetails] : undefined,
|
||||
)
|
||||
@@ -1137,7 +1138,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
|
||||
state.lifecycle,
|
||||
events,
|
||||
"reasoning-0",
|
||||
reasoningMetadata(state.routeID, state.reasoningField),
|
||||
reasoningMetadata(state.providerMetadataKey, state.reasoningField),
|
||||
)
|
||||
: state.lifecycle
|
||||
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
|
||||
@@ -1165,7 +1166,7 @@ export const protocol = Protocol.make({
|
||||
stream: {
|
||||
event: OpenAIChatStreamEvent,
|
||||
initial: (request) => ({
|
||||
routeID: request.model.route.id,
|
||||
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
tools: ToolStream.empty<number>(),
|
||||
pendingTools: {},
|
||||
toolCallEvents: [],
|
||||
@@ -1189,6 +1190,7 @@ export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>().with({
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "openai",
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
|
||||
@@ -15,6 +15,7 @@ export type OpenAICompatibleChatLanguageModelInput = RouteRoutedLanguageModelInp
|
||||
*/
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
providerMetadataKey: "openai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type OpenAICompatibleResponsesLanguageModelInput = RouteRoutedLanguageMod
|
||||
*/
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
providerMetadataKey: "openresponses",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path(OpenResponses.PATH),
|
||||
transport: OpenResponses.httpTransport,
|
||||
|
||||
@@ -313,6 +313,7 @@ export const route = Route.make({
|
||||
compact: { endpoint: ResponsesCompaction.make(adapter), trigger: ResponsesCheckpoint.make(checkpointBody) },
|
||||
id: ADAPTER,
|
||||
provider: "openai",
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint,
|
||||
auth,
|
||||
|
||||
@@ -93,7 +93,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
|
||||
result = new CompactionCheckpointResponse({
|
||||
checkpoint,
|
||||
responseID,
|
||||
usage: OpenResponses.mapUsage(event.response?.usage, request.model.route.id),
|
||||
usage: OpenResponses.mapUsage(event.response?.usage, OpenResponses.metadataKey(request.model)),
|
||||
})
|
||||
return [next, [LLMEvent.finish({ reason: { normalized: "stop" } })]] as const
|
||||
}),
|
||||
@@ -102,6 +102,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
|
||||
const route = Route.make({
|
||||
id: source.id,
|
||||
provider: source.provider,
|
||||
providerMetadataKey: source.providerMetadataKey,
|
||||
protocol,
|
||||
endpoint: source.endpoint,
|
||||
auth: source.auth,
|
||||
|
||||
@@ -125,7 +125,7 @@ export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =
|
||||
return yield* invalid("Compaction response did not contain a checkpoint")
|
||||
return new CompactionResponse({
|
||||
replacement: result.output.map((item) => toMessage(item, request.model)),
|
||||
usage: OpenResponses.mapUsage(result.usage, request.model.route.id),
|
||||
usage: OpenResponses.mapUsage(result.usage, OpenResponses.metadataKey(request.model)),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,7 +135,7 @@ function toMessage(item: (typeof Response.Type.output)[number], model: LLMReques
|
||||
CompactionPart.make({ provider: model.provider, id: item.id ?? undefined, encrypted: item.encrypted_content }),
|
||||
)
|
||||
|
||||
const key = model.route.id
|
||||
const key = OpenResponses.metadataKey(model)
|
||||
if (item.type === "reasoning") {
|
||||
const summary = item.summary.length ? item.summary : [{ text: "" }]
|
||||
return Message.assistant(
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const responsesRoute = Route.make({
|
||||
id: "bedrock-mantle-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "mantle",
|
||||
protocol: OpenAIResponses.protocol,
|
||||
endpoint: OpenAIResponses.route.endpoint,
|
||||
auth: OpenAIResponses.route.auth,
|
||||
@@ -46,6 +47,7 @@ const responsesRoute = Route.make({
|
||||
const chatRoute = OpenAIChat.route.with({
|
||||
id: "bedrock-mantle-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "mantle",
|
||||
})
|
||||
|
||||
export const routes = [responsesRoute, chatRoute]
|
||||
|
||||
@@ -45,6 +45,7 @@ const configuredRoute = (input: Config) => {
|
||||
return BedrockConverse.route.with({
|
||||
...rest,
|
||||
provider: id,
|
||||
providerMetadataKey: "bedrock",
|
||||
endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) },
|
||||
auth: BedrockAuth.resolveAuth({ apiKey, credentials, profile }, resolvedRegion, { mode: auth }),
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "baseten-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "baseten",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "cerebras-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "cerebras",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -53,6 +53,7 @@ const auth = (input: LanguageModelOptions) => {
|
||||
export const route = Route.make({
|
||||
id: "cloudflare-ai-gateway",
|
||||
provider: id,
|
||||
providerMetadataKey: "cloudflare-ai-gateway",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -35,6 +35,7 @@ export const baseURL = (input: WorkersAIURL) => {
|
||||
export const route = Route.make({
|
||||
id: "cloudflare-workers-ai",
|
||||
provider: id,
|
||||
providerMetadataKey: "cloudflare-workers-ai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "deepinfra-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "deepinfra",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "deepseek-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "deepseek",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "fireworks-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "fireworks",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions"),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-messages",
|
||||
provider: id,
|
||||
providerMetadataKey: "anthropic",
|
||||
protocol: Protocol.make({
|
||||
id: AnthropicMessages.protocol.id,
|
||||
body: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol: OpenResponses.protocol,
|
||||
endpoint: Endpoint.path(OpenResponses.PATH),
|
||||
transport: OpenResponses.httpTransport,
|
||||
|
||||
@@ -68,6 +68,7 @@ const protocol = {
|
||||
const route = Route.make({
|
||||
id: "google-vertex-gemini",
|
||||
provider: id,
|
||||
providerMetadataKey: "vertex",
|
||||
protocol,
|
||||
endpoint: Endpoint.path(({ request }) => {
|
||||
const model = String(request.model.id)
|
||||
|
||||
@@ -71,6 +71,7 @@ export const protocol = Protocol.make({
|
||||
export const route = Route.make({
|
||||
id: "groq-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "openai",
|
||||
protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -162,6 +162,7 @@ const bodyOptions = (input: unknown) => {
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Settings extends ProviderPackage.Settings {
|
||||
export const route = Route.make({
|
||||
id: "togetherai-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "togetherai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -32,12 +32,13 @@ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 24 * 60 * 1000
|
||||
|
||||
const responsesRoute = Route.make({
|
||||
compact: { endpoint: XAIResponses.compact },
|
||||
id: "xai-responses",
|
||||
id: "openai-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "xai",
|
||||
protocol: XAIResponses.protocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
transport: OpenResponsesChannel.transport({
|
||||
id: "xai-responses",
|
||||
id: "openai-responses",
|
||||
name: "xAI Responses",
|
||||
rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
|
||||
}),
|
||||
@@ -45,8 +46,9 @@ const responsesRoute = Route.make({
|
||||
})
|
||||
|
||||
const chatRoute = Route.make({
|
||||
id: "xai-chat",
|
||||
id: "openai-compatible-chat",
|
||||
provider: id,
|
||||
providerMetadataKey: "xai",
|
||||
protocol: OpenAIChat.protocol,
|
||||
endpoint: Endpoint.path("/chat/completions", { baseURL }),
|
||||
framing: OpenAIChat.framing,
|
||||
|
||||
@@ -42,9 +42,10 @@ export interface Route<
|
||||
Compact extends CompactionOperations | undefined = CompactionOperations | undefined,
|
||||
> {
|
||||
readonly compact: Compact
|
||||
/** Route identity and namespace for metadata emitted and replayed by this route. */
|
||||
readonly id: string
|
||||
readonly provider?: ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
readonly protocol: ProtocolID
|
||||
readonly endpoint: Endpoint.Definition<Body>
|
||||
readonly auth: Auth.Definition
|
||||
@@ -112,6 +113,7 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
|
||||
readonly compact?: CompactionOperations
|
||||
readonly id?: string
|
||||
readonly provider?: string | ProviderID
|
||||
readonly providerMetadataKey?: string
|
||||
readonly auth?: Auth.Definition
|
||||
readonly transport?: Transport<Body, Prepared, unknown>
|
||||
readonly endpoint?: EndpointPatch<Body>
|
||||
@@ -274,10 +276,12 @@ const resolveRequestOptions = (request: LLMRequest) => {
|
||||
|
||||
export interface MakeInput<Body, Frame, Event, State> {
|
||||
readonly compact?: CompactionOperations
|
||||
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
|
||||
/** Route id used in diagnostics and prepared request metadata. */
|
||||
readonly id: string
|
||||
/** Provider identity for route-owned model construction. */
|
||||
readonly provider?: string | ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
/** Semantic API contract — owns body construction, body schema, and parsing. */
|
||||
readonly protocol: Protocol<Body, Frame, Event, State>
|
||||
/** Where the request is sent. */
|
||||
@@ -294,10 +298,12 @@ export interface MakeInput<Body, Frame, Event, State> {
|
||||
|
||||
export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
|
||||
readonly compact?: CompactionOperations
|
||||
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
|
||||
/** Route id used in diagnostics and prepared request metadata. */
|
||||
readonly id: string
|
||||
/** Provider identity for route-owned model construction. */
|
||||
readonly provider?: string | ProviderID
|
||||
/** ProviderMetadata namespace emitted and consumed by this route. */
|
||||
readonly providerMetadataKey?: string
|
||||
/** Semantic API contract — owns body construction, body schema, and parsing. */
|
||||
readonly protocol: Protocol<Body, Frame, Event, State>
|
||||
/** Where the request is sent. */
|
||||
@@ -370,6 +376,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
compact: routeInput.compact,
|
||||
id: routeInput.id,
|
||||
provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider),
|
||||
providerMetadataKey: routeInput.providerMetadataKey,
|
||||
protocol: protocol.id,
|
||||
endpoint: routeInput.endpoint,
|
||||
auth: routeInput.auth ?? Auth.none,
|
||||
@@ -378,12 +385,17 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
defaults: routeInput.defaults ?? {},
|
||||
body: protocol.body,
|
||||
with: (patch: RoutePatch<Body, Prepared>) => {
|
||||
const { compact, id, provider, auth, transport, endpoint, ...defaults } = patch
|
||||
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
|
||||
return build({
|
||||
...routeInput,
|
||||
compact: "compact" in patch ? compact : routeInput.compact,
|
||||
id: id ?? routeInput.id,
|
||||
provider: provider ?? routeInput.provider,
|
||||
providerMetadataKey:
|
||||
providerMetadataKey ??
|
||||
(provider !== undefined && String(provider) !== String(routeInput.provider)
|
||||
? String(provider)
|
||||
: routeInput.providerMetadataKey),
|
||||
auth: auth ?? routeInput.auth,
|
||||
endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
|
||||
transport: (transport as Transport<Body, Prepared, Frame> | undefined) ?? routeInput.transport,
|
||||
@@ -524,6 +536,7 @@ export function make<Body, Prepared, Frame, Event, State>(
|
||||
compact: input.compact,
|
||||
id: input.id,
|
||||
provider: input.provider,
|
||||
providerMetadataKey: input.providerMetadataKey,
|
||||
protocol,
|
||||
endpoint: input.endpoint,
|
||||
auth: input.auth,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ProviderID } from "./ids.js"
|
||||
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
|
||||
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
|
||||
|
||||
/** Provider-specific data; LLM routes emit and replay it under their route ID. */
|
||||
export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
identifier: "LLM.ProviderMetadata",
|
||||
})
|
||||
|
||||
@@ -66,14 +66,14 @@ export function continuationRequest(input: {
|
||||
firstAssistant.push({
|
||||
type: "reasoning",
|
||||
text: "I inspected the previous turn.",
|
||||
providerMetadata: { [input.model.route.id]: { signature: "sig_continuation_1" } },
|
||||
providerMetadata: { anthropic: { signature: "sig_continuation_1" } },
|
||||
})
|
||||
if (features.has("encrypted-reasoning"))
|
||||
firstAssistant.push({
|
||||
type: "reasoning",
|
||||
text: "I inspected the previous turn.",
|
||||
providerMetadata: {
|
||||
[input.model.route.id]: {
|
||||
openai: {
|
||||
itemId: "rs_continuation_1",
|
||||
reasoningEncryptedContent: "encrypted-continuation-state",
|
||||
},
|
||||
|
||||
@@ -69,8 +69,8 @@ describe("public exports", () => {
|
||||
expect(XAI.model).toBeFunction()
|
||||
expect(XAI.provider.responses).toBe(XAI.responses)
|
||||
expect(XAI.provider.chat).toBe(XAI.chat)
|
||||
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("xai-responses")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("xai-chat")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
|
||||
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
|
||||
})
|
||||
|
||||
test("protocol barrels expose supported low-level routes", () => {
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("Anthropic Messages route", () => {
|
||||
Message.assistant([{ type: "reasoning", text: "" }]),
|
||||
Message.assistant([
|
||||
{ type: "text", text: "" },
|
||||
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
]),
|
||||
],
|
||||
cache: "none",
|
||||
@@ -753,11 +753,7 @@ describe("Anthropic Messages route", () => {
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
|
||||
},
|
||||
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
@@ -835,12 +831,8 @@ describe("Anthropic Messages route", () => {
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } } },
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "visible",
|
||||
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
|
||||
},
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } },
|
||||
{ type: "reasoning", text: "visible", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
@@ -920,28 +912,20 @@ describe("Anthropic Messages route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toMatchObject([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Thinking.",
|
||||
providerMetadata: { "custom-anthropic-messages": { signature: "custom_sig" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { "custom-anthropic-messages": { redactedData: "custom_redacted" } },
|
||||
},
|
||||
{ type: "reasoning", text: "Thinking.", providerMetadata: { "custom-anthropic": { signature: "custom_sig" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { "custom-anthropic": { redactedData: "custom_redacted" } } },
|
||||
{ type: "tool-call", id: "custom_tool", providerExecuted: true },
|
||||
{
|
||||
type: "tool-result",
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "custom-anthropic-messages": { blockType: "web_search_tool_result", result } },
|
||||
providerMetadata: { "custom-anthropic": { blockType: "web_search_tool_result", result } },
|
||||
},
|
||||
])
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
"custom-anthropic-messages": { input_tokens: 5, custom_start: true, output_tokens: 2, custom_terminal: true },
|
||||
"custom-anthropic": { input_tokens: 5, custom_start: true, output_tokens: 2, custom_terminal: true },
|
||||
})
|
||||
expect(response.events.at(-1)).toMatchObject({
|
||||
providerMetadata: { "custom-anthropic-messages": { stopSequence: "custom_stop" } },
|
||||
providerMetadata: { "custom-anthropic": { stopSequence: "custom_stop" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
@@ -990,13 +974,13 @@ describe("Anthropic Messages route", () => {
|
||||
const reasoningEnds = response.events.filter((event) => event.type === "reasoning-end")
|
||||
expect(reasoningEnds).toHaveLength(1)
|
||||
expect(reasoningEnds[0]).toMatchObject({
|
||||
providerMetadata: { "custom-anthropic-messages": { signature: "sig_1" } },
|
||||
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
|
||||
})
|
||||
expect(response.message.content).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Reasoning.",
|
||||
providerMetadata: { "custom-anthropic-messages": { signature: "sig_1" } },
|
||||
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1040,18 +1024,18 @@ describe("Anthropic Messages route", () => {
|
||||
totalTokens: 8,
|
||||
})
|
||||
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
|
||||
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
|
||||
providerMetadata: { anthropic: { signature: "sig_1" } },
|
||||
})
|
||||
expect(response.events.filter((event) => event.type === "reasoning-end")).toHaveLength(1)
|
||||
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined()
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "text", text: "Hello!" },
|
||||
{ type: "reasoning", text: "thinking", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
|
||||
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
])
|
||||
expect(response.events.at(-1)).toMatchObject({
|
||||
type: "finish",
|
||||
reason: { normalized: "stop", raw: "end_turn" },
|
||||
providerMetadata: { "anthropic-messages": { stopSequence: "\n\nHuman:" } },
|
||||
providerMetadata: { anthropic: { stopSequence: "\n\nHuman:" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1080,13 +1064,13 @@ describe("Anthropic Messages route", () => {
|
||||
expect(response.events.find((event) => event.type === "step-finish")).toMatchObject({
|
||||
reason: { normalized: "stop", raw: "end_turn" },
|
||||
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
|
||||
providerMetadata: { "anthropic-messages": { stopSequence: "X" } },
|
||||
providerMetadata: { anthropic: { stopSequence: "X" } },
|
||||
})
|
||||
expect(response.events.at(-1)).toMatchObject({
|
||||
type: "finish",
|
||||
reason: { normalized: "stop", raw: "end_turn" },
|
||||
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
|
||||
providerMetadata: { "anthropic-messages": { stopSequence: "X" } },
|
||||
providerMetadata: { anthropic: { stopSequence: "X" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1312,7 +1296,7 @@ describe("Anthropic Messages route", () => {
|
||||
reasoningTokens: 3,
|
||||
totalTokens: 15,
|
||||
providerMetadata: {
|
||||
"anthropic-messages": {
|
||||
anthropic: {
|
||||
input_tokens: null,
|
||||
cache_read_input_tokens: 2,
|
||||
service_tier: "standard",
|
||||
@@ -1353,7 +1337,7 @@ describe("Anthropic Messages route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
])
|
||||
|
||||
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
|
||||
@@ -1384,10 +1368,10 @@ describe("Anthropic Messages route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
|
||||
])
|
||||
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
|
||||
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
|
||||
providerMetadata: { anthropic: { signature: "sig_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1451,10 +1435,10 @@ describe("Anthropic Messages route", () => {
|
||||
const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)))
|
||||
|
||||
expect(response.events.find((event) => event.type === "reasoning-start")).toMatchObject({
|
||||
providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } },
|
||||
providerMetadata: { anthropic: { redactedData: "opaque_1" } },
|
||||
})
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } },
|
||||
{ type: "text", text: "Hello" },
|
||||
])
|
||||
}),
|
||||
@@ -1598,7 +1582,7 @@ describe("Anthropic Messages route", () => {
|
||||
cacheReadInputTokens: undefined,
|
||||
cacheWriteInputTokens: undefined,
|
||||
totalTokens: 6,
|
||||
providerMetadata: { "anthropic-messages": { input_tokens: 5, output_tokens: 1 } },
|
||||
providerMetadata: { anthropic: { input_tokens: 5, output_tokens: 1 } },
|
||||
})
|
||||
|
||||
expect(response.toolCalls).toEqual([
|
||||
@@ -1907,7 +1891,7 @@ describe("Anthropic Messages route", () => {
|
||||
// The complete payload rides in provider metadata as irreducible replay
|
||||
// state for later stateless requests.
|
||||
providerMetadata: {
|
||||
"anthropic-messages": {
|
||||
anthropic: {
|
||||
blockType: "web_search_tool_result",
|
||||
result: [{ type: "web_search_result", url: "https://example.com", title: "Example" }],
|
||||
},
|
||||
|
||||
@@ -837,7 +837,7 @@ describe("Bedrock Converse route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Partial thought",
|
||||
providerMetadata: signature === undefined ? undefined : { "bedrock-converse": { signature } },
|
||||
providerMetadata: signature === undefined ? undefined : { bedrock: { signature } },
|
||||
cache: new CacheHint({ type: "ephemeral" }),
|
||||
},
|
||||
]),
|
||||
@@ -865,7 +865,7 @@ describe("Bedrock Converse route", () => {
|
||||
Message.user("Think"),
|
||||
Message.assistant([
|
||||
{ type: "reasoning", text: "", cache },
|
||||
{ type: "reasoning", text: " ", providerMetadata: { "bedrock-converse": { signature: "" } }, cache },
|
||||
{ type: "reasoning", text: " ", providerMetadata: { bedrock: { signature: "" } }, cache },
|
||||
]),
|
||||
Message.user([{ type: "text", text: "Continue", cache }]),
|
||||
],
|
||||
@@ -893,16 +893,11 @@ describe("Bedrock Converse route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Signed thought",
|
||||
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
|
||||
providerMetadata: { bedrock: { signature: "sig_1" } },
|
||||
cache,
|
||||
},
|
||||
{ type: "reasoning", text: "", encrypted: "sig_2", cache },
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { "bedrock-converse": { redactedData: "cmVkYWN0ZWQ=" } },
|
||||
cache,
|
||||
},
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "cmVkYWN0ZWQ=" } }, cache },
|
||||
{ type: "text", text: "Checking" },
|
||||
ToolCallPart.make({ id: "call_1", name: "lookup", input: {} }),
|
||||
]),
|
||||
@@ -942,7 +937,7 @@ describe("Bedrock Converse route", () => {
|
||||
expect(reasoning).toEqual({
|
||||
type: "reasoning-end",
|
||||
id: "reasoning-0",
|
||||
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
|
||||
providerMetadata: { bedrock: { signature: "sig_1" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
@@ -965,7 +960,7 @@ describe("Bedrock Converse route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves route-keyed reasoning and usage metadata when the provider is reassigned", () =>
|
||||
it.effect("round-trips reassigned provider reasoning and usage metadata in its own namespace", () =>
|
||||
Effect.gen(function* () {
|
||||
const compatible = model.route.with({ provider: "custom-bedrock" }).model({ id: model.id })
|
||||
const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc="
|
||||
@@ -993,12 +988,12 @@ describe("Bedrock Converse route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Let me think.",
|
||||
providerMetadata: { "bedrock-converse": { signature: "custom_sig" } },
|
||||
providerMetadata: { "custom-bedrock": { signature: "custom_sig" } },
|
||||
},
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { "custom-bedrock": { redactedData } } },
|
||||
])
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
"bedrock-converse": { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
"custom-bedrock": { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
@@ -1035,13 +1030,13 @@ describe("Bedrock Converse route", () => {
|
||||
type: "reasoning-delta",
|
||||
id: "reasoning-0",
|
||||
text: "",
|
||||
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
|
||||
providerMetadata: { bedrock: { signature: "sig_1" } },
|
||||
})
|
||||
expect(response.message.content).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Let me think.",
|
||||
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
|
||||
providerMetadata: { bedrock: { signature: "sig_1" } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1066,7 +1061,7 @@ describe("Bedrock Converse route", () => {
|
||||
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { signature: "sig_1" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { signature: "sig_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1086,10 +1081,10 @@ describe("Bedrock Converse route", () => {
|
||||
type: "reasoning-delta",
|
||||
id: "reasoning-0",
|
||||
text: "",
|
||||
providerMetadata: { "bedrock-converse": { redactedData } },
|
||||
providerMetadata: { bedrock: { redactedData } },
|
||||
})
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1129,12 +1124,12 @@ describe("Bedrock Converse route", () => {
|
||||
type: "reasoning-delta",
|
||||
id: "reasoning-0",
|
||||
text: "",
|
||||
providerMetadata: { "bedrock-converse": { redactedData } },
|
||||
providerMetadata: { bedrock: { redactedData } },
|
||||
})
|
||||
expect(response.events.find((event) => event.type === "reasoning-end")).toEqual({
|
||||
type: "reasoning-end",
|
||||
id: "reasoning-0",
|
||||
providerMetadata: { "bedrock-converse": { redactedData } },
|
||||
providerMetadata: { bedrock: { redactedData } },
|
||||
})
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
@@ -1186,8 +1181,8 @@ describe("Bedrock Converse route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AQI=" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AwQ=" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQI=" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AwQ=" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1208,7 +1203,7 @@ describe("Bedrock Converse route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AQID" } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQID" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -41,8 +41,8 @@ describe("Amazon Bedrock Mantle provider", () => {
|
||||
protocol: "openai-responses",
|
||||
body: { model: "openai.gpt-oss-120b", store: false },
|
||||
})
|
||||
expect(provider.model("openai.gpt-oss-120b").route.id).toBe("bedrock-mantle-responses")
|
||||
expect(provider.chat("openai.gpt-oss-120b").route.id).toBe("bedrock-mantle-chat")
|
||||
expect(provider.model("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
|
||||
expect(provider.chat("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -160,7 +160,7 @@ describe("Amazon Bedrock Mantle provider", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"bedrock-mantle-responses": { itemId: "msg_95d4d0af4350432a", reasoningEncryptedContent: "mantle-state" },
|
||||
mantle: { itemId: "msg_95d4d0af4350432a", reasoningEncryptedContent: "mantle-state" },
|
||||
})
|
||||
expect(prepared.body.input).toEqual([
|
||||
{
|
||||
|
||||
@@ -126,7 +126,7 @@ for (const model of [
|
||||
name: item.type,
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { [model.route.id]: { itemId: item.id } },
|
||||
providerMetadata: { [model.route.providerMetadataKey ?? model.provider]: { itemId: item.id } },
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -196,10 +196,11 @@ const retainedItems = [
|
||||
|
||||
for (const model of [
|
||||
OpenAI.configure({ apiKey: "test" }).responses("gpt-5.3-codex"),
|
||||
...["default-metadata", "custom"].map((id) =>
|
||||
...[undefined, "custom"].map((providerMetadataKey) =>
|
||||
Route.make({
|
||||
id,
|
||||
id: providerMetadataKey ?? "default-metadata",
|
||||
provider: "openai",
|
||||
providerMetadataKey,
|
||||
protocol: OpenAIResponses.protocol,
|
||||
compact: OpenAIResponses.route.compact,
|
||||
endpoint: OpenAIResponses.route.endpoint,
|
||||
|
||||
@@ -493,7 +493,7 @@ describe("Gemini route", () => {
|
||||
id: "call_image",
|
||||
name: "read",
|
||||
input: { path: "pixel.png" },
|
||||
providerMetadata: { gemini: { thoughtSignature: "sig_1" } },
|
||||
providerMetadata: { google: { thoughtSignature: "sig_1" } },
|
||||
}),
|
||||
]),
|
||||
Message.tool({
|
||||
@@ -872,7 +872,7 @@ describe("Gemini route", () => {
|
||||
reasoningTokens: 1,
|
||||
totalTokens: 7,
|
||||
providerMetadata: {
|
||||
gemini: {
|
||||
google: {
|
||||
promptTokenCount: 5,
|
||||
candidatesTokenCount: 2,
|
||||
totalTokenCount: 7,
|
||||
@@ -973,22 +973,22 @@ describe("Gemini route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "A",
|
||||
providerMetadata: { gemini: { thoughtSignature: "reasoning_sig_a" } },
|
||||
providerMetadata: { google: { thoughtSignature: "reasoning_sig_a" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "X",
|
||||
providerMetadata: { gemini: { thoughtSignature: "text_sig_x" } },
|
||||
providerMetadata: { google: { thoughtSignature: "text_sig_x" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "B",
|
||||
providerMetadata: { gemini: { thoughtSignature: "reasoning_sig_b" } },
|
||||
providerMetadata: { google: { thoughtSignature: "reasoning_sig_b" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Y",
|
||||
providerMetadata: { gemini: { thoughtSignature: "text_sig_y" } },
|
||||
providerMetadata: { google: { thoughtSignature: "text_sig_y" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -1079,11 +1079,11 @@ describe("Gemini route", () => {
|
||||
expect(reasoningEnd).toEqual({
|
||||
type: "reasoning-end",
|
||||
id: "reasoning-0",
|
||||
providerMetadata: { gemini: { thoughtSignature: "thought_sig" } },
|
||||
providerMetadata: { google: { thoughtSignature: "thought_sig" } },
|
||||
})
|
||||
expect(toolCall).toMatchObject({
|
||||
id: "provider_call",
|
||||
providerMetadata: { gemini: { thoughtSignature: "tool_sig" } },
|
||||
providerMetadata: { google: { thoughtSignature: "tool_sig" } },
|
||||
})
|
||||
expect(response.events.findIndex((event) => event.type === "reasoning-end")).toBeLessThan(
|
||||
response.events.findIndex((event) => event.type === "tool-call"),
|
||||
@@ -1153,7 +1153,7 @@ describe("Gemini route", () => {
|
||||
expect(delta).toMatchObject({
|
||||
id: "text-0",
|
||||
text: "All done.",
|
||||
providerMetadata: { gemini: { thoughtSignature: "text_sig" } },
|
||||
providerMetadata: { google: { thoughtSignature: "text_sig" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
@@ -1190,7 +1190,7 @@ describe("Gemini route", () => {
|
||||
expect(delta).toMatchObject({ id: "text-0", text: "Working.", providerMetadata: undefined })
|
||||
expect(end).toMatchObject({
|
||||
id: "text-0",
|
||||
providerMetadata: { gemini: { thoughtSignature: "tail_sig" } },
|
||||
providerMetadata: { google: { thoughtSignature: "tail_sig" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1244,7 +1244,7 @@ describe("Gemini route", () => {
|
||||
id: "tool_0",
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerMetadata: { gemini: { thoughtSignature: "parallel_signature" } },
|
||||
providerMetadata: { google: { thoughtSignature: "parallel_signature" } },
|
||||
}),
|
||||
ToolCallPart.make({ id: "tool_1", name: "lookup", input: { query: "news" } }),
|
||||
ToolCallPart.make({ id: "tool_2", name: "lookup", input: { query: "sports" } }),
|
||||
@@ -1333,7 +1333,7 @@ describe("Gemini route", () => {
|
||||
cacheReadInputTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalTokens: 6,
|
||||
providerMetadata: { gemini: { promptTokenCount: 5, candidatesTokenCount: 1 } },
|
||||
providerMetadata: { google: { promptTokenCount: 5, candidatesTokenCount: 1 } },
|
||||
})
|
||||
|
||||
expect(response.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
|
||||
@@ -1629,7 +1629,7 @@ describe("Gemini route", () => {
|
||||
type: "finish",
|
||||
reason: { normalized: "content-filter", raw: "FUTURE_SAFETY_REASON" },
|
||||
providerMetadata: {
|
||||
gemini: {
|
||||
google: {
|
||||
promptFeedback: {
|
||||
blockReason: "FUTURE_SAFETY_REASON",
|
||||
blockReasonMessage: "Prompt blocked",
|
||||
|
||||
@@ -11,49 +11,6 @@ import { deltaChunk, finishChunk } from "../lib/openai-chunks.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
describe("Google Vertex providers", () => {
|
||||
it.effect("round-trips Claude signatures under the Vertex Messages route ID", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = GoogleVertexMessages.configure({ project: "project", accessToken: "token" }).model(
|
||||
"claude-sonnet-4-5",
|
||||
)
|
||||
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Think." })).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents(
|
||||
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
|
||||
{ type: "content_block_start", index: 0, content_block: { type: "thinking", thinking: "Thinking." } },
|
||||
{
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "signature_delta", signature: "vertex-signature" },
|
||||
},
|
||||
{ type: "content_block_stop", index: 0 },
|
||||
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
|
||||
{ type: "message_stop" },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(response.message.content).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Thinking.",
|
||||
providerMetadata: { "google-vertex-messages": { signature: "vertex-signature" } },
|
||||
},
|
||||
])
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
"google-vertex-messages": { input_tokens: 5, output_tokens: 2 },
|
||||
})
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
expect(replay.body.messages).toEqual([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "Thinking.", signature: "vertex-signature" }],
|
||||
},
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("sends Gemini requests to the global Vertex endpoint", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
@@ -132,7 +89,7 @@ describe("Google Vertex providers", () => {
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerMetadata: { "google-vertex-gemini": { functionCallId: "provider_call_1" } },
|
||||
providerMetadata: { vertex: { functionCallId: "provider_call_1" } },
|
||||
}),
|
||||
]),
|
||||
Message.tool({
|
||||
@@ -140,7 +97,7 @@ describe("Google Vertex providers", () => {
|
||||
name: "lookup",
|
||||
result: "sunny",
|
||||
resultType: "text",
|
||||
providerMetadata: { "google-vertex-gemini": { functionCallId: "provider_call_1" } },
|
||||
providerMetadata: { vertex: { functionCallId: "provider_call_1" } },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -201,17 +158,17 @@ describe("Google Vertex providers", () => {
|
||||
const text = response.events.find((event) => event.type === "text-delta")
|
||||
const toolCall = response.toolCalls[0]
|
||||
|
||||
expect(reasoning?.providerMetadata).toEqual({ "google-vertex-gemini": { thoughtSignature: "reasoning_sig" } })
|
||||
expect(text?.providerMetadata).toEqual({ "google-vertex-gemini": { thoughtSignature: "text_sig" } })
|
||||
expect(reasoning?.providerMetadata).toEqual({ vertex: { thoughtSignature: "reasoning_sig" } })
|
||||
expect(text?.providerMetadata).toEqual({ vertex: { thoughtSignature: "text_sig" } })
|
||||
expect(toolCall).toMatchObject({
|
||||
id: "provider_call_1",
|
||||
providerMetadata: { "google-vertex-gemini": { thoughtSignature: "tool_sig" } },
|
||||
providerMetadata: { vertex: { thoughtSignature: "tool_sig" } },
|
||||
})
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
"google-vertex-gemini": { promptTokenCount: 5, candidatesTokenCount: 2, thoughtsTokenCount: 1 },
|
||||
vertex: { promptTokenCount: 5, candidatesTokenCount: 2, thoughtsTokenCount: 1 },
|
||||
})
|
||||
expect(response.events.at(-1)?.providerMetadata).toEqual({
|
||||
"google-vertex-gemini": { promptFeedback: { blockReasonMessage: "Reviewed" } },
|
||||
vertex: { promptFeedback: { blockReasonMessage: "Reviewed" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
@@ -18,7 +18,8 @@ for (const model of [
|
||||
type: "media",
|
||||
mediaType: "image/png",
|
||||
data: "https://example.com/image.png",
|
||||
providerMetadata: detail === undefined ? undefined : { [model.route.id]: { detail } },
|
||||
providerMetadata:
|
||||
detail === undefined ? undefined : { [model.route.providerMetadataKey ?? model.provider]: { detail } },
|
||||
})),
|
||||
)
|
||||
const codec = Schema.fromJsonString(Message)
|
||||
@@ -49,7 +50,7 @@ it.effect("rejects malformed image detail instead of silently discarding it", ()
|
||||
type: "media",
|
||||
mediaType: "image/png",
|
||||
data: "https://example.com/image.png",
|
||||
providerMetadata: { "openai-responses": { detail: 42 } },
|
||||
providerMetadata: { openai: { detail: 42 } },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -26,6 +26,7 @@ describe("Mistral Chat", () => {
|
||||
expect(Mistral.route).toMatchObject({
|
||||
id: "mistral-chat",
|
||||
provider: "mistral",
|
||||
providerMetadataKey: "mistral",
|
||||
protocol: "mistral-chat",
|
||||
})
|
||||
expect(Mistral.route.endpoint).toMatchObject({
|
||||
@@ -315,7 +316,7 @@ describe("Mistral Chat", () => {
|
||||
type: "reasoning",
|
||||
text: "Consider",
|
||||
providerMetadata: {
|
||||
"mistral-chat": {
|
||||
mistral: {
|
||||
thinking: {
|
||||
type: "thinking",
|
||||
thinking: [{ type: "text", text: "Consider" }],
|
||||
@@ -359,7 +360,7 @@ describe("Mistral Chat", () => {
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: {
|
||||
"mistral-chat": { thinking: { type: "thinking", thinking: [], marker: "opaque" } },
|
||||
mistral: { thinking: { type: "thinking", thinking: [], marker: "opaque" } },
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -34,48 +34,45 @@ import { dynamicResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
describe("native OpenAI-compatible providers", () => {
|
||||
test("assigns API-specific route identities across native providers", () => {
|
||||
test("assigns provider-owned metadata namespaces across native routes", () => {
|
||||
const vertex = { project: "project", accessToken: "token" }
|
||||
const providers = [
|
||||
[OpenAI.configure({ apiKey: "test" }).chat("model"), "openai-chat"],
|
||||
[OpenAI.configure({ apiKey: "test" }).responses("model"), "openai-responses"],
|
||||
[Azure.configure({ resourceName: "resource", apiKey: "test" }).chat("model"), "azure-openai-chat"],
|
||||
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure-openai-responses"],
|
||||
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock-converse"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "bedrock-mantle-chat"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "bedrock-mantle-responses"],
|
||||
[Google.configure({ apiKey: "test" }).model("model"), "gemini"],
|
||||
[GoogleVertex.configure(vertex).model("model"), "google-vertex-gemini"],
|
||||
[GoogleVertexChat.configure(vertex).model("model"), "google-vertex-chat"],
|
||||
[GoogleVertexResponses.configure(vertex).model("model"), "google-vertex-responses"],
|
||||
[GoogleVertexMessages.configure(vertex).model("model"), "google-vertex-messages"],
|
||||
[Anthropic.configure({ apiKey: "test" }).model("model"), "anthropic-messages"],
|
||||
[OpenAI.configure({ apiKey: "test" }).chat("model"), "openai"],
|
||||
[OpenAI.configure({ apiKey: "test" }).responses("model"), "openai"],
|
||||
[Azure.configure({ resourceName: "resource", apiKey: "test" }).chat("model"), "azure"],
|
||||
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure"],
|
||||
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "mantle"],
|
||||
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "mantle"],
|
||||
[Google.configure({ apiKey: "test" }).model("model"), "google"],
|
||||
[GoogleVertex.configure(vertex).model("model"), "vertex"],
|
||||
[GoogleVertexChat.configure(vertex).model("model"), "vertex"],
|
||||
[GoogleVertexResponses.configure(vertex).model("model"), "vertex"],
|
||||
[GoogleVertexMessages.configure(vertex).model("model"), "anthropic"],
|
||||
[Anthropic.configure({ apiKey: "test" }).model("model"), "anthropic"],
|
||||
[
|
||||
AnthropicCompatible.configure({ baseURL: "https://example.test/v1", provider: "minimax" }).model("model"),
|
||||
"anthropic-messages",
|
||||
],
|
||||
[
|
||||
OpenAICompatible.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"),
|
||||
"openai-compatible-chat",
|
||||
"minimax",
|
||||
],
|
||||
[OpenAICompatible.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"), "custom"],
|
||||
[
|
||||
OpenAICompatibleResponses.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"),
|
||||
"openai-compatible-responses",
|
||||
"custom",
|
||||
],
|
||||
[Cerebras.configure({ apiKey: "test" }).model("model"), "cerebras-chat"],
|
||||
[Baseten.configure({ apiKey: "test" }).model("model"), "baseten-chat"],
|
||||
[DeepSeek.configure({ apiKey: "test" }).model("model"), "deepseek-chat"],
|
||||
[Fireworks.configure({ apiKey: "test" }).model("model"), "fireworks-chat"],
|
||||
[DeepInfra.configure({ apiKey: "test" }).model("model"), "deepinfra-chat"],
|
||||
[TogetherAI.configure({ apiKey: "test" }).model("model"), "togetherai-chat"],
|
||||
[Cerebras.configure({ apiKey: "test" }).model("model"), "cerebras"],
|
||||
[Baseten.configure({ apiKey: "test" }).model("model"), "baseten"],
|
||||
[DeepSeek.configure({ apiKey: "test" }).model("model"), "deepseek"],
|
||||
[Fireworks.configure({ apiKey: "test" }).model("model"), "fireworks"],
|
||||
[DeepInfra.configure({ apiKey: "test" }).model("model"), "deepinfra"],
|
||||
[TogetherAI.configure({ apiKey: "test" }).model("model"), "togetherai"],
|
||||
[CloudflareAIGateway.configure({ accountId: "account" }).model("model"), "cloudflare-ai-gateway"],
|
||||
[CloudflareWorkersAI.configure({ accountId: "account" }).model("model"), "cloudflare-workers-ai"],
|
||||
[OpenRouter.configure({ apiKey: "test" }).model("model"), "openrouter"],
|
||||
[XAI.configure({ apiKey: "test" }).chat("model"), "xai-chat"],
|
||||
[XAI.configure({ apiKey: "test" }).responses("model"), "xai-responses"],
|
||||
[XAI.configure({ apiKey: "test" }).chat("model"), "xai"],
|
||||
[XAI.configure({ apiKey: "test" }).responses("model"), "xai"],
|
||||
] as const
|
||||
|
||||
for (const [model, id] of providers) expect(model.route.id).toBe(id)
|
||||
for (const [model, key] of providers) expect(model.route.providerMetadataKey).toBe(key)
|
||||
})
|
||||
|
||||
test("preserves native Together AI and Cerebras provider and route identities", () => {
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("Open Responses completed item text", () => {
|
||||
type: "text-end",
|
||||
id: "msg_1",
|
||||
text,
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "final_answer" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "final_answer" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -138,7 +138,7 @@ describe("Open Responses completed item reasoning", () => {
|
||||
expect(response.reasoning).toBe(fixture.text)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted" },
|
||||
"openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted" },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -97,37 +97,27 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1" } },
|
||||
},
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-compatible": { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1" } },
|
||||
},
|
||||
{ type: "reasoning-end", id: "rs_1:1", providerMetadata: { "openai-compatible": { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:2",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:2", text: "Third" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:2",
|
||||
providerMetadata: {
|
||||
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -151,17 +141,13 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1",
|
||||
providerMetadata: {
|
||||
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1",
|
||||
text: "Not streamed",
|
||||
providerMetadata: {
|
||||
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -190,36 +176,32 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_1",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "commentary" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
|
||||
},
|
||||
{ type: "text-delta", id: "msg_1", text: "Checking" },
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_1",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "commentary" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
|
||||
},
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_2",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_2", phase: null } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_2", phase: null } },
|
||||
},
|
||||
{ type: "text-delta", id: "msg_2", text: "Cannot help." },
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_2",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_2", phase: "final_answer" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_2", phase: "final_answer" } },
|
||||
},
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_3",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_3", phase: null } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_3", phase: null } },
|
||||
},
|
||||
{ type: "text-delta", id: "msg_3", text: "Done-only refusal." },
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_3",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_3", phase: null } },
|
||||
},
|
||||
{ type: "text-end", id: "msg_3", providerMetadata: { "openai-compatible": { itemId: "msg_3", phase: null } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -250,24 +232,24 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_text",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_text" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_text" } },
|
||||
},
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_text",
|
||||
text: "Done-only text.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_text" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_text" } },
|
||||
},
|
||||
{
|
||||
type: "text-start",
|
||||
id: "msg_refusal",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_refusal" } },
|
||||
},
|
||||
{
|
||||
type: "text-end",
|
||||
id: "msg_refusal",
|
||||
text: "Done-only refusal.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "msg_refusal" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -323,7 +305,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
response: { id: "resp_1", output: [{ type: "reasoning", id: "msg_879a68b589198b4c" }, terminal] },
|
||||
},
|
||||
)
|
||||
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_828bee50dee1d029" } }
|
||||
const providerMetadata = { "openai-compatible": { itemId: "fc_828bee50dee1d029" } }
|
||||
expect(events.filter((event) => event.type.startsWith("tool-"))).toEqual([
|
||||
{ type: "tool-input-start", id: "call_bc1eb4b42e70ee53", name: "get_weather", providerMetadata },
|
||||
{
|
||||
@@ -356,7 +338,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
)
|
||||
const call = events.find(LLMEvent.is.toolCall)
|
||||
expect(call).toMatchObject({ id: "call_1", name: "lookup", input: { query: "weather" } })
|
||||
expect(call?.providerMetadata?.["openai-compatible-responses"]).toMatchObject({
|
||||
expect(call?.providerMetadata?.["openai-compatible"]).toMatchObject({
|
||||
itemId: expect.stringMatching(/^fc_[0-9a-f]{32}$/),
|
||||
})
|
||||
}),
|
||||
@@ -372,7 +354,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
arguments: '{"query":"weather"}',
|
||||
}
|
||||
const events = yield* collect({ type: "response.output_item.done", item }, completed)
|
||||
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_1" } }
|
||||
const providerMetadata = { "openai-compatible": { itemId: "fc_1" } }
|
||||
expect(events.filter((event) => event.type.startsWith("tool-"))).toEqual([
|
||||
{ type: "tool-input-start", id: "call_1", name: "lookup", namespace: undefined, providerMetadata },
|
||||
{ type: "tool-input-end", id: "call_1", name: "lookup", namespace: undefined, providerMetadata },
|
||||
@@ -389,7 +371,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "finish",
|
||||
reason: { normalized: "tool-calls", raw: undefined },
|
||||
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1", serviceTier: undefined } },
|
||||
providerMetadata: { "openai-compatible": { responseId: "resp_1", serviceTier: undefined } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -423,7 +405,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
type: "tool-input-end",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "fc_1" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "fc_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
@@ -431,7 +413,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
name: "lookup",
|
||||
input: { query: "final" },
|
||||
providerExecuted: undefined,
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "fc_1" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "fc_1" } },
|
||||
},
|
||||
{ type: "reasoning-end", id: "rs_1:0" },
|
||||
])
|
||||
@@ -456,7 +438,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: ":0",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "", reasoningEncryptedContent: "state" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "", reasoningEncryptedContent: "state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -492,7 +474,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
completed,
|
||||
)
|
||||
// Generic terminal closure does not repeat the message's phase metadata.
|
||||
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_1" } }
|
||||
const providerMetadata = { "openai-compatible": { itemId: "fc_1" } }
|
||||
expect(events.slice(4, -2)).toEqual([
|
||||
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata },
|
||||
{ type: "tool-call", id: "call_1", name: "lookup", input: {}, providerMetadata },
|
||||
@@ -536,7 +518,7 @@ describe("Open Responses basic-item lifecycles", () => {
|
||||
{
|
||||
type: "finish",
|
||||
reason: { normalized: "length", raw: "max_output_tokens" },
|
||||
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1", serviceTier: undefined } },
|
||||
providerMetadata: { "openai-compatible": { responseId: "resp_1", serviceTier: undefined } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
|
||||
@@ -69,7 +69,7 @@ for (const item of cases) {
|
||||
expect(response.reasoning.length).toBeGreaterThan(0)
|
||||
expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
|
||||
const metadata = response.message.content.find((part) => part.type === "reasoning")?.providerMetadata?.[
|
||||
item.model.route.id
|
||||
item.model.route.providerMetadataKey ?? String(item.model.provider)
|
||||
]
|
||||
expect(metadata?.reasoningField).toBe(item.structured ? "reasoning" : "reasoning_content")
|
||||
expect(Array.isArray(metadata?.reasoningDetails)).toBe(item.structured)
|
||||
@@ -128,7 +128,11 @@ for (const item of cases) {
|
||||
).toMatch(/^Paris is sunny\.?$/)
|
||||
const details = events
|
||||
.filter(LLMEvent.is.reasoningEnd)
|
||||
.map((event) => event.providerMetadata?.[item.model.route.id]?.reasoningDetails)
|
||||
.map(
|
||||
(event) =>
|
||||
event.providerMetadata?.[item.model.route.providerMetadataKey ?? String(item.model.provider)]
|
||||
?.reasoningDetails,
|
||||
)
|
||||
.find(Array.isArray)
|
||||
expect(Array.isArray(details)).toBe(item.structured)
|
||||
if (!item.structured || !Array.isArray(details)) return
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("OpenAI Chat route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
providerMetadata: { "openai-chat": { reasoningField: "reasoning" } },
|
||||
providerMetadata: { openai: { reasoningField: "reasoning" } },
|
||||
},
|
||||
{ type: "text", text: "Hello" },
|
||||
]),
|
||||
@@ -180,7 +180,7 @@ describe("OpenAI Chat route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
providerMetadata: { "openai-chat": { reasoningField: "reasoning_text" } },
|
||||
providerMetadata: { openai: { reasoningField: "reasoning_text" } },
|
||||
},
|
||||
{ type: "text", text: "Hello" },
|
||||
]),
|
||||
@@ -775,7 +775,7 @@ describe("OpenAI Chat route", () => {
|
||||
reasoningTokens: 0,
|
||||
totalTokens: 7,
|
||||
providerMetadata: {
|
||||
"openai-chat": {
|
||||
openai: {
|
||||
prompt_tokens: 5,
|
||||
completion_tokens: 2,
|
||||
total_tokens: 7,
|
||||
@@ -868,7 +868,7 @@ describe("OpenAI Chat route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "openai-chat": { reasoningDetails: [] } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { openai: { reasoningDetails: [] } } },
|
||||
{
|
||||
type: "text",
|
||||
text: "I can't help with that.",
|
||||
@@ -917,7 +917,7 @@ describe("OpenAI Chat route", () => {
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.text).toBe("Hello")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: field },
|
||||
openai: { reasoningField: field },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
@@ -926,10 +926,10 @@ describe("OpenAI Chat route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses the route ID for reasoning and usage metadata", () =>
|
||||
it.effect("uses the configured provider metadata namespace for reasoning and usage", () =>
|
||||
Effect.gen(function* () {
|
||||
const selected = LanguageModel.update(model, {
|
||||
route: model.route.with({ id: "vendor" }),
|
||||
route: { ...model.route, providerMetadataKey: "vendor" },
|
||||
})
|
||||
const details = [{ type: "reasoning.text", text: "thinking", signature: "signed" }]
|
||||
const response = yield* LLMClient.generate(LLMRequest.update(request, { model: selected })).pipe(
|
||||
@@ -959,9 +959,12 @@ describe("OpenAI Chat route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("preserves the route metadata namespace when the selected provider changes", () =>
|
||||
it.effect("falls back to the selected provider for the metadata namespace", () =>
|
||||
Effect.gen(function* () {
|
||||
const selected = model.route.with({ provider: "deepseek" }).model({ id: "deepseek-chat" })
|
||||
const compatible = model.route.with({ provider: "deepseek" }).model({ id: "deepseek-chat" })
|
||||
const selected = LanguageModel.update(compatible, {
|
||||
route: { ...compatible.route, providerMetadataKey: undefined },
|
||||
})
|
||||
const response = yield* LLMClient.generate(LLMRequest.update(request, { model: selected })).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
@@ -976,10 +979,10 @@ describe("OpenAI Chat route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning_content" },
|
||||
deepseek: { reasoningField: "reasoning_content" },
|
||||
})
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
"openai-chat": { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
||||
deepseek: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model: selected, messages: [response.message] }))
|
||||
@@ -1004,7 +1007,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "vendor_reasoning" },
|
||||
openai: { reasoningField: "vendor_reasoning" },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model: custom, messages: [response.message] }))
|
||||
@@ -1047,7 +1050,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
@@ -1090,7 +1093,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningDetails: details },
|
||||
openai: { reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
@@ -1115,7 +1118,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
@@ -1142,7 +1145,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1168,7 +1171,7 @@ describe("OpenAI Chat route", () => {
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1194,7 +1197,7 @@ describe("OpenAI Chat route", () => {
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: details },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -1215,7 +1218,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningDetails: [] },
|
||||
openai: { reasoningDetails: [] },
|
||||
})
|
||||
|
||||
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
|
||||
@@ -1265,14 +1268,12 @@ describe("OpenAI Chat route", () => {
|
||||
expect(response.reasoning).toBe("thinking")
|
||||
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
|
||||
"openai-chat": { reasoningField: "reasoning", reasoningDetails: merged },
|
||||
openai: { reasoningField: "reasoning", reasoningDetails: merged },
|
||||
})
|
||||
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningDelta)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
expect(publishedEndMetadata).toEqual([
|
||||
{ "openai-chat": { reasoningField: "reasoning", reasoningDetails: merged } },
|
||||
])
|
||||
expect(publishedEndMetadata).toEqual([{ openai: { reasoningField: "reasoning", reasoningDetails: merged } }])
|
||||
expect(response.events.findIndex(LLMEvent.is.reasoningStart)).toBeLessThan(
|
||||
response.events.findIndex(LLMEvent.is.textStart),
|
||||
)
|
||||
@@ -1303,7 +1304,7 @@ describe("OpenAI Chat route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "", providerMetadata: { "openai-chat": { reasoningDetails: details } } },
|
||||
{ type: "reasoning", text: "", providerMetadata: { openai: { reasoningDetails: details } } },
|
||||
])
|
||||
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
|
||||
@@ -1329,7 +1330,7 @@ describe("OpenAI Chat route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("summary")
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "reasoning", text: "summary", providerMetadata: { "openai-chat": { reasoningDetails: details } } },
|
||||
{ type: "reasoning", text: "summary", providerMetadata: { openai: { reasoningDetails: details } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -1346,12 +1347,12 @@ describe("OpenAI Chat route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "first",
|
||||
providerMetadata: { "openai-chat": { reasoningDetails: [first] } },
|
||||
providerMetadata: { openai: { reasoningDetails: [first] } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "second",
|
||||
providerMetadata: { "openai-chat": { reasoningField: "reasoning", reasoningDetails: [second] } },
|
||||
providerMetadata: { openai: { reasoningField: "reasoning", reasoningDetails: [second] } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
@@ -1375,7 +1376,7 @@ describe("OpenAI Chat route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "A",
|
||||
providerMetadata: { "openai-chat": { reasoningDetails: [detail] } },
|
||||
providerMetadata: { openai: { reasoningDetails: [detail] } },
|
||||
},
|
||||
{ type: "reasoning", text: "B" },
|
||||
]),
|
||||
|
||||
@@ -405,7 +405,7 @@ describe("OpenAI-compatible Chat route", () => {
|
||||
outputTokens: undefined,
|
||||
totalTokens: undefined,
|
||||
providerMetadata: {
|
||||
"openai-compatible-chat": {
|
||||
deepseek: {
|
||||
prompt_tokens: null,
|
||||
completion_tokens: null,
|
||||
total_tokens: null,
|
||||
|
||||
@@ -276,31 +276,19 @@ describe("Open Responses-compatible route", () => {
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
type: "text",
|
||||
text: "Kept.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "history_1" } },
|
||||
},
|
||||
{ type: "text", text: "Kept.", providerMetadata: { "openai-compatible": { itemId: "history_1" } } },
|
||||
{
|
||||
type: "text",
|
||||
text: "Long.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: `history_${"a".repeat(64)}` } },
|
||||
providerMetadata: { "openai-compatible": { itemId: `history_${"a".repeat(64)}` } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Opaque.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "provider_value/with+symbols" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "No suffix.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "No prefix.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "_item" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "provider_value/with+symbols" } },
|
||||
},
|
||||
{ type: "text", text: "No suffix.", providerMetadata: { "openai-compatible": { itemId: "msg_" } } },
|
||||
{ type: "text", text: "No prefix.", providerMetadata: { "openai-compatible": { itemId: "_item" } } },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
@@ -360,7 +348,7 @@ describe("Open Responses-compatible route", () => {
|
||||
name: item.type,
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: item.id } },
|
||||
providerMetadata: { example: { itemId: item.id } },
|
||||
}),
|
||||
),
|
||||
}),
|
||||
@@ -395,7 +383,7 @@ describe("Open Responses-compatible route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "text", text: "Indexed", providerMetadata: { "openai-compatible-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text", text: "Indexed", providerMetadata: { "openai-compatible": { itemId: "msg_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -461,7 +449,7 @@ describe("Open Responses-compatible route", () => {
|
||||
),
|
||||
)
|
||||
|
||||
const metadata = { "openai-compatible-responses": { itemId: routing.id } }
|
||||
const metadata = { "openai-compatible": { itemId: routing.id } }
|
||||
if (fixture.item.type === "function_call") {
|
||||
expect(response.toolCalls).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -479,10 +467,7 @@ describe("Open Responses-compatible route", () => {
|
||||
type: "reasoning",
|
||||
text: "Preserved",
|
||||
providerMetadata: {
|
||||
"openai-compatible-responses": {
|
||||
itemId: routing.id,
|
||||
reasoningEncryptedContent: "encrypted-state",
|
||||
},
|
||||
"openai-compatible": { itemId: routing.id, reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -534,13 +519,13 @@ describe("Open Responses-compatible route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "First.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: routing.id } },
|
||||
providerMetadata: { "openai-compatible": { itemId: routing.id } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Second.",
|
||||
providerMetadata: {
|
||||
"openai-compatible-responses": { itemId: routing.id, reasoningEncryptedContent: null },
|
||||
"openai-compatible": { itemId: routing.id, reasoningEncryptedContent: null },
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -549,7 +534,7 @@ describe("Open Responses-compatible route", () => {
|
||||
type: "reasoning-end",
|
||||
id: `${routing.id}:0`,
|
||||
text: undefined,
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: routing.id } },
|
||||
providerMetadata: { "openai-compatible": { itemId: routing.id } },
|
||||
},
|
||||
{ type: "reasoning-end", id: `${routing.id}:1` },
|
||||
])
|
||||
@@ -580,7 +565,7 @@ describe("Open Responses-compatible route", () => {
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
input: { query: "complete" },
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "" } },
|
||||
providerMetadata: { "openai-compatible": { itemId: "" } },
|
||||
}),
|
||||
])
|
||||
}),
|
||||
@@ -607,11 +592,7 @@ describe("Open Responses-compatible route", () => {
|
||||
)
|
||||
|
||||
expect(response.message.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: "Before after",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1" } },
|
||||
},
|
||||
{ type: "text", text: "Before after", providerMetadata: { "openai-compatible": { itemId: "msg_1" } } },
|
||||
])
|
||||
expect(response.events.map((event) => event.type)).toEqual([
|
||||
"step-start",
|
||||
@@ -724,7 +705,7 @@ describe("Open Responses-compatible route", () => {
|
||||
|
||||
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
|
||||
input: { query: "complete" },
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "item_1" } },
|
||||
providerMetadata: { example: { itemId: "item_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -806,7 +787,7 @@ describe("Open Responses-compatible route", () => {
|
||||
Message.assistant({
|
||||
type: "text",
|
||||
text: "Unclassified.",
|
||||
providerMetadata: { "openai-compatible-responses": { phase: null } },
|
||||
providerMetadata: { "openai-compatible": { phase: null } },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -865,7 +846,7 @@ describe("Open Responses-compatible route", () => {
|
||||
{
|
||||
type: "text",
|
||||
text: "I can't help with that.",
|
||||
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
|
||||
providerMetadata: { example: { itemId: "msg_refusal" } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -954,7 +935,7 @@ describe("Open Responses-compatible route", () => {
|
||||
|
||||
expect(response.toolCalls).toEqual([])
|
||||
expect(response.events.find(LLMEvent.is.finish)).toMatchObject({
|
||||
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1" } },
|
||||
providerMetadata: { example: { responseId: "resp_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1938,7 +1938,7 @@ describe("OpenAI Responses route", () => {
|
||||
reasoningTokens: 0,
|
||||
totalTokens: 7,
|
||||
providerMetadata: {
|
||||
"openai-responses": {
|
||||
openai: {
|
||||
input_tokens: 5,
|
||||
output_tokens: 2,
|
||||
total_tokens: 7,
|
||||
@@ -1951,7 +1951,7 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.text).toBe("Hello!")
|
||||
expect(response.events).toEqual([
|
||||
{ type: "step-start", index: 0 },
|
||||
{ type: "text-start", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text-start", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
{ type: "text-delta", id: "msg_1", text: "Hello" },
|
||||
{ type: "text-delta", id: "msg_1", text: "!" },
|
||||
{ type: "text-end", id: "msg_1" },
|
||||
@@ -1959,13 +1959,13 @@ describe("OpenAI Responses route", () => {
|
||||
type: "step-finish",
|
||||
index: 0,
|
||||
reason: { normalized: "stop", raw: undefined },
|
||||
providerMetadata: { "openai-responses": { responseId: "resp_1", serviceTier: "default" } },
|
||||
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
|
||||
usage,
|
||||
},
|
||||
{
|
||||
type: "finish",
|
||||
reason: { normalized: "stop", raw: undefined },
|
||||
providerMetadata: { "openai-responses": { responseId: "resp_1", serviceTier: "default" } },
|
||||
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
|
||||
usage,
|
||||
},
|
||||
])
|
||||
@@ -2040,7 +2040,7 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "text",
|
||||
text: "I can't help with that.",
|
||||
providerMetadata: { "openai-responses": { itemId: "msg_refusal", phase: "final_answer" } },
|
||||
providerMetadata: { openai: { itemId: "msg_refusal", phase: "final_answer" } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -2112,17 +2112,17 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "text",
|
||||
text: "Checking.",
|
||||
providerMetadata: { "openai-responses": { itemId: "msg_commentary", phase: "commentary" } },
|
||||
providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Finished.",
|
||||
providerMetadata: { "openai-responses": { itemId: "msg_final", phase: "final_answer" } },
|
||||
providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Unclassified.",
|
||||
providerMetadata: { "openai-responses": { itemId: "msg_null", phase: null } },
|
||||
providerMetadata: { openai: { itemId: "msg_null", phase: null } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -2170,7 +2170,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
expect(response.text).toBe("Indexed")
|
||||
expect(response.message.content).toEqual([
|
||||
{ type: "text", text: "Indexed", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text", text: "Indexed", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -2331,7 +2331,7 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Thinking",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -2379,7 +2379,7 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Raw",
|
||||
providerMetadata: { "openai-responses": { itemId: "", reasoningEncryptedContent: "state" } },
|
||||
providerMetadata: { openai: { itemId: "", reasoningEncryptedContent: "state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -2588,12 +2588,12 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([
|
||||
{ type: "text-start", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text-start", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
{ type: "text-delta", id: "msg_1", text: "First", providerMetadata: undefined },
|
||||
{ type: "text-end", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text-start", id: "msg_2", providerMetadata: { "openai-responses": { itemId: "msg_2" } } },
|
||||
{ type: "text-end", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
{ type: "text-start", id: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
|
||||
{ type: "text-delta", id: "msg_2", text: "Second", providerMetadata: undefined },
|
||||
{ type: "text-end", id: "msg_2", providerMetadata: { "openai-responses": { itemId: "msg_2" } } },
|
||||
{ type: "text-end", id: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -2629,9 +2629,9 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "thinking",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "text", text: "Hello", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
|
||||
{ type: "text", text: "Hello", providerMetadata: { openai: { itemId: "msg_1" } } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
@@ -2663,7 +2663,7 @@ describe("OpenAI Responses route", () => {
|
||||
expect.objectContaining({
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@@ -2715,7 +2715,7 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.message.content).toContainEqual({
|
||||
type: "reasoning",
|
||||
text: "Checked the diff.",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -2829,20 +2829,20 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
{ type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } },
|
||||
{ type: "finish", reason: { normalized: "stop", raw: undefined } },
|
||||
@@ -2887,31 +2887,31 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:0", text: "First", providerMetadata: undefined },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:1", text: "Second", providerMetadata: undefined },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:2",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:2", text: "Third", providerMetadata: undefined },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:2",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -3093,14 +3093,14 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning-start",
|
||||
id: "rs_1:0",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
|
||||
},
|
||||
{ type: "reasoning-delta", id: "rs_1:0", text: "Checked the diff.", providerMetadata: undefined },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:0",
|
||||
text: "Checked the diff.",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
|
||||
@@ -3178,11 +3178,11 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
|
||||
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
|
||||
{
|
||||
type: "reasoning-end",
|
||||
id: "rs_1:1",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -3201,7 +3201,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "reasoning",
|
||||
text: "Checked the previous diff.",
|
||||
providerMetadata: {
|
||||
"openai-responses": {
|
||||
openai: {
|
||||
itemId: "rs_1",
|
||||
reasoningEncryptedContent: "encrypted-state",
|
||||
},
|
||||
@@ -3262,7 +3262,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "reasoning",
|
||||
text: "Checked order.",
|
||||
providerMetadata: {
|
||||
"openai-responses": {
|
||||
openai: {
|
||||
itemId: "rs_1",
|
||||
reasoningEncryptedContent: "encrypted-state",
|
||||
},
|
||||
@@ -3298,9 +3298,7 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Checked the previous diff.",
|
||||
providerMetadata: {
|
||||
"openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
@@ -3332,7 +3330,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
input: { query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
}),
|
||||
{
|
||||
type: "tool-result",
|
||||
@@ -3340,7 +3338,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
]),
|
||||
Message.user("Continue."),
|
||||
@@ -3370,7 +3368,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
input: { query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
}),
|
||||
{
|
||||
type: "tool-result",
|
||||
@@ -3378,7 +3376,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
]),
|
||||
Message.user("Continue."),
|
||||
@@ -3412,7 +3410,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: item.type,
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: item.id } },
|
||||
providerMetadata: { openai: { itemId: item.id } },
|
||||
}),
|
||||
),
|
||||
}),
|
||||
@@ -3439,7 +3437,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
input: { query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
}),
|
||||
{
|
||||
type: "tool-result",
|
||||
@@ -3447,7 +3445,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
]),
|
||||
Message.user("Continue."),
|
||||
@@ -3479,7 +3477,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json", value: { type: "web_search_call", id: "ws_other", status: "completed" } },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
@@ -3505,41 +3503,39 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello",
|
||||
providerMetadata: { "openai-responses": { itemId: "history_1" } },
|
||||
providerMetadata: { openai: { itemId: "history_1" } },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "World",
|
||||
providerMetadata: { "openai-responses": { itemId: `message_${"a".repeat(64)}` } },
|
||||
providerMetadata: { openai: { itemId: `message_${"a".repeat(64)}` } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Checked the diff.",
|
||||
providerMetadata: {
|
||||
"openai-responses": { itemId: "thinking_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { openai: { itemId: "thinking_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Missing suffix.",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_", reasoningEncryptedContent: "another-state" } },
|
||||
providerMetadata: { openai: { itemId: "rs_", reasoningEncryptedContent: "another-state" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "No prefix separator.",
|
||||
providerMetadata: { "openai-responses": { itemId: "550e8400-e29b-41d4-a716-446655440000" } },
|
||||
providerMetadata: { openai: { itemId: "550e8400-e29b-41d4-a716-446655440000" } },
|
||||
},
|
||||
ToolCallPart.make({
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerMetadata: { "openai-responses": { itemId: "toolu_01A" } },
|
||||
providerMetadata: { openai: { itemId: "toolu_01A" } },
|
||||
}),
|
||||
ToolCallPart.make({
|
||||
id: "call_2",
|
||||
name: "lookup",
|
||||
input: { query: "news" },
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_" } },
|
||||
providerMetadata: { openai: { itemId: "fc_" } },
|
||||
}),
|
||||
]),
|
||||
],
|
||||
@@ -3601,7 +3597,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
input: { query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId } },
|
||||
providerMetadata: { openai: { itemId } },
|
||||
}),
|
||||
{
|
||||
type: "tool-result" as const,
|
||||
@@ -3609,7 +3605,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json" as const, value: item },
|
||||
providerExecuted: true as const,
|
||||
providerMetadata: { "openai-responses": { itemId } },
|
||||
providerMetadata: { openai: { itemId } },
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -3645,7 +3641,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "image_generation",
|
||||
input: {},
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
}),
|
||||
ToolResultPart.make({
|
||||
id: "ig_1",
|
||||
@@ -3655,7 +3651,7 @@ describe("OpenAI Responses route", () => {
|
||||
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
|
||||
},
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
}),
|
||||
]),
|
||||
Message.user("Make it blue."),
|
||||
@@ -3686,7 +3682,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "image_generation",
|
||||
input: {},
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
}),
|
||||
ToolResultPart.make({
|
||||
id: "ig_1",
|
||||
@@ -3696,7 +3692,7 @@ describe("OpenAI Responses route", () => {
|
||||
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
|
||||
},
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
}),
|
||||
]),
|
||||
],
|
||||
@@ -3721,14 +3717,12 @@ describe("OpenAI Responses route", () => {
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "First",
|
||||
providerMetadata: { "openai-responses": { itemId: "rs_1" } },
|
||||
providerMetadata: { openai: { itemId: "rs_1" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "Second",
|
||||
providerMetadata: {
|
||||
"openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
|
||||
},
|
||||
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
@@ -3763,7 +3757,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "reasoning",
|
||||
text: "Checked the previous diff.",
|
||||
providerMetadata: {
|
||||
"openai-responses": {
|
||||
openai: {
|
||||
itemId: "rs_1",
|
||||
reasoningEncryptedContent: null,
|
||||
},
|
||||
@@ -3827,7 +3821,7 @@ describe("OpenAI Responses route", () => {
|
||||
cacheReadInputTokens: undefined,
|
||||
reasoningTokens: undefined,
|
||||
totalTokens: 6,
|
||||
providerMetadata: { "openai-responses": { input_tokens: 5, output_tokens: 1 } },
|
||||
providerMetadata: { openai: { input_tokens: 5, output_tokens: 1 } },
|
||||
})
|
||||
|
||||
expect(response.events).toEqual([
|
||||
@@ -3836,7 +3830,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "tool-input-start",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-input-delta",
|
||||
@@ -3856,7 +3850,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "tool-input-end",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
@@ -3864,7 +3858,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerExecuted: undefined,
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
{
|
||||
type: "step-finish",
|
||||
@@ -3933,7 +3927,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "lookup",
|
||||
input: { query: "weather" },
|
||||
providerExecuted: undefined,
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -4110,7 +4104,7 @@ describe("OpenAI Responses route", () => {
|
||||
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
|
||||
id: "call_1",
|
||||
input: { query: "weather" },
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
})
|
||||
expect(response.events.filter(LLMEvent.is.toolInputEnd)).toHaveLength(1)
|
||||
expect(response.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
|
||||
@@ -4234,7 +4228,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "tool-input-end",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
@@ -4242,7 +4236,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "lookup",
|
||||
input: {},
|
||||
providerExecuted: undefined,
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
},
|
||||
],
|
||||
)
|
||||
@@ -4336,7 +4330,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
|
||||
id: "call_1",
|
||||
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
|
||||
providerMetadata: { openai: { itemId: "fc_item_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -4366,7 +4360,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
input: { type: "search", query: "effect 4" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
@@ -4374,7 +4368,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "web_search",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
|
||||
providerMetadata: { openai: { itemId: "ws_1" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -4407,7 +4401,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "computer_use",
|
||||
input: { type: "click", x: 100, y: 200 },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "computer_1" } },
|
||||
providerMetadata: { openai: { itemId: "computer_1" } },
|
||||
},
|
||||
{
|
||||
type: "tool-result",
|
||||
@@ -4415,7 +4409,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "computer_use",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "computer_1" } },
|
||||
providerMetadata: { openai: { itemId: "computer_1" } },
|
||||
},
|
||||
])
|
||||
}),
|
||||
@@ -4443,7 +4437,7 @@ describe("OpenAI Responses route", () => {
|
||||
)
|
||||
|
||||
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
})
|
||||
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
|
||||
id: "ig_1",
|
||||
@@ -4453,7 +4447,7 @@ describe("OpenAI Responses route", () => {
|
||||
type: "content",
|
||||
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
|
||||
},
|
||||
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
|
||||
providerMetadata: { openai: { itemId: "ig_1" } },
|
||||
})
|
||||
|
||||
const prepared = yield* Effect.forEach([false, true], (store) =>
|
||||
@@ -4487,7 +4481,7 @@ describe("OpenAI Responses route", () => {
|
||||
|
||||
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
|
||||
result: { type: "error", value: item.error },
|
||||
providerMetadata: { "openai-responses": { itemId: "ws_failed" } },
|
||||
providerMetadata: { openai: { itemId: "ws_failed" } },
|
||||
})
|
||||
|
||||
const prepared = yield* compileRequest(
|
||||
@@ -4547,7 +4541,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "code_interpreter",
|
||||
input: { code: "print(1+1)", container_id: "cnt_xyz" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ci_1" } },
|
||||
providerMetadata: { openai: { itemId: "ci_1" } },
|
||||
})
|
||||
const toolResult = response.events.find((event) => event.type === "tool-result")
|
||||
expect(toolResult).toEqual({
|
||||
@@ -4556,7 +4550,7 @@ describe("OpenAI Responses route", () => {
|
||||
name: "code_interpreter",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "openai-responses": { itemId: "ci_1" } },
|
||||
providerMetadata: { openai: { itemId: "ci_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("xAI Responses route", () => {
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")).toMatchObject({
|
||||
type: "reasoning",
|
||||
text: "Considering.",
|
||||
providerMetadata: { "xai-responses": { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
|
||||
providerMetadata: { xai: { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -101,7 +101,7 @@ describe("xAI Responses route", () => {
|
||||
|
||||
expect(response.reasoning).toBe("Considering.")
|
||||
expect(response.message.content.find((part) => part.type === "reasoning")).toMatchObject({
|
||||
providerMetadata: { "xai-responses": { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
|
||||
providerMetadata: { xai: { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -120,7 +120,7 @@ describe("xAI Responses route", () => {
|
||||
name: "x_search",
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
|
||||
providerMetadata: { xai: { itemId: "x_search_1" } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
@@ -148,7 +148,7 @@ describe("xAI Responses route", () => {
|
||||
name: item.type,
|
||||
result: { type: "json", value: item },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "xai-responses": { itemId: item.id } },
|
||||
providerMetadata: { xai: { itemId: item.id } },
|
||||
}),
|
||||
),
|
||||
}),
|
||||
@@ -181,11 +181,11 @@ describe("xAI Responses route", () => {
|
||||
name: "x_search",
|
||||
input: { query: "news" },
|
||||
providerExecuted: true,
|
||||
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
|
||||
providerMetadata: { xai: { itemId: "x_search_1" } },
|
||||
})
|
||||
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
|
||||
result: { type: "json", value: item },
|
||||
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
|
||||
providerMetadata: { xai: { itemId: "x_search_1" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -253,10 +253,10 @@ const assistantMessageFromResponse = (response: LLMResponse, step: AssistantStep
|
||||
if (step.reasoning === "openai-encrypted") {
|
||||
const reasoning = response.events.find(
|
||||
(event): event is Extract<LLMEvent, { readonly type: "reasoning-end" }> =>
|
||||
LLMEvent.is.reasoningEnd(event) && typeof event.providerMetadata?.["openai-responses"]?.itemId === "string",
|
||||
LLMEvent.is.reasoningEnd(event) && typeof event.providerMetadata?.openai?.itemId === "string",
|
||||
)
|
||||
if (!reasoning) throw new Error("OpenAI Responses did not return reasoning metadata")
|
||||
expect(reasoning.providerMetadata?.["openai-responses"]?.reasoningEncryptedContent).toEqual(expect.any(String))
|
||||
expect(reasoning.providerMetadata?.openai?.reasoningEncryptedContent).toEqual(expect.any(String))
|
||||
content.push({ type: "reasoning", text: response.reasoning, providerMetadata: reasoning.providerMetadata })
|
||||
}
|
||||
|
||||
|
||||
@@ -47,29 +47,14 @@ describe("Route.with", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.effect("namespaces emitted metadata by route ID independently of provider identity", () =>
|
||||
Effect.gen(function* () {
|
||||
const base = OpenAIChat.route.with({ provider: "azure", auth: Auth.none })
|
||||
for (const route of [base, base.with({ id: "custom-azure-chat" })]) {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({ model: route.model({ id: "test" }), prompt: "Hello" }),
|
||||
).pipe(
|
||||
Effect.provide(
|
||||
fixedResponse(
|
||||
sseEvents({
|
||||
choices: [{ delta: { content: "Hello" }, finish_reason: "stop" }],
|
||||
usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(response.usage?.providerMetadata).toEqual({
|
||||
[route.id]: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
|
||||
})
|
||||
expect(route).not.toHaveProperty("providerMetadataKey")
|
||||
}
|
||||
}),
|
||||
)
|
||||
test("assigns metadata ownership to a replacement provider and preserves explicit overrides", () => {
|
||||
const route = OpenAIChat.route.with({ provider: "azure" })
|
||||
const overridden = route.with({ providerMetadataKey: "custom-azure" }).with({ headers: { "x-test": "value" } })
|
||||
|
||||
expect(route.providerMetadataKey).toBe("azure")
|
||||
expect(overridden.providerMetadataKey).toBe("custom-azure")
|
||||
expect(overridden.defaults).not.toHaveProperty("providerMetadataKey")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Route diagnostics", () => {
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/session-ui": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
|
||||
@@ -4,6 +4,16 @@ export { useCommand } from "./shell/commands/command"
|
||||
export { currentRoute, type LayoutRoute, useCurrentRoute } from "./shell/state/layout"
|
||||
export { loadLocaleDict, normalizeLocale, type Locale, useLanguage } from "./runtime/i18n/language"
|
||||
export { type FatalRendererErrorLog, type Platform, PlatformProvider } from "./runtime/platform/platform"
|
||||
export type {
|
||||
BrowserPaneCommand,
|
||||
BrowserPaneEndpoint,
|
||||
BrowserPaneEvent,
|
||||
BrowserPaneLayout,
|
||||
BrowserPanePlatform,
|
||||
BrowserPaneRegistration,
|
||||
BrowserPaneState,
|
||||
BrowserPaneTarget,
|
||||
} from "./runtime/platform/browser-pane"
|
||||
export { ServerConnection, useServers } from "./runtime/server/registry"
|
||||
export { useTabs } from "./shell/tabs/tabs"
|
||||
export { createDraftStore } from "./runtime/persistence/drafts"
|
||||
|
||||
@@ -75,9 +75,14 @@ export function createNewSessionWorkspaceController(input: {
|
||||
if (event.type === "worktree.updated") void worktreeActions.refetch()
|
||||
}),
|
||||
)
|
||||
// `latest` only skips Suspense once the resource has resolved at least once. Before that it
|
||||
// behaves like a plain read, which holds the transition that opens the New Session tab until
|
||||
// the worktree list returns.
|
||||
const worktreesLoaded = () => worktrees.state === "ready" || worktrees.state === "refreshing"
|
||||
const worktreeItems = createMemo(() => {
|
||||
const project = currentProject()
|
||||
if (!project) return []
|
||||
if (!worktreesLoaded()) return project.worktrees
|
||||
const loaded = worktrees.latest
|
||||
return loaded?.projectID === project.id ? loaded.items : project.worktrees
|
||||
})
|
||||
@@ -110,10 +115,11 @@ export function createNewSessionWorkspaceController(input: {
|
||||
const project = currentProject()
|
||||
const worktree = input.selectedWorktree()
|
||||
if (!project || !worktree) return
|
||||
return isWorkspaceSelection(project, worktree) ||
|
||||
worktreeDirectories().some((item) => sameDirectory(item, worktree))
|
||||
? worktree
|
||||
: undefined
|
||||
if (isWorkspaceSelection(project, worktree)) return worktree
|
||||
// A saved choice may only exist in the server inventory. Keep it until the list can confirm it,
|
||||
// otherwise the selector falls back to Local while loading and a submit would target the wrong directory.
|
||||
if (!worktreesLoaded()) return worktree
|
||||
return worktreeDirectories().some((item) => sameDirectory(item, worktree)) ? worktree : undefined
|
||||
})
|
||||
const fallback = createMemo(() => {
|
||||
const project = currentProject()
|
||||
@@ -140,12 +146,19 @@ export function createNewSessionWorkspaceController(input: {
|
||||
.catch(() => ({ directory, search, data: [] })),
|
||||
)
|
||||
createEffect(() => {
|
||||
void Promise.all([data.location.syncInfo({ directory: sdk().directory }), data.project.sync()]).catch(
|
||||
() => undefined,
|
||||
)
|
||||
const project = currentProject()
|
||||
const directories = project ? [project.worktree, ...worktreeDirectories()] : [sdk().directory]
|
||||
directories.forEach((directory) => void data.location.vcs.sync({ directory }).catch(() => undefined))
|
||||
void Promise.all([
|
||||
data.location.syncInfo({ directory: sdk().directory }),
|
||||
data.project.sync(),
|
||||
data.location.vcs.sync({ directory: sdk().directory }),
|
||||
]).catch(() => undefined)
|
||||
})
|
||||
// Only the selected worktree feeds the branch label. Syncing every worktree in the inventory boots
|
||||
// each one on the server, which then emits `agent.updated` and makes the client run the full
|
||||
// catalog fan-out for every directory.
|
||||
createEffect(() => {
|
||||
const selection = value()
|
||||
if (selection === "main" || selection === "create") return
|
||||
void data.location.vcs.sync({ directory: selection }).catch(() => undefined)
|
||||
})
|
||||
const branch = createMemo(() =>
|
||||
resolveNewSessionBranch({
|
||||
@@ -169,12 +182,10 @@ export function createNewSessionWorkspaceController(input: {
|
||||
workspace: createMemo(() => {
|
||||
const project = currentProject()
|
||||
const current = value()
|
||||
return (
|
||||
current === "create" ||
|
||||
(!!project &&
|
||||
(isWorkspaceDirectory(project, current) ||
|
||||
worktreeDirectories().some((item) => sameDirectory(item, current))))
|
||||
)
|
||||
if (current === "create") return true
|
||||
if (current === "main" || !project) return false
|
||||
if (isWorkspaceDirectory(project, current) || !worktreesLoaded()) return true
|
||||
return worktreeDirectories().some((item) => sameDirectory(item, current))
|
||||
}),
|
||||
reset: () => {
|
||||
input.setSelectedWorktree(undefined)
|
||||
|
||||
@@ -667,6 +667,8 @@ export const dict = {
|
||||
"session.tab.usage": "Usage",
|
||||
"session.view.select": "Session view",
|
||||
"session.tab.review": "Review",
|
||||
"session.tab.browser": "Browser",
|
||||
"session.tab.add": "Add tab",
|
||||
"session.tab.context": "Context",
|
||||
"session.tab.unknown": "Unknown Session",
|
||||
"session.panel.reviewAndFiles": "Review and files",
|
||||
@@ -823,6 +825,10 @@ export const dict = {
|
||||
"PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.",
|
||||
"terminal.connectTicket.statusError": "PTY connect ticket failed with {{status}}",
|
||||
|
||||
"session.browser.address": "Browser address",
|
||||
"session.browser.replaced": "Browser control moved to another desktop window.",
|
||||
"session.browser.address.placeholder": "Enter a URL",
|
||||
|
||||
"titlebar.update": "Update",
|
||||
"titlebar.tabs": "Tabs",
|
||||
"titlebar.channel.local": "Local",
|
||||
@@ -1049,6 +1055,8 @@ export const dict = {
|
||||
"settings.general.row.uiFont.description": "Customise the font used throughout the interface",
|
||||
"settings.general.row.showFileTree.title": "File tree",
|
||||
"settings.general.row.showFileTree.description": "Show the file tree panel in sessions",
|
||||
"settings.general.row.browserPane.title": "Browser pane",
|
||||
"settings.general.row.browserPane.description": "Allow agents to open and control an in-app development browser.",
|
||||
"settings.general.row.showNavigation.title": "Navigation controls",
|
||||
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
|
||||
"settings.general.row.showSearch.title": "Command palette",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Browser } from "@opencode-ai/plugin-browser/rpc"
|
||||
|
||||
export type BrowserPaneEndpoint = Readonly<{ url: string; username?: string; password?: string }>
|
||||
export type BrowserPaneTarget = Readonly<{ sessionID: string; endpoint: BrowserPaneEndpoint }>
|
||||
export type BrowserPaneLayout = {
|
||||
tabID: Browser.TabID
|
||||
visible: boolean
|
||||
bounds?: { x: number; y: number; width: number; height: number }
|
||||
background?: readonly [number, number, number, number]
|
||||
radius?: number
|
||||
}
|
||||
|
||||
export type BrowserPaneCommand = Browser.Action
|
||||
export type BrowserPaneState = Browser.State | null
|
||||
export type BrowserPaneEvent =
|
||||
| { type: "focus"; tabID: Browser.TabID }
|
||||
| { type: "state"; state: BrowserPaneState; error?: string }
|
||||
|
||||
export type BrowserPaneRegistration = {
|
||||
setLayout(layout?: BrowserPaneLayout): void
|
||||
command(command: BrowserPaneCommand): Promise<void>
|
||||
close(): void
|
||||
}
|
||||
|
||||
export type BrowserPanePlatform = {
|
||||
register(target: BrowserPaneTarget, listener: (event: BrowserPaneEvent) => void): BrowserPaneRegistration
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { ServerConnection } from "@/runtime/server/registry"
|
||||
import type { WslServersPlatform } from "@/servers/wsl/types"
|
||||
import type { UpdaterPlatform } from "@/shell/updates/types"
|
||||
import type { DraftStore } from "@/runtime/persistence/drafts"
|
||||
import type { BrowserPanePlatform } from "./browser-pane"
|
||||
|
||||
type PickerPaths = string | string[] | null
|
||||
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
|
||||
@@ -118,6 +119,9 @@ type PlatformBase = {
|
||||
|
||||
/** Record a fatal renderer error in platform logs (desktop only) */
|
||||
recordFatalRendererError?(error: FatalRendererErrorLog): Promise<void>
|
||||
|
||||
/** Native browser pane hosted by the platform (desktop only). */
|
||||
browserPane?: BrowserPanePlatform
|
||||
}
|
||||
|
||||
export type Platform = PlatformBase &
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { batch, createEffect, createMemo, getOwner, onCleanup, runWithOwner } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import type { Browser } from "@opencode-ai/plugin-browser/rpc"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { BrowserPaneCommand, BrowserPaneRegistration, BrowserPaneState } from "@/runtime/platform/browser-pane"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import type { useServer } from "@/runtime/server/current"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { findSessionTab, tabKey, useTabs } from "@/shell/tabs/tabs"
|
||||
|
||||
type Server = ReturnType<typeof useServer>
|
||||
|
||||
export type BrowserAttachment = {
|
||||
registration?: BrowserPaneRegistration
|
||||
browser: BrowserPaneState
|
||||
error?: string
|
||||
}
|
||||
|
||||
type Live = {
|
||||
server: Server
|
||||
sessionID: string
|
||||
/** Shell tab that owns this attachment once seen; it may route to a child session later. */
|
||||
tab?: string
|
||||
registration?: BrowserPaneRegistration
|
||||
retry?: ReturnType<typeof setTimeout>
|
||||
attempts: number
|
||||
dispose: () => void
|
||||
}
|
||||
|
||||
// Attachments belong to the shell session tab, not the session route: native pages and the agent's
|
||||
// browser survive visiting Settings or another tab and close when the session tab or the setting does.
|
||||
export const { use: useBrowserAttachments, provider: BrowserAttachmentsProvider } = createSimpleContext({
|
||||
name: "BrowserAttachments",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const platform = usePlatform()
|
||||
const settings = useSettings()
|
||||
const language = useLanguage()
|
||||
const shellTabs = useTabs()
|
||||
const owner = getOwner()
|
||||
const [store, setStore] = createStore<Record<string, BrowserAttachment | undefined>>({})
|
||||
// Servers whose plugin lacks the browser RPC; sessions on them stop retrying.
|
||||
const [unsupported, setUnsupported] = createStore<Record<string, true | undefined>>({})
|
||||
const live = new Map<string, Live>()
|
||||
const focus = new Map<string, Set<(tabID: Browser.TabID) => void>>()
|
||||
const key = (server: Server, sessionID: string) => `${server.key}\n${sessionID}`
|
||||
const enabled = createMemo(
|
||||
() => !!platform.browserPane && settings.ready() && settings.general.experimentalBrowser(),
|
||||
)
|
||||
const close = (id: string) => {
|
||||
live.get(id)?.dispose()
|
||||
live.delete(id)
|
||||
setStore(id, undefined)
|
||||
}
|
||||
createEffect(() => {
|
||||
const on = enabled()
|
||||
const tabs = shellTabs.store
|
||||
// The store's keys mirror `live`, and reading them keeps this effect subscribed to new attachments.
|
||||
Object.keys(store).forEach((id) => {
|
||||
const entry = live.get(id)
|
||||
if (!entry) return
|
||||
// Tabs hydrate asynchronously, so the owner is learned when first seen rather than required up
|
||||
// front. A tab keeps owning the attachment while it exists, even after routing back to its parent.
|
||||
const current = findSessionTab(tabs, entry.server.key, entry.sessionID)
|
||||
if (current) entry.tab = tabKey(current)
|
||||
const owned = entry.tab === undefined || tabs.some((tab) => tabKey(tab) === entry.tab)
|
||||
if (on && owned && !entry.server.health?.incompatible) return
|
||||
close(id)
|
||||
})
|
||||
})
|
||||
onCleanup(() => Array.from(live.keys()).forEach(close))
|
||||
|
||||
return {
|
||||
enabled,
|
||||
supported: (server: Server) => !unsupported[server.key],
|
||||
state: (server: Server, sessionID: string) => store[key(server, sessionID)],
|
||||
attach(server: Server, sessionID: string) {
|
||||
const id = key(server, sessionID)
|
||||
if (live.has(id)) return
|
||||
const pane = platform.browserPane
|
||||
if (!pane || !enabled() || unsupported[server.key] || server.health?.incompatible) return
|
||||
const entry: Live = { server, sessionID, attempts: 0, dispose: () => undefined }
|
||||
live.set(id, entry)
|
||||
setStore(id, { browser: null })
|
||||
const register = () => {
|
||||
if (entry.registration || live.get(id) !== entry) return
|
||||
// The server's shared transport follows a restarted sidecar's port whether or not any route
|
||||
// for this session is mounted; the connection captured at attach time may predate it.
|
||||
const endpoint = { ...server.conn.http, url: server.ctx.sdk.url }
|
||||
const registration = pane.register({ sessionID, endpoint }, (event) => {
|
||||
if (live.get(id) !== entry) return
|
||||
if (event.type === "focus") return focus.get(id)?.forEach((listener) => listener(event.tabID))
|
||||
if (event.error === "browser.pane.unsupported") {
|
||||
setUnsupported(server.key, true)
|
||||
return close(id)
|
||||
}
|
||||
if (event.error === "browser.pane.replaced") {
|
||||
registration.close()
|
||||
entry.registration = undefined
|
||||
return setStore(id, {
|
||||
registration: undefined,
|
||||
browser: null,
|
||||
error: language.t("session.browser.replaced"),
|
||||
})
|
||||
}
|
||||
// The desktop dropped the attachment (server restart, attach race). Re-register so the
|
||||
// agent's browser tool comes back without a reload.
|
||||
if (event.error === "browser.pane.registration.closed") {
|
||||
registration.close()
|
||||
entry.registration = undefined
|
||||
setStore(id, { registration: undefined, browser: null, error: undefined })
|
||||
entry.retry = setTimeout(register, Math.min(30_000, 1_000 * 2 ** entry.attempts++))
|
||||
return
|
||||
}
|
||||
if (event.state) entry.attempts = 0
|
||||
batch(() => {
|
||||
setStore(id, "browser", reconcile(event.state))
|
||||
setStore(id, "error", event.error ? language.t("common.requestFailed") : undefined)
|
||||
})
|
||||
})
|
||||
entry.registration = registration
|
||||
setStore(id, { registration, browser: null, error: undefined })
|
||||
}
|
||||
// A new session appears in the UI before its server-side creation finishes. The listener
|
||||
// belongs to this provider, not to the route effect that happened to call attach().
|
||||
const data = server.ctx.data
|
||||
const unsubscribe = runWithOwner(owner, () =>
|
||||
data.on("session.created", (event) => {
|
||||
if (event.data.sessionID === sessionID) register()
|
||||
}),
|
||||
)
|
||||
if (!data.session.creating(sessionID)) register()
|
||||
entry.dispose = () => {
|
||||
unsubscribe?.()
|
||||
clearTimeout(entry.retry)
|
||||
entry.registration?.close()
|
||||
}
|
||||
},
|
||||
/** Desktop focus requests for a mounted session route; nothing is replayed to routes mounted later. */
|
||||
onFocus(server: Server, sessionID: string, listener: (tabID: Browser.TabID) => void) {
|
||||
const id = key(server, sessionID)
|
||||
const listeners = focus.get(id) ?? new Set()
|
||||
listeners.add(listener)
|
||||
focus.set(id, listeners)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
if (!listeners.size) focus.delete(id)
|
||||
}
|
||||
},
|
||||
command(server: Server, sessionID: string, command: BrowserPaneCommand) {
|
||||
const registration = live.get(key(server, sessionID))?.registration
|
||||
if (!registration) return Promise.reject(new Error("browser.pane.unavailable"))
|
||||
return registration.command(command)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
import { batch, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import type { Browser } from "@opencode-ai/plugin-browser/rpc"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { BrowserPaneCommand } from "@/runtime/platform/browser-pane"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import type { SessionModel } from "../model"
|
||||
import { isSessionBrowserTab, sessionBrowserTab } from "../helpers"
|
||||
import { useBrowserAttachments } from "./attachments"
|
||||
|
||||
export function createSessionBrowser(session: SessionModel) {
|
||||
const attachments = useBrowserAttachments()
|
||||
const language = useLanguage()
|
||||
const server = useServer()
|
||||
const [local, setLocal] = createStore({ error: undefined as string | undefined })
|
||||
const attachment = () => {
|
||||
const sessionID = session.identity.sessionID()
|
||||
return sessionID ? attachments.state(server, sessionID) : undefined
|
||||
}
|
||||
const available = createMemo(
|
||||
() =>
|
||||
attachments.enabled() &&
|
||||
attachments.supported(server) &&
|
||||
!!session.identity.sessionID() &&
|
||||
!server.health?.incompatible &&
|
||||
session.isDesktop(),
|
||||
)
|
||||
const attached = () => attachment()?.registration !== undefined
|
||||
const browserTabs = createMemo(
|
||||
() =>
|
||||
attachment()?.browser?.tabs.filter((tab) => session.layout.tabs().all().includes(sessionBrowserTab(tab.id))) ??
|
||||
[],
|
||||
)
|
||||
const focus = (tabID: Browser.TabID) => {
|
||||
session.layout.view().reviewPanel.open()
|
||||
const tabs = session.layout.tabs()
|
||||
const key = sessionBrowserTab(tabID)
|
||||
if (!tabs.all().includes(key)) tabs.setAll([...tabs.all(), key])
|
||||
tabs.setActive(key)
|
||||
}
|
||||
const command = (command: BrowserPaneCommand) => {
|
||||
const sessionID = session.identity.sessionID()
|
||||
if (!sessionID) return
|
||||
setLocal("error", undefined)
|
||||
const owner = session.ownership.capture()
|
||||
void attachments.command(server, sessionID, command).catch(() => {
|
||||
if (owner.current()) setLocal("error", language.t("common.requestFailed"))
|
||||
})
|
||||
}
|
||||
createEffect(() => {
|
||||
const sessionID = session.identity.sessionID()
|
||||
if (!sessionID) return
|
||||
if (attachments.enabled()) attachments.attach(server, sessionID)
|
||||
onCleanup(attachments.onFocus(server, sessionID, focus))
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => session.layout.tabs().active(),
|
||||
(active) => {
|
||||
const tab = attachment()?.browser?.tabs.find((tab) => sessionBrowserTab(tab.id) === active)
|
||||
if (tab && tab.id !== attachment()?.browser?.focusedTabID) command({ type: "tabs.focus", tabID: tab.id })
|
||||
},
|
||||
),
|
||||
)
|
||||
createEffect(
|
||||
on(
|
||||
() => session.layout.tabs().all(),
|
||||
(current, previous) => {
|
||||
previous
|
||||
?.filter((key) => isSessionBrowserTab(key) && !current.includes(key))
|
||||
.forEach((key) => {
|
||||
const tab = attachment()?.browser?.tabs.find((tab) => sessionBrowserTab(tab.id) === key)
|
||||
if (tab) command({ type: "tabs.close", tabID: tab.id })
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
// Mirror the desktop's tab inventory into this session's layout tabs. Only tabs new since the last
|
||||
// inventory are added, so a layout tab the user just closed is not reopened before the desktop confirms.
|
||||
createEffect(
|
||||
on(
|
||||
() => attachment()?.browser?.tabs.map((tab) => sessionBrowserTab(tab.id)),
|
||||
(ids, previous) => {
|
||||
if (!ids) return
|
||||
const known = new Set(previous ?? [])
|
||||
batch(() => {
|
||||
const tabs = session.layout.tabs()
|
||||
tabs
|
||||
.all()
|
||||
.filter((key) => isSessionBrowserTab(key) && !ids.includes(key))
|
||||
.forEach(tabs.close)
|
||||
const current = tabs.all()
|
||||
const added = ids.filter((key) => !known.has(key) && !current.includes(key))
|
||||
if (added.length) tabs.setAll([...current, ...added])
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
return {
|
||||
available,
|
||||
attached,
|
||||
opened: () => attached() && browserTabs().length > 0,
|
||||
state: () => attachment()?.browser ?? null,
|
||||
tabs: browserTabs,
|
||||
active: () =>
|
||||
browserTabs().find((tab) => sessionBrowserTab(tab.id) === session.layout.tabs().active()) ??
|
||||
browserTabs().find((tab) => tab.id === attachment()?.browser?.focusedTabID) ??
|
||||
browserTabs()[0],
|
||||
error: () => local.error ?? attachment()?.error,
|
||||
registration: () => attachment()?.registration,
|
||||
close: (tabID: Browser.TabID) => session.layout.tabs().close(sessionBrowserTab(tabID)),
|
||||
open: () => command({ type: "tabs.open" }),
|
||||
command,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Loader } from "@opencode-ai/ui/loader"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { createEventListener } from "@solid-primitives/event-listener"
|
||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||
import { createEffect, For, on, onCleanup, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import type { BrowserPaneRegistration } from "@/runtime/platform/browser-pane"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import type { createSessionBrowser } from "./model"
|
||||
|
||||
export function SessionBrowserPane(props: {
|
||||
registration: BrowserPaneRegistration
|
||||
browser: ReturnType<typeof createSessionBrowser>
|
||||
visible: boolean
|
||||
}) {
|
||||
const platform = usePlatform()
|
||||
const language = useLanguage()
|
||||
const dialog = useDialog()
|
||||
const state = props.browser.active
|
||||
const button = { variant: "ghost", size: "large" } as const
|
||||
const [store, setStore] = createStore({
|
||||
address: "",
|
||||
editing: false,
|
||||
visible: typeof document === "undefined" || document.visibilityState === "visible",
|
||||
})
|
||||
let surface: HTMLDivElement | undefined
|
||||
let frame: number | undefined
|
||||
let layout: string | undefined
|
||||
let until = 0
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = canvas.height = 1
|
||||
const paint = canvas.getContext("2d", { willReadFrequently: true })
|
||||
|
||||
// The native page always paints above the DOM, so hide it while a floating
|
||||
// menu, select, or popover overlaps it. Tooltips are excluded.
|
||||
const covered = (rect: DOMRect) =>
|
||||
Array.from(document.querySelectorAll('[data-popper-positioner]:not(:has([role="tooltip"]))')).some((el) => {
|
||||
const r = el.getBoundingClientRect()
|
||||
return r.width > 0 && r.left < rect.right && r.right > rect.left && r.top < rect.bottom && r.bottom > rect.top
|
||||
})
|
||||
const measure = () => {
|
||||
if (!surface) return
|
||||
const tab = state()
|
||||
if (!tab) {
|
||||
props.registration.setLayout()
|
||||
return
|
||||
}
|
||||
const rect = surface.getBoundingClientRect()
|
||||
const zoom = platform.webviewZoom?.() ?? 1
|
||||
const left = Math.round(rect.left * zoom)
|
||||
const top = Math.round(rect.top * zoom)
|
||||
const right = Math.round(rect.right * zoom)
|
||||
const bottom = Math.round(rect.bottom * zoom)
|
||||
const visible = props.visible && store.visible && !dialog.active && !covered(rect)
|
||||
// The cutout exposes the app backdrop outside the rounded Review card,
|
||||
// not the browser surface inside it.
|
||||
const color = getComputedStyle(
|
||||
surface.closest(".bg-v2-background-bg-deep") ?? document.documentElement,
|
||||
).backgroundColor
|
||||
const next = `${tab.id}:${visible}:${left}:${top}:${right}:${bottom}:${color}:${window.devicePixelRatio}`
|
||||
if (next !== layout) {
|
||||
layout = next
|
||||
// Let the browser resolve the semantic backdrop color, including custom
|
||||
// themes using color formats that Electron's color parser cannot read.
|
||||
if (paint) {
|
||||
paint.clearRect(0, 0, 1, 1)
|
||||
paint.fillStyle = color
|
||||
paint.fillRect(0, 0, 1, 1)
|
||||
}
|
||||
const rgba = paint?.getImageData(0, 0, 1, 1).data
|
||||
props.registration.setLayout({
|
||||
tabID: tab.id,
|
||||
visible,
|
||||
bounds: { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) },
|
||||
background: rgba ? [rgba[0], rgba[1], rgba[2], rgba[3]] : undefined,
|
||||
radius: Math.round(10 * zoom),
|
||||
})
|
||||
}
|
||||
}
|
||||
const tick = () => {
|
||||
frame = undefined
|
||||
measure()
|
||||
if (performance.now() < until) frame = requestAnimationFrame(tick)
|
||||
}
|
||||
const schedule = (duration = 0) => {
|
||||
until = Math.max(until, performance.now() + duration)
|
||||
if (frame === undefined) frame = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
createEffect(() => !store.editing && setStore("address", state()?.url ?? ""))
|
||||
createEffect(
|
||||
on(
|
||||
[
|
||||
() => platform.webviewZoom?.(),
|
||||
() => dialog.active,
|
||||
() => store.visible,
|
||||
() => props.visible,
|
||||
() => state()?.id,
|
||||
],
|
||||
() => schedule(300),
|
||||
),
|
||||
)
|
||||
// ResizeObserver runs after layout in the same frame; measuring here instead of on the next
|
||||
// animation frame keeps the native view in step with a pane drag.
|
||||
createResizeObserver(() => surface, measure)
|
||||
createEventListener(window, "resize", () => schedule(300))
|
||||
// Floating content portals directly into <body>; keep measuring briefly so
|
||||
// the positioner has settled before the overlap check runs.
|
||||
const portals = new MutationObserver(() => schedule(300))
|
||||
portals.observe(document.body, { childList: true })
|
||||
onCleanup(() => portals.disconnect())
|
||||
const appearance = new MutationObserver(() => schedule(300))
|
||||
appearance.observe(document.documentElement, { attributes: true, attributeFilter: ["style", "data-theme"] })
|
||||
onCleanup(() => appearance.disconnect())
|
||||
createEventListener(window.matchMedia("(prefers-color-scheme: dark)"), "change", () => schedule(300))
|
||||
createEventListener(document, "visibilitychange", () => setStore("visible", document.visibilityState === "visible"))
|
||||
onCleanup(() => {
|
||||
if (frame !== undefined) cancelAnimationFrame(frame)
|
||||
props.registration.setLayout()
|
||||
})
|
||||
|
||||
return (
|
||||
<aside id="browser-panel" class="relative size-full min-w-0 overflow-hidden bg-v2-background-bg-base flex flex-col">
|
||||
<div class="h-10 shrink-0 flex items-center gap-1 px-2 border-b border-v2-border-border-muted bg-v2-background-bg-layer-02">
|
||||
<For each={["back", "forward"] as const}>
|
||||
{(direction) => (
|
||||
<IconButton
|
||||
{...button}
|
||||
disabled={!state()?.[direction === "back" ? "canGoBack" : "canGoForward"]}
|
||||
aria-label={language.t(direction === "back" ? "common.goBack" : "common.goForward")}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: direction, tabID: tab.id })
|
||||
}}
|
||||
icon={<Icon name={direction === "back" ? "chevron-left" : "chevron-right"} size="small" />}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<IconButton
|
||||
{...button}
|
||||
disabled={!state()}
|
||||
aria-label={language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")}
|
||||
onClick={() => {
|
||||
const tab = state()
|
||||
if (tab) props.browser.command({ type: tab.loading ? "stop" : "reload", tabID: tab.id })
|
||||
}}
|
||||
icon={
|
||||
<Show when={state()?.loading} fallback={<Icon name="reset" size="small" />}>
|
||||
<Loader />
|
||||
</Show>
|
||||
}
|
||||
/>
|
||||
<form
|
||||
class="min-w-0 flex-1"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
const tab = state()
|
||||
if (tab && store.address.trim())
|
||||
props.browser.command({ type: "navigate", tabID: tab.id, url: store.address })
|
||||
}}
|
||||
>
|
||||
<input
|
||||
class="w-full h-7 px-2 rounded-md border border-v2-border-border-muted bg-v2-background-bg-base text-12-regular text-v2-text-text-base outline-none focus:border-v2-border-border-focus"
|
||||
value={store.address}
|
||||
disabled={!state()}
|
||||
placeholder={language.t("session.browser.address.placeholder")}
|
||||
aria-label={language.t("session.browser.address")}
|
||||
onFocus={() => setStore("editing", true)}
|
||||
onBlur={() => setStore({ editing: false, address: state()?.url ?? "" })}
|
||||
onInput={(event) => setStore("address", event.currentTarget.value)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<Show when={props.browser.error()}>
|
||||
<div class="shrink-0 px-3 py-1.5 text-12-regular text-text-danger-base border-b border-v2-border-border-muted">
|
||||
{props.browser.error()}
|
||||
</div>
|
||||
</Show>
|
||||
<div ref={surface} class="min-h-0 flex-1 bg-v2-background-bg-base" />
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { Menu } from "@opencode-ai/ui/menu"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
|
||||
import FileTree from "@/session/files/file-tree"
|
||||
@@ -20,6 +21,8 @@ import { SessionContextUsage } from "@/session/timeline/session-context-usage"
|
||||
|
||||
const reviewTabID = "session-side-panel-review-tab"
|
||||
const reviewTabPanelID = "session-side-panel-review-tabpanel"
|
||||
const browserTabID = "session-side-panel-browser-tab"
|
||||
const browserTabPanelID = "session-side-panel-browser-tabpanel"
|
||||
const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel"
|
||||
import { SessionContextTab } from "@/session/files/session-context-tab"
|
||||
import { SortableTab } from "@/session/files/tab"
|
||||
@@ -33,6 +36,8 @@ import { useSettings } from "@/settings/model"
|
||||
import { createFileTabListSync } from "@/session/files/file-tab-scroll"
|
||||
import {
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
isSessionBrowserTab,
|
||||
sessionBrowserTab,
|
||||
createOpenSessionFileTab,
|
||||
createSessionTabs,
|
||||
shouldShowFileTree,
|
||||
@@ -41,6 +46,8 @@ import {
|
||||
import { setSessionHandoff } from "@/session/handoff"
|
||||
import { useSessionLayout } from "@/session/session-layout"
|
||||
import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/session/files/session-file-browser-tab"
|
||||
import { SessionBrowserPane } from "@/session/browser/pane"
|
||||
import type { createSessionBrowser } from "@/session/browser/model"
|
||||
|
||||
type ReviewDiff = FileDiffInfo
|
||||
type RenderDiff = FileDiffInfo
|
||||
@@ -65,6 +72,7 @@ export function SessionSidePanel(props: {
|
||||
reviewPresent?: boolean
|
||||
size: Sizing
|
||||
stacked?: boolean
|
||||
browser: ReturnType<typeof createSessionBrowser>
|
||||
}) {
|
||||
const layout = useLayout()
|
||||
const settings = useSettings()
|
||||
@@ -165,6 +173,7 @@ export function SessionSidePanel(props: {
|
||||
review: reviewTab,
|
||||
hasReview: () => props.canReview,
|
||||
fileBrowser: () => true,
|
||||
browser: props.browser.attached,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
const openFileOpen = tabState.openFileOpen
|
||||
@@ -202,7 +211,7 @@ export function SessionSidePanel(props: {
|
||||
openReviewPanel()
|
||||
tabs().setActive(next)
|
||||
}
|
||||
const browserTab = createMemo(() => {
|
||||
const fileTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (active === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB
|
||||
if (active && file.pathFromTab(active)) return active
|
||||
@@ -212,11 +221,11 @@ export function SessionSidePanel(props: {
|
||||
// selects Review while the tab For replaces a preview trigger, which would
|
||||
// otherwise dispose the sidebar and reset scroll.
|
||||
const fileBrowserMounted = createMemo(() => {
|
||||
return openedTabs().length > 0 || openFileOpen() || !!browserTab()
|
||||
return openedTabs().length > 0 || openFileOpen() || !!fileTab()
|
||||
})
|
||||
const fileBrowserVisible = createMemo(() => {
|
||||
const active = activeTab()
|
||||
return active !== "review" && active !== "context" && active !== "empty"
|
||||
return active !== "review" && active !== "context" && active !== "empty" && !isSessionBrowserTab(active)
|
||||
})
|
||||
const openFileKeybind = createMemo(() => command.keybindParts("file.open"))
|
||||
const closeTabKeybind = createMemo(() => command.keybindParts("file.close"))
|
||||
@@ -298,7 +307,7 @@ export function SessionSidePanel(props: {
|
||||
onCleanup(stop)
|
||||
}}
|
||||
>
|
||||
<div class="session-review-v2-sidebar-toggle-slot h-full shrink-0 sticky left-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||
<div class="session-review-v2-sidebar-toggle-slot h-full shrink-0 sticky start-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||
{props.reviewSidebarToggle(activeTab() === SESSION_OPEN_FILE_TAB)}
|
||||
</div>
|
||||
<Show when={reviewTab() && props.canReview}>
|
||||
@@ -345,8 +354,7 @@ export function SessionSidePanel(props: {
|
||||
</Show>
|
||||
<For each={panelTabs()}>
|
||||
{(tab) => (
|
||||
<Show
|
||||
when={tab === SESSION_OPEN_FILE_TAB}
|
||||
<Switch
|
||||
fallback={
|
||||
<SortableTab
|
||||
tab={tab}
|
||||
@@ -357,73 +365,143 @@ export function SessionSidePanel(props: {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Tabs.Trigger
|
||||
value={SESSION_OPEN_FILE_TAB}
|
||||
class="group"
|
||||
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||
closeButton={
|
||||
<Tooltip
|
||||
value={
|
||||
<>
|
||||
{language.t("common.closeTab")}
|
||||
<Show when={closeTabKeybind().length > 0}>
|
||||
<Keybind keys={closeTabKeybind()} variant="neutral" />
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
class="hover-reveal relative z-10 group-hover:opacity-100"
|
||||
classList={{ "opacity-100": activeTab() === SESSION_OPEN_FILE_TAB }}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
tabs().close(SESSION_OPEN_FILE_TAB)
|
||||
}}
|
||||
icon={<Icon name="xmark-small" />}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="file-tree" size="small" />
|
||||
<span>{language.t("command.file.open")}</span>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Match when={isSessionBrowserTab(tab)}>
|
||||
<Show when={props.browser.tabs().find((item) => sessionBrowserTab(item.id) === tab)}>
|
||||
{(item) => (
|
||||
<SortableTab
|
||||
tab={tab}
|
||||
index={tabs().all().indexOf(tab)}
|
||||
onTabClose={() => props.browser.close(item().id)}
|
||||
id={`${browserTabID}-${item().id}`}
|
||||
ariaControls={activeTab() === tab ? browserTabPanelID : undefined}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="window-cursor" size="small" />
|
||||
<span class="max-w-40 truncate">
|
||||
{item().title || language.t("session.tab.browser")}
|
||||
</span>
|
||||
</div>
|
||||
</SortableTab>
|
||||
)}
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={tab === SESSION_OPEN_FILE_TAB}>
|
||||
<Tabs.Trigger
|
||||
value={SESSION_OPEN_FILE_TAB}
|
||||
class="group"
|
||||
onMiddleClick={() => tabs().close(SESSION_OPEN_FILE_TAB)}
|
||||
closeButton={
|
||||
<Tooltip
|
||||
value={
|
||||
<>
|
||||
{language.t("common.closeTab")}
|
||||
<Show when={closeTabKeybind().length > 0}>
|
||||
<Keybind keys={closeTabKeybind()} variant="neutral" />
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
gutter={10}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost-muted"
|
||||
class="hover-reveal relative z-10 group-hover:opacity-100"
|
||||
classList={{ "opacity-100": activeTab() === SESSION_OPEN_FILE_TAB }}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
tabs().close(SESSION_OPEN_FILE_TAB)
|
||||
}}
|
||||
icon={<Icon name="xmark-small" />}
|
||||
aria-label={language.t("common.closeTab")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="file-tree" size="small" />
|
||||
<span>{language.t("command.file.open")}</span>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
<div class="h-full shrink-0 sticky right-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||
<Tooltip
|
||||
value={
|
||||
<>
|
||||
{language.t("command.file.open")}
|
||||
<Show when={openFileKeybind().length > 0}>
|
||||
<Keybind keys={openFileKeybind()} variant="neutral" />
|
||||
</Show>
|
||||
</>
|
||||
<div class="h-full shrink-0 sticky end-0 z-10 flex items-center justify-center bg-v2-background-bg-base">
|
||||
{/* With only files to add, the plus stays a one-click "Open file" button. */}
|
||||
<Show
|
||||
when={props.browser.available()}
|
||||
fallback={
|
||||
<Tooltip
|
||||
value={
|
||||
<>
|
||||
{language.t("command.file.open")}
|
||||
<Show when={openFileKeybind().length > 0}>
|
||||
<Keybind keys={openFileKeybind()} variant="neutral" />
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
placement="bottom"
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon={<Icon name="plus" />}
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
onClick={() => openFileBrowser()}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="bottom"
|
||||
class="flex items-center"
|
||||
>
|
||||
<IconButton
|
||||
icon={<Icon name="plus" />}
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
onClick={() => openFileBrowser()}
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
value={language.t("session.tab.add")}
|
||||
placement="bottom"
|
||||
class="flex items-center"
|
||||
>
|
||||
<Menu appearance="standard" modal={false} placement="bottom-end" gutter={4}>
|
||||
<Menu.Trigger
|
||||
as={IconButton}
|
||||
icon={<Icon name="plus" />}
|
||||
variant="ghost-muted"
|
||||
size="large"
|
||||
aria-label={language.t("session.tab.add")}
|
||||
// The tablist redirects focus entering it to the selected
|
||||
// tab, which counts as focus-outside and closes the menu.
|
||||
onPointerDown={(event: PointerEvent) => event.preventDefault()}
|
||||
/>
|
||||
<Menu.Portal>
|
||||
<Menu.Content>
|
||||
<Menu.Item
|
||||
onSelect={openFileBrowser}
|
||||
shortcut={
|
||||
<Show when={openFileKeybind().length > 0}>
|
||||
<Keybind keys={openFileKeybind()} variant="neutral" />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="open-file" size="small" />
|
||||
<span>{language.t("command.file.open")}</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item onSelect={props.browser.open}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="window-cursor" size="small" />
|
||||
<span>{language.t("session.tab.browser")}</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu.Content>
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
<div
|
||||
@@ -474,6 +552,30 @@ export function SessionSidePanel(props: {
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
|
||||
<Show when={props.browser.opened()}>
|
||||
<div
|
||||
id={browserTabPanelID}
|
||||
role="tabpanel"
|
||||
aria-labelledby={
|
||||
props.browser.active() ? `${browserTabID}-${props.browser.active()?.id}` : undefined
|
||||
}
|
||||
data-slot="tabs-content"
|
||||
class="h-full min-h-0 overflow-hidden"
|
||||
classList={{ hidden: !isSessionBrowserTab(activeTab()) }}
|
||||
inert={!isSessionBrowserTab(activeTab()) || undefined}
|
||||
>
|
||||
<Show when={props.browser.registration()} keyed>
|
||||
{(registration) => (
|
||||
<SessionBrowserPane
|
||||
registration={registration}
|
||||
browser={props.browser}
|
||||
visible={isSessionBrowserTab(activeTab())}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={fileBrowserMounted()}>
|
||||
<div
|
||||
id={fileBrowserTabPanelID}
|
||||
@@ -484,11 +586,11 @@ export function SessionSidePanel(props: {
|
||||
inert={!fileBrowserVisible() || undefined}
|
||||
>
|
||||
<SessionFileBrowserTab
|
||||
tab={browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB}
|
||||
tab={fileTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB}
|
||||
placeholder={
|
||||
(browserTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB) === SESSION_OPEN_FILE_TAB
|
||||
(fileTab() ?? activeFileTab() ?? SESSION_OPEN_FILE_TAB) === SESSION_OPEN_FILE_TAB
|
||||
}
|
||||
active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")}
|
||||
active={file.pathFromTab(fileTab() ?? activeFileTab() ?? "")}
|
||||
kinds={kinds()}
|
||||
state={props.fileBrowserState}
|
||||
onSelect={(path) => previewTab(file.tab(path))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemo, Show } from "solid-js"
|
||||
import { children, createMemo, Show } from "solid-js"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSortable } from "@dnd-kit/solid/sortable"
|
||||
import { Keybind } from "@opencode-ai/ui/keybind"
|
||||
@@ -15,6 +15,10 @@ export function SortableTab(props: {
|
||||
temporary?: boolean
|
||||
onTabClose: (tab: string) => void
|
||||
onTabDoubleClick?: (tab: string) => void
|
||||
/** Replaces the file visual for non-file tabs such as the browser. */
|
||||
children?: JSX.Element
|
||||
id?: string
|
||||
ariaControls?: string
|
||||
}): JSX.Element {
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
@@ -29,6 +33,7 @@ export function SortableTab(props: {
|
||||
},
|
||||
})
|
||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||
const custom = children(() => props.children)
|
||||
const content = createMemo(() => {
|
||||
const value = path()
|
||||
if (!value) return
|
||||
@@ -39,6 +44,8 @@ export function SortableTab(props: {
|
||||
<div class="relative">
|
||||
<Tabs.Trigger
|
||||
value={props.tab}
|
||||
id={props.id}
|
||||
aria-controls={props.ariaControls}
|
||||
onMiddleClick={() => props.onTabClose(props.tab)}
|
||||
onDblClick={() => props.onTabDoubleClick?.(props.tab)}
|
||||
closeButton={
|
||||
@@ -63,7 +70,9 @@ export function SortableTab(props: {
|
||||
}
|
||||
hideCloseButton
|
||||
>
|
||||
<Show when={content()}>{(value) => value()}</Show>
|
||||
<Show when={custom()} fallback={<Show when={content()}>{(value) => value()}</Show>}>
|
||||
{custom()}
|
||||
</Show>
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, test } from "bun:test"
|
||||
import { createMemo, createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import {
|
||||
SESSION_BROWSER_TAB,
|
||||
sessionBrowserTab,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
createOpenReviewFile,
|
||||
createOpenSessionFileTab,
|
||||
@@ -233,4 +235,49 @@ describe("createSessionTabs", () => {
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("exposes one browser tab without treating it as a file tab", () => {
|
||||
createRoot((dispose) => {
|
||||
const tabs = createMemo(() => ({ active: () => SESSION_BROWSER_TAB, all: () => [SESSION_BROWSER_TAB] }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: () => undefined,
|
||||
normalizeTab: (tab) => tab,
|
||||
browser: () => true,
|
||||
})
|
||||
|
||||
expect(result.panelTabs()).toEqual([SESSION_BROWSER_TAB])
|
||||
expect(result.openedTabs()).toEqual([])
|
||||
expect(result.activeTab()).toBe(SESSION_BROWSER_TAB)
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBe(SESSION_BROWSER_TAB)
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("keeps browser tabs in layout order beside file tabs, and drops them when the browser is detached", () => {
|
||||
const first = sessionBrowserTab("first")
|
||||
const second = sessionBrowserTab("second")
|
||||
const input = {
|
||||
tabs: () => ({ active: () => second, all: () => [first, "file://src/a.ts", second] }),
|
||||
pathFromTab: (tab: string) => (tab.startsWith("file://") ? tab.slice(7) : undefined),
|
||||
normalizeTab: (tab: string) => tab,
|
||||
}
|
||||
createRoot((dispose) => {
|
||||
const result = createSessionTabs({ ...input, browser: () => true })
|
||||
expect(result.panelTabs()).toEqual([first, "file://src/a.ts", second])
|
||||
expect(result.openedTabs()).toEqual(["file://src/a.ts"])
|
||||
expect(result.activeTab()).toBe(second)
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBe(second)
|
||||
dispose()
|
||||
})
|
||||
createRoot((dispose) => {
|
||||
const result = createSessionTabs({ ...input, browser: () => false })
|
||||
expect(result.panelTabs()).toEqual(["file://src/a.ts"])
|
||||
expect(result.activeTab()).toBe("file://src/a.ts")
|
||||
expect(result.closableTab()).toBe("file://src/a.ts")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,14 @@ import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { makeEventListener } from "@solid-primitives/event-listener"
|
||||
import { same } from "@/runtime/persistence/equality"
|
||||
import { SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
import { isSessionBrowserTab, SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
|
||||
export { SESSION_OPEN_FILE_TAB } from "@/shell/state/session-tabs"
|
||||
export {
|
||||
SESSION_BROWSER_TAB,
|
||||
SESSION_OPEN_FILE_TAB,
|
||||
sessionBrowserTab,
|
||||
isSessionBrowserTab,
|
||||
} from "@/shell/state/session-tabs"
|
||||
|
||||
const emptyTabs: string[] = []
|
||||
|
||||
@@ -20,6 +25,7 @@ type TabsInput = {
|
||||
review?: Accessor<boolean>
|
||||
hasReview?: Accessor<boolean>
|
||||
fileBrowser?: Accessor<boolean>
|
||||
browser?: Accessor<boolean>
|
||||
}
|
||||
|
||||
export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) {
|
||||
@@ -30,6 +36,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
const review = input.review ?? (() => false)
|
||||
const hasReview = input.hasReview ?? (() => false)
|
||||
const fileBrowser = input.fileBrowser ?? (() => false)
|
||||
const browser = input.browser ?? (() => false)
|
||||
const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context"))
|
||||
const openFileOpen = createMemo(
|
||||
() =>
|
||||
@@ -44,6 +51,7 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
.all()
|
||||
.flatMap((tab) => {
|
||||
if (tab === "context" || tab === "review") return []
|
||||
if (isSessionBrowserTab(tab)) return browser() ? [tab] : []
|
||||
if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return []
|
||||
const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab
|
||||
if (seen.has(value)) return []
|
||||
@@ -54,13 +62,16 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
emptyTabs,
|
||||
{ equals: same },
|
||||
)
|
||||
const openedTabs = createMemo(() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB), emptyTabs, {
|
||||
equals: same,
|
||||
})
|
||||
const openedTabs = createMemo(
|
||||
() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB && !isSessionBrowserTab(tab)),
|
||||
emptyTabs,
|
||||
{ equals: same },
|
||||
)
|
||||
const activeTab = createMemo(() => {
|
||||
const active = input.tabs().active()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (active === "review" && review()) return active
|
||||
if (active && input.pathFromTab(active)) return input.normalizeTab(active)
|
||||
|
||||
@@ -70,15 +81,16 @@ export const createSessionTabs = (input: TabsInput) => {
|
||||
if (review() && hasReview()) return "review"
|
||||
return "empty"
|
||||
})
|
||||
const activeFileTab = createMemo(() => {
|
||||
const activeFileTab = createMemo<string | undefined>(() => {
|
||||
const active = activeTab()
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
const closableTab = createMemo(() => {
|
||||
const closableTab = createMemo<string | undefined>(() => {
|
||||
const active = activeTab()
|
||||
if (active === "context") return active
|
||||
if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active
|
||||
if (active && isSessionBrowserTab(active) && browser()) return active
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useData } from "@/runtime/server/current"
|
||||
import { same } from "@/runtime/persistence/equality"
|
||||
import { containsDirectory, isProjectDirectory, isWorkspaceDirectory } from "@/workspaces/paths"
|
||||
import { projectForSession } from "@/shell/layout/helpers"
|
||||
import { useBrowserAttachments } from "./browser/attachments"
|
||||
import { createSessionTabs } from "./helpers"
|
||||
import {
|
||||
normalizeSessionTab,
|
||||
@@ -28,6 +29,7 @@ export function useSessionModel() {
|
||||
const data = useData()
|
||||
const server = useServer()
|
||||
const shellTabs = useTabs()
|
||||
const attachments = useBrowserAttachments()
|
||||
const layout = useSessionLayout()
|
||||
const location = useWorkspaceLocation()
|
||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||
@@ -82,6 +84,11 @@ export function useSessionModel() {
|
||||
review: isDesktop,
|
||||
hasReview: canReview,
|
||||
fileBrowser: () => isDesktop() && !!sessionID(),
|
||||
// Same flag the side panel uses, so keyboard tab commands see the browser tab the panel shows.
|
||||
browser: () => {
|
||||
const id = sessionID()
|
||||
return !!id && attachments.state(server, id)?.registration !== undefined
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SessionSidePanel } from "../files/session-side-panel"
|
||||
import { ReviewPanel } from "./panel"
|
||||
import { SessionReviewTab } from "./review-tab"
|
||||
import type { ChangeMode, SessionReviewModel } from "./model"
|
||||
import type { createSessionBrowser } from "../browser/model"
|
||||
|
||||
const StatusDrawer = lazy(async () => {
|
||||
const { StatusDrawer } = await import("@/shell/status/status-drawer")
|
||||
@@ -144,7 +145,11 @@ export function SessionMobileReview(props: { review: SessionReviewModel }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionDesktopReview(props: { review: SessionReviewModel; present?: boolean }) {
|
||||
export function SessionDesktopReview(props: {
|
||||
review: SessionReviewModel
|
||||
browser: ReturnType<typeof createSessionBrowser>
|
||||
present?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Suspense>
|
||||
<SessionSidePanel
|
||||
@@ -168,6 +173,7 @@ export function SessionDesktopReview(props: { review: SessionReviewModel; presen
|
||||
reviewPresent={props.present}
|
||||
size={props.review.screen.size}
|
||||
stacked={props.review.screen.side.layout().stacked}
|
||||
browser={props.browser}
|
||||
/>
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ActiveSessionComposerRegion, createActiveSessionRegion } from "./compos
|
||||
import { SessionIdentityHeader } from "./session-identity-header"
|
||||
import { SessionReviewToggle } from "./header/session-header-actions"
|
||||
import { createAnimatedPresence } from "@/runtime/animated-presence"
|
||||
import { createSessionBrowser } from "./browser/model"
|
||||
|
||||
const SessionMobileFiles = lazy(async () => {
|
||||
const { SessionMobileFiles } = await import("./files/session-mobile-files")
|
||||
@@ -47,6 +48,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
return info ? projectForSession(info, server.ctx.sync.data.project) : undefined
|
||||
})
|
||||
const isDesktop = session.isDesktop
|
||||
const browser = createSessionBrowser(session)
|
||||
const screen = createSessionScreenLayout(session)
|
||||
const timeline = createSessionTimelineInteraction(session)
|
||||
const timelineSearch = createTimelineSearchController({
|
||||
@@ -377,7 +379,7 @@ export function SessionScreen(props: { session: SessionModel }) {
|
||||
setStore("sideReviewPresent", false)
|
||||
}}
|
||||
>
|
||||
<SessionDesktopReview review={review} present={store.sideReviewPresent} />
|
||||
<SessionDesktopReview review={review} browser={browser} present={store.sideReviewPresent} />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, Show } from "solid-js"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { SettingsRow } from "@/settings/row"
|
||||
@@ -12,6 +13,7 @@ const tabLayoutOptions: ("horizontal" | "vertical")[] = ["horizontal", "vertical
|
||||
export const SettingsExperimental: Component = () => {
|
||||
const language = useLanguage()
|
||||
const settings = useSettings()
|
||||
const platform = usePlatform()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,6 +31,22 @@ export const SettingsExperimental: Component = () => {
|
||||
<div class="settings-tab-body">
|
||||
<div class="settings-section">
|
||||
<SettingsList>
|
||||
<Show when={platform.browserPane}>
|
||||
<SettingsRow
|
||||
title={language.t("settings.general.row.browserPane.title")}
|
||||
description={language.t("settings.general.row.browserPane.description")}
|
||||
>
|
||||
<div data-action="settings-experimental-browser">
|
||||
<Switch
|
||||
checked={settings.general.experimentalBrowser()}
|
||||
onChange={settings.general.setExperimentalBrowser}
|
||||
hideLabel
|
||||
>
|
||||
{language.t("settings.general.row.browserPane.title")}
|
||||
</Switch>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
<SettingsRow
|
||||
title={language.t("settings.appearance.row.tabs.title")}
|
||||
description={language.t("settings.appearance.row.tabs.description")}
|
||||
|
||||
@@ -67,6 +67,7 @@ describe("settings schema", () => {
|
||||
mobileDiffWrap: true,
|
||||
terminalPlacement: "side",
|
||||
followUpBehavior: "steer",
|
||||
experimentalBrowser: false,
|
||||
},
|
||||
appearance: {
|
||||
fontSize: 14,
|
||||
@@ -130,6 +131,12 @@ describe("settings schema", () => {
|
||||
expect(decode(encode(settings))).toEqual(settings)
|
||||
})
|
||||
|
||||
test("browser attachment is opt-in and preserves an explicit choice", () => {
|
||||
expect(decode({}).general.experimentalBrowser).toBe(false)
|
||||
expect(decode({ general: { experimentalBrowser: true } }).general.experimentalBrowser).toBe(true)
|
||||
expect(decode({ general: { experimentalBrowser: false } }).general.experimentalBrowser).toBe(false)
|
||||
})
|
||||
|
||||
test.each([undefined, null, false, 7, "invalid", []].map((invalid) => [invalid]))(
|
||||
"defaults malformed sections without losing other sections: %j",
|
||||
(invalid) => {
|
||||
|
||||
@@ -95,6 +95,7 @@ const generalSchema = Persistence.struct({
|
||||
mobileDiffWrap: Schema.Boolean,
|
||||
terminalPlacement: Schema.Literals(["side", "bottom"]),
|
||||
followUpBehavior: Schema.Literals(["queue", "steer"]),
|
||||
experimentalBrowser: Schema.Boolean,
|
||||
})
|
||||
|
||||
const appearanceSchema = Persistence.struct({
|
||||
@@ -251,6 +252,7 @@ export const defaultSettings: Settings = {
|
||||
mobileDiffWrap: true,
|
||||
terminalPlacement: "side",
|
||||
followUpBehavior: "steer",
|
||||
experimentalBrowser: false,
|
||||
},
|
||||
appearance: { fontSize: 14, mono: "", sans: "", terminal: "", tabLayout: "horizontal", showProjectName: false },
|
||||
keybinds: {},
|
||||
@@ -358,6 +360,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
setFollowUpBehavior(value: FollowUpBehavior) {
|
||||
setStore("general", "followUpBehavior", value)
|
||||
},
|
||||
experimentalBrowser: withFallback(
|
||||
() => store.general?.experimentalBrowser,
|
||||
defaultSettings.general.experimentalBrowser,
|
||||
),
|
||||
setExperimentalBrowser(value: boolean) {
|
||||
setStore("general", "experimentalBrowser", value)
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
fileTree: showFileTree,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Home } from "@/home/route"
|
||||
import { ServerProvider } from "@/runtime/server/current"
|
||||
import { useGlobal } from "@/runtime/server/runtime"
|
||||
import { ServerConnection } from "@/runtime/server/registry"
|
||||
import { BrowserAttachmentsProvider } from "@/session/browser/attachments"
|
||||
import { SessionPanelFrame, SessionRouteFrame } from "@/session/session-frame"
|
||||
import { LayoutProvider } from "@/shell/state/layout"
|
||||
import { SettingsSurfaceProvider } from "@/settings/surface"
|
||||
@@ -73,7 +74,9 @@ function AppLayout(props: ParentProps) {
|
||||
return (
|
||||
<LayoutProvider>
|
||||
<SettingsSurfaceProvider>
|
||||
<Shell>{props.children}</Shell>
|
||||
<BrowserAttachmentsProvider>
|
||||
<Shell>{props.children}</Shell>
|
||||
</BrowserAttachmentsProvider>
|
||||
</SettingsSurfaceProvider>
|
||||
</LayoutProvider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export const SESSION_OPEN_FILE_TAB = "open-file"
|
||||
export const SESSION_BROWSER_TAB = "browser"
|
||||
export const sessionBrowserTab = (tabID: string) => `${SESSION_BROWSER_TAB}:${tabID}`
|
||||
export const isSessionBrowserTab = (tab: string | undefined) =>
|
||||
!!tab && (tab === SESSION_BROWSER_TAB || tab.startsWith(`${SESSION_BROWSER_TAB}:`))
|
||||
|
||||
export type SessionTabs = {
|
||||
active?: string
|
||||
|
||||
@@ -111,7 +111,7 @@ export default { path: file, version: ${JSON.stringify(opencodePty.version)}, sh
|
||||
const parcelWatcherPlugin: BunPlugin = {
|
||||
name: "parcel-watcher-binding",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /filesystem\/watcher-binding\.ts$/ }, () => ({
|
||||
build.onLoad({ filter: /filesystem[/\\]watcher-binding\.ts$/ }, () => ({
|
||||
contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`,
|
||||
loader: "js",
|
||||
}))
|
||||
|
||||
@@ -361,6 +361,34 @@ const Root = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME
|
||||
...PermissionParams,
|
||||
},
|
||||
}),
|
||||
Spec.make("session", {
|
||||
description: "Manage sessions",
|
||||
commands: [
|
||||
Spec.make("list", {
|
||||
description: "List top-level sessions in the current project, newest first",
|
||||
params: {
|
||||
...ServerParams,
|
||||
maxCount: Flag.integer("max-count").pipe(
|
||||
Flag.withAlias("n"),
|
||||
Flag.withSchema(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
|
||||
Flag.withDescription("Limit to N most recent sessions (default: 100)"),
|
||||
Flag.optional,
|
||||
),
|
||||
format: Flag.choice("format", ["table", "json"]).pipe(
|
||||
Flag.withDescription("Output format"),
|
||||
Flag.withDefault("table"),
|
||||
),
|
||||
},
|
||||
}),
|
||||
Spec.make("delete", {
|
||||
description: "Delete a session and its child sessions",
|
||||
params: {
|
||||
...ServerParams,
|
||||
sessionID: Argument.string("sessionID").pipe(Argument.withDescription("Session ID to delete")),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Spec.make("service", {
|
||||
description: "Manage the background server",
|
||||
commands: [
|
||||
|
||||
@@ -44,6 +44,10 @@ export default Runtime.handler(
|
||||
)
|
||||
const output = yield* Effect.promise(() => response.text())
|
||||
if (output) process.stdout.write(output + (output.endsWith(EOL) ? "" : EOL))
|
||||
if (!response.ok) {
|
||||
process.stderr.write(`HTTP ${response.status} ${response.statusText}${EOL}`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { OpenCode } from "@opencode-ai/client"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Effect, Option } from "effect"
|
||||
import { EOL } from "node:os"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServerConnection } from "../../../services/server-connection"
|
||||
import { errorMessage } from "../../../util/error"
|
||||
|
||||
const handler = Effect.fn("cli.session.delete")(function* (
|
||||
input: Runtime.Input<typeof Commands.commands.session.commands.delete>,
|
||||
) {
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
})
|
||||
const client = OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) })
|
||||
yield* Effect.tryPromise({
|
||||
try: (signal) => client.session.remove({ sessionID: input.sessionID }, { signal }),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
process.stdout.write(`Session ${input.sessionID} deleted${EOL}`)
|
||||
})
|
||||
|
||||
export default Runtime.handler(Commands.commands.session.commands.delete, (input) =>
|
||||
handler(input).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
process.stderr.write(errorMessage(error) + EOL)
|
||||
process.exitCode = 1
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
import { OpenCode, type SessionInfo } from "@opencode-ai/client"
|
||||
import { Service } from "@opencode-ai/client/effect/service"
|
||||
import { Effect, Option, Stream } from "effect"
|
||||
import { EOL } from "node:os"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { ServerConnection } from "../../../services/server-connection"
|
||||
import { errorMessage } from "../../../util/error"
|
||||
|
||||
const handler = Effect.fn("cli.session.list")(function* (
|
||||
input: Runtime.Input<typeof Commands.commands.session.commands.list>,
|
||||
) {
|
||||
const server = yield* ServerConnection.resolve({
|
||||
server: Option.getOrUndefined(input.server),
|
||||
standalone: input.standalone,
|
||||
})
|
||||
const client = OpenCode.make({ baseUrl: server.endpoint.url, headers: Service.headers(server.endpoint) })
|
||||
const location = yield* Effect.tryPromise({
|
||||
try: (signal) => client.location.get({ location: { directory: process.cwd() } }, { signal }),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
const page = yield* Effect.tryPromise({
|
||||
try: (signal) =>
|
||||
client.session.list(
|
||||
{
|
||||
project: location.project.id,
|
||||
parentID: null,
|
||||
order: "desc",
|
||||
limit: Option.getOrElse(input.maxCount, () => 100),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
catch: (cause) => cause,
|
||||
})
|
||||
if (input.format === "table" && page.data.length === 0) return
|
||||
const output =
|
||||
(input.format === "json"
|
||||
? JSON.stringify(
|
||||
page.data.map((session) => ({
|
||||
id: session.id,
|
||||
title: session.title,
|
||||
updated: session.time.updated,
|
||||
created: session.time.created,
|
||||
projectId: session.projectID,
|
||||
directory: session.location.directory,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
: formatTable(page.data)) + EOL
|
||||
const write = Effect.tryPromise(
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
process.stdout.write(output, (error) => (error ? reject(error) : resolve()))
|
||||
}),
|
||||
)
|
||||
if (!process.stdout.isTTY || Option.isSome(input.maxCount) || input.format === "json") {
|
||||
yield* write
|
||||
return
|
||||
}
|
||||
|
||||
const { AppProcess } = yield* Effect.promise(() => import("@opencode-ai/util/process"))
|
||||
const { LayerNode } = yield* Effect.promise(() => import("@opencode-ai/util/effect/layer-node"))
|
||||
const { ChildProcess } = yield* Effect.promise(() => import("effect/unstable/process"))
|
||||
yield* Effect.gen(function* () {
|
||||
const processService = yield* AppProcess.Service
|
||||
const pager = yield* processService
|
||||
.spawn(
|
||||
ChildProcess.make(
|
||||
process.platform === "win32" ? "cmd" : "less",
|
||||
process.platform === "win32" ? ["/c", "more"] : ["-R", "-S"],
|
||||
{
|
||||
stdin: Stream.make(new TextEncoder().encode(output)),
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
},
|
||||
),
|
||||
)
|
||||
.pipe(Effect.option)
|
||||
if (Option.isNone(pager)) {
|
||||
yield* write
|
||||
return
|
||||
}
|
||||
yield* pager.value.exitCode
|
||||
}).pipe(Effect.provide(LayerNode.compile(AppProcess.node)))
|
||||
})
|
||||
|
||||
export default Runtime.handler(Commands.commands.session.commands.list, (input) =>
|
||||
handler(input).pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.sync(() => {
|
||||
process.stderr.write(errorMessage(error) + EOL)
|
||||
process.exitCode = 1
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
function formatTable(sessions: ReadonlyArray<SessionInfo>) {
|
||||
const rows = sessions.map((session) => ({
|
||||
id: session.id,
|
||||
title: (session.title ?? "Untitled session").replace(/[\r\n\t]/g, " "),
|
||||
updated: new Date(session.time.updated).toLocaleString(),
|
||||
}))
|
||||
const idWidth = Math.max(20, ...rows.map((row) => row.id.length))
|
||||
const titleWidth = Math.max(25, ...rows.map((row) => row.title.length))
|
||||
const header = `${"Session ID".padEnd(idWidth)} ${"Title".padEnd(titleWidth)} Updated`
|
||||
return [
|
||||
header,
|
||||
"─".repeat(header.length),
|
||||
...rows.map((row) => `${row.id.padEnd(idWidth)} ${row.title.padEnd(titleWidth)} ${row.updated}`),
|
||||
].join(EOL)
|
||||
}
|
||||
@@ -53,6 +53,10 @@ const Handlers = Runtime.handlers(Commands, {
|
||||
mini: () => import("./commands/handlers/mini"),
|
||||
run: () => import("./commands/handlers/run"),
|
||||
pair: () => import("./commands/handlers/pair"),
|
||||
session: {
|
||||
list: () => import("./commands/handlers/session/list"),
|
||||
delete: () => import("./commands/handlers/session/delete"),
|
||||
},
|
||||
service: {
|
||||
start: () => import("./commands/handlers/service/start"),
|
||||
restart: () => import("./commands/handlers/service/restart"),
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { PromptInput } from "@opencode-ai/schema/prompt-input"
|
||||
import type { AgentAttachment } from "@opencode-ai/schema/prompt"
|
||||
import type { Skill } from "@opencode-ai/schema/skill"
|
||||
import type { Event } from "@opencode-ai/schema/event"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
|
||||
import type { Schema } from "effect"
|
||||
import type { EventLog } from "@opencode-ai/schema/event-log"
|
||||
@@ -36,7 +37,6 @@ import type { PtyTicket } from "@opencode-ai/schema/pty-ticket"
|
||||
import type { Reference } from "@opencode-ai/schema/reference"
|
||||
import type { Worktree } from "@opencode-ai/schema/worktree"
|
||||
import type { Vcs } from "@opencode-ai/schema/vcs"
|
||||
import type { FileDiff } from "@opencode-ai/schema/file-diff"
|
||||
import type { WebSearch } from "@opencode-ai/schema/websearch"
|
||||
import type { Config } from "@opencode-ai/schema/config"
|
||||
|
||||
@@ -360,6 +360,15 @@ export type SessionContextInput = { readonly sessionID: Session.ID }
|
||||
export type SessionContextOutput = ReadonlyArray<SessionMessage.Info>
|
||||
export type SessionContextOperation<E = never> = (input: SessionContextInput) => Effect.Effect<SessionContextOutput, E>
|
||||
|
||||
export type SessionDiffInput = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly messageID?: SessionMessage.ID | undefined
|
||||
readonly to?: SessionMessage.ID | undefined
|
||||
readonly context?: number | undefined
|
||||
}
|
||||
export type SessionDiffOutput = ReadonlyArray<FileDiff.Info>
|
||||
export type SessionDiffOperation<E = never> = (input: SessionDiffInput) => Effect.Effect<SessionDiffOutput, E>
|
||||
|
||||
export type SessionInboxListInput = { readonly sessionID: Session.ID }
|
||||
export type SessionInboxListOutput = ReadonlyArray<SessionInbox.Info>
|
||||
export type SessionInboxListOperation<E = never> = (
|
||||
@@ -970,6 +979,20 @@ export type SessionLogOutput =
|
||||
readonly reason: "auto" | "manual"
|
||||
readonly model?: Model.Ref | undefined
|
||||
readonly providerState?: SessionMessage.ProviderState | undefined
|
||||
readonly providerContext?:
|
||||
| {
|
||||
readonly version: 1
|
||||
readonly provenance: {
|
||||
readonly providerID: Provider.ID
|
||||
readonly provider: string
|
||||
readonly modelID: string
|
||||
readonly route: string
|
||||
readonly protocol: string
|
||||
readonly endpoint: string
|
||||
}
|
||||
readonly messages: Schema.Json
|
||||
}
|
||||
| undefined
|
||||
readonly text: string
|
||||
readonly recent: string
|
||||
}
|
||||
@@ -1119,6 +1142,7 @@ export interface SessionApi<E = never> {
|
||||
readonly commit: SessionRevertCommitOperation<E>
|
||||
}
|
||||
readonly context: SessionContextOperation<E>
|
||||
readonly diff: SessionDiffOperation<E>
|
||||
readonly inbox: {
|
||||
readonly list: SessionInboxListOperation<E>
|
||||
readonly cancel: SessionInboxCancelOperation<E>
|
||||
|
||||
@@ -68,6 +68,8 @@ import type {
|
||||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionDiffInput,
|
||||
SessionDiffOutput,
|
||||
SessionInboxListInput,
|
||||
SessionInboxListOutput,
|
||||
SessionInboxCancelInput,
|
||||
@@ -594,6 +596,17 @@ const EndpointSessionContext = (raw: RawClient["server.session"]) => (input: Ses
|
||||
),
|
||||
)
|
||||
|
||||
const EndpointSessionDiff = (raw: RawClient["server.session"]) => (input: SessionDiffInput) =>
|
||||
preserveEffect<SessionDiffOutput>()(
|
||||
raw["session.diff"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
query: { messageID: input["messageID"], to: input["to"], context: input["context"] },
|
||||
}).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const EndpointSessionInboxList = (raw: RawClient["server.session"]) => (input: SessionInboxListInput) =>
|
||||
preserveEffect<SessionInboxListOutput>()(
|
||||
raw["session.inbox.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
@@ -744,6 +757,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({
|
||||
commit: EndpointSessionRevertCommit(raw),
|
||||
},
|
||||
context: EndpointSessionContext(raw),
|
||||
diff: EndpointSessionDiff(raw),
|
||||
inbox: {
|
||||
list: EndpointSessionInboxList(raw),
|
||||
cancel: EndpointSessionInboxCancel(raw),
|
||||
|
||||
@@ -62,6 +62,8 @@ import type {
|
||||
SessionRevertCommitOutput,
|
||||
SessionContextInput,
|
||||
SessionContextOutput,
|
||||
SessionDiffInput,
|
||||
SessionDiffOutput,
|
||||
SessionInboxListInput,
|
||||
SessionInboxListOutput,
|
||||
SessionInboxCancelInput,
|
||||
@@ -844,6 +846,18 @@ export function make(options: ClientOptions) {
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
diff: (input: SessionDiffInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionDiffOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/diff`,
|
||||
query: { messageID: input["messageID"], to: input["to"], context: input["context"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404, 500],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
).then((value) => value.data),
|
||||
inbox: {
|
||||
list: (input: SessionInboxListInput, requestOptions?: RequestOptions) =>
|
||||
request<{ readonly data: SessionInboxListOutput }>(
|
||||
|
||||
@@ -138,6 +138,23 @@ export type SessionMessageCompactionRunning = {
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type SessionProviderContextProvenance = {
|
||||
providerID: string
|
||||
provider: string
|
||||
modelID: string
|
||||
route: string
|
||||
protocol: string
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export type SessionMessageIdle = {
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
type: "idle"
|
||||
outcome: "succeeded" | "failed" | "interrupted"
|
||||
}
|
||||
|
||||
export type SessionActive = { type: "running" }
|
||||
|
||||
export type SessionInboxDelivery = "steer" | "queue"
|
||||
@@ -183,6 +200,8 @@ export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning
|
||||
|
||||
export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens"
|
||||
|
||||
export type ProviderCompaction = { mode: "local" } | { mode: "provider"; threshold?: number }
|
||||
|
||||
export type ModelCapabilities = {
|
||||
tools: boolean
|
||||
input: Array<string>
|
||||
@@ -201,18 +220,6 @@ export type MoneyUSDPerMillionTokens = number
|
||||
|
||||
export type GenerateTextResponse = { data: { text: string } }
|
||||
|
||||
export type ProviderInfo = {
|
||||
id: string
|
||||
canonical?: string
|
||||
integrationID?: string
|
||||
name: string
|
||||
activation: "auto" | "enabled" | "disabled"
|
||||
package: string
|
||||
settings?: { [x: string]: any }
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
}
|
||||
|
||||
export type FormWhen = {
|
||||
key: string
|
||||
op: "eq" | "neq"
|
||||
@@ -512,19 +519,6 @@ export type SessionMessageAssistantReasoning = {
|
||||
time?: { created: number; completed?: number }
|
||||
}
|
||||
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
providerState?: SessionMessageProviderState
|
||||
summary: string
|
||||
recent: string
|
||||
}
|
||||
|
||||
export type ToolContent = ToolTextContent | ToolFileContent
|
||||
|
||||
export type SessionMessageAssistantRetry = { attempt: number; at: number; error: SessionStructuredError }
|
||||
@@ -539,6 +533,8 @@ export type SessionMessageCompactionFailed = {
|
||||
error: SessionStructuredError
|
||||
}
|
||||
|
||||
export type SessionProviderContext = { version: 1; provenance: SessionProviderContextProvenance; messages: JsonValue }
|
||||
|
||||
export type SessionInboxSynthetic = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -1345,23 +1341,6 @@ export type SessionToolCalled = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionCompactionEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
providerState?: SessionMessageProviderState1
|
||||
text: string
|
||||
recent: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantText1 = { type: "text"; text: string; state?: SessionMessageProviderState1 }
|
||||
|
||||
export type SessionMessageAssistantReasoning1 = {
|
||||
@@ -1381,6 +1360,19 @@ export type ModelCompatibility = {
|
||||
requireAssistantAfterTool?: boolean
|
||||
}
|
||||
|
||||
export type ProviderInfo = {
|
||||
id: string
|
||||
canonical?: string
|
||||
integrationID?: string
|
||||
name: string
|
||||
activation: "auto" | "enabled" | "disabled"
|
||||
package: string
|
||||
compaction?: ProviderCompaction
|
||||
settings?: { [x: string]: any }
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
}
|
||||
|
||||
export type ModelCost = {
|
||||
tier?: { type: "context"; size: number }
|
||||
input: MoneyUSDPerMillionTokens
|
||||
@@ -1742,10 +1734,37 @@ export type SessionMessageToolStateError = {
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
}
|
||||
|
||||
export type SessionMessageCompaction =
|
||||
| SessionMessageCompactionRunning
|
||||
| SessionMessageCompactionCompleted
|
||||
| SessionMessageCompactionFailed
|
||||
export type SessionMessageCompactionCompleted = {
|
||||
type: "compaction"
|
||||
id: string
|
||||
metadata?: { [x: string]: JsonValue }
|
||||
time: { created: number }
|
||||
status: "completed"
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
providerState?: SessionMessageProviderState
|
||||
summary: string
|
||||
recent: string
|
||||
providerContext?: SessionProviderContext
|
||||
}
|
||||
|
||||
export type SessionCompactionEnded = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "session.compaction.ended"
|
||||
durable: { aggregateID: string; seq: number; version: 1 }
|
||||
location?: LocationRef
|
||||
data: {
|
||||
sessionID: string
|
||||
reason: "auto" | "manual"
|
||||
model?: ModelRef
|
||||
providerState?: SessionMessageProviderState1
|
||||
providerContext?: SessionProviderContext
|
||||
text: string
|
||||
recent: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionForked = {
|
||||
id: string
|
||||
@@ -1824,6 +1843,7 @@ export type ModelInfo = {
|
||||
name: string
|
||||
compatibility?: ModelCompatibility
|
||||
package?: string
|
||||
compaction?: ProviderCompaction
|
||||
settings?: { [x: string]: any }
|
||||
headers?: { [x: string]: string }
|
||||
body?: { [x: string]: any }
|
||||
@@ -1999,6 +2019,7 @@ export type ConfigEntry =
|
||||
warming?: boolean | { prompt?: string; interval?: string; duration?: string }
|
||||
providers?: {
|
||||
[x: string]: {
|
||||
compaction?: ProviderCompaction
|
||||
canonical?: string
|
||||
name?: string
|
||||
env?: Array<string>
|
||||
@@ -2008,6 +2029,7 @@ export type ConfigEntry =
|
||||
body?: { [x: string]: JsonValue }
|
||||
models?: {
|
||||
[x: string]: {
|
||||
compaction?: ProviderCompaction
|
||||
modelID?: string
|
||||
family?: string
|
||||
name?: string
|
||||
@@ -2085,6 +2107,11 @@ export type SessionMessageAssistantTool = {
|
||||
time: { created: number; ran?: number; completed?: number }
|
||||
}
|
||||
|
||||
export type SessionMessageCompaction =
|
||||
| SessionMessageCompactionRunning
|
||||
| SessionMessageCompactionCompleted
|
||||
| SessionMessageCompactionFailed
|
||||
|
||||
export type SessionMessageAssistantTool1 = {
|
||||
type: "tool"
|
||||
id: string
|
||||
@@ -2158,6 +2185,7 @@ export type SessionMessageInfo =
|
||||
| SessionMessageShell
|
||||
| SessionMessageAssistant
|
||||
| SessionMessageCompaction
|
||||
| SessionMessageIdle
|
||||
|
||||
export type SessionMessageContentUpdated = {
|
||||
id: string
|
||||
@@ -3080,6 +3108,18 @@ export type SessionImportInput = {
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
readonly version: 1
|
||||
readonly provenance: {
|
||||
readonly providerID: string
|
||||
readonly provider: string
|
||||
readonly modelID: string
|
||||
readonly route: string
|
||||
readonly protocol: string
|
||||
readonly endpoint: string
|
||||
}
|
||||
readonly messages: JsonValue
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly type: "compaction"
|
||||
@@ -3091,6 +3131,13 @@ export type SessionImportInput = {
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||
}
|
||||
)
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number }
|
||||
readonly type: "idle"
|
||||
readonly outcome: "succeeded" | "failed" | "interrupted"
|
||||
}
|
||||
>
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
}["info"]
|
||||
@@ -3359,6 +3406,18 @@ export type SessionImportInput = {
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
readonly version: 1
|
||||
readonly provenance: {
|
||||
readonly providerID: string
|
||||
readonly provider: string
|
||||
readonly modelID: string
|
||||
readonly route: string
|
||||
readonly protocol: string
|
||||
readonly endpoint: string
|
||||
}
|
||||
readonly messages: JsonValue
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly type: "compaction"
|
||||
@@ -3370,6 +3429,13 @@ export type SessionImportInput = {
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||
}
|
||||
)
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number }
|
||||
readonly type: "idle"
|
||||
readonly outcome: "succeeded" | "failed" | "interrupted"
|
||||
}
|
||||
>
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
}["messages"]
|
||||
@@ -3638,6 +3704,18 @@ export type SessionImportInput = {
|
||||
readonly providerState?: { readonly [x: string]: JsonValue }
|
||||
readonly summary: string
|
||||
readonly recent: string
|
||||
readonly providerContext?: {
|
||||
readonly version: 1
|
||||
readonly provenance: {
|
||||
readonly providerID: string
|
||||
readonly provider: string
|
||||
readonly modelID: string
|
||||
readonly route: string
|
||||
readonly protocol: string
|
||||
readonly endpoint: string
|
||||
}
|
||||
readonly messages: JsonValue
|
||||
}
|
||||
}
|
||||
| {
|
||||
readonly type: "compaction"
|
||||
@@ -3649,6 +3727,13 @@ export type SessionImportInput = {
|
||||
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||
}
|
||||
)
|
||||
| {
|
||||
readonly id: string
|
||||
readonly metadata?: { readonly [x: string]: JsonValue }
|
||||
readonly time: { readonly created: number }
|
||||
readonly type: "idle"
|
||||
readonly outcome: "succeeded" | "failed" | "interrupted"
|
||||
}
|
||||
>
|
||||
readonly location?: { readonly directory: string; readonly workspaceID?: string } | null
|
||||
}["location"]
|
||||
@@ -4138,6 +4223,27 @@ export type SessionContextInput = { readonly sessionID: { readonly sessionID: st
|
||||
|
||||
export type SessionContextOutput = { data: Array<SessionMessageInfo> }["data"]
|
||||
|
||||
export type SessionDiffInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly messageID?: {
|
||||
readonly messageID?: string | undefined
|
||||
readonly to?: string | undefined
|
||||
readonly context?: number | undefined
|
||||
}["messageID"]
|
||||
readonly to?: {
|
||||
readonly messageID?: string | undefined
|
||||
readonly to?: string | undefined
|
||||
readonly context?: number | undefined
|
||||
}["to"]
|
||||
readonly context?: {
|
||||
readonly messageID?: string | undefined
|
||||
readonly to?: string | undefined
|
||||
readonly context?: number | undefined
|
||||
}["context"]
|
||||
}
|
||||
|
||||
export type SessionDiffOutput = { data: Array<FileDiffInfo> }["data"]
|
||||
|
||||
export type SessionInboxListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type SessionInboxListOutput = { data: Array<SessionInboxInfo> }["data"]
|
||||
|
||||
@@ -1032,6 +1032,18 @@ export function createData(config: CreateDataInput) {
|
||||
if (currentAssistant) currentAssistant.retry = undefined
|
||||
})
|
||||
if (event.type === "session.execution.interrupted" && event.data.reason === "shutdown") return
|
||||
// Mirror the projected idle marker so turn boundaries match before the next message read.
|
||||
message.insert(event.data.sessionID, {
|
||||
id: messageIDFromEvent(event.id),
|
||||
type: "idle",
|
||||
outcome:
|
||||
event.type === "session.execution.succeeded"
|
||||
? "succeeded"
|
||||
: event.type === "session.execution.failed"
|
||||
? "failed"
|
||||
: "interrupted",
|
||||
time: { created: event.created },
|
||||
})
|
||||
// An event can overtake the first read; queue a revalidation when that read is still active.
|
||||
if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`)) return
|
||||
result.session.invalidate(event.data.sessionID)
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
"@opencode-ai/pty": "0.1.13",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/plugin-browser": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@standard-schema/spec": "catalog:",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
|
||||
+30
-42
@@ -309,6 +309,7 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
|
||||
compact: undefined,
|
||||
id: `ai-sdk:${packageName}`,
|
||||
provider: ProviderID.make(providerID),
|
||||
providerMetadataKey: optionKey,
|
||||
protocol: "ai-sdk",
|
||||
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
|
||||
auth: Auth.none,
|
||||
@@ -341,16 +342,7 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
|
||||
model: (input) =>
|
||||
LanguageModel.make({ ...input, provider: "provider" in input ? input.provider : providerID, route }),
|
||||
prepareTransport: (body) => Effect.succeed(body),
|
||||
streamPrepared: (prepared) =>
|
||||
streamLanguage(language, prepared as LanguageModelV3CallOptions).pipe(
|
||||
Stream.map((event) => {
|
||||
if (!("providerMetadata" in event)) return event
|
||||
// Preserve the same inner state that Session persists, while keeping SDK
|
||||
// option namespaces private to this bridge.
|
||||
const state = event.providerMetadata?.[optionKey]
|
||||
return { ...event, providerMetadata: state === undefined ? undefined : { [route.id]: state } }
|
||||
}),
|
||||
),
|
||||
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
|
||||
}
|
||||
return LanguageModel.make({
|
||||
id: info.modelID ?? info.id,
|
||||
@@ -426,7 +418,7 @@ function callOptions(
|
||||
): LanguageModelV3CallOptions {
|
||||
const flattened = ProviderShared.flattenToolRequest(request)
|
||||
return {
|
||||
prompt: prompt(flattened.request, optionKey),
|
||||
prompt: prompt(flattened.request),
|
||||
maxOutputTokens: request.generation?.maxTokens,
|
||||
temperature: request.generation?.temperature,
|
||||
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
|
||||
@@ -442,21 +434,15 @@ function callOptions(
|
||||
}
|
||||
}
|
||||
|
||||
type MetadataOptions = (input: ProviderMetadata | undefined) => SharedV3ProviderOptions | undefined
|
||||
|
||||
function prompt(request: LLMRequest, optionKey: string): LanguageModelV3Prompt {
|
||||
const metadata: MetadataOptions = (input) => {
|
||||
const state = input?.[request.model.route.id]
|
||||
return state === undefined ? undefined : { [optionKey]: jsonObject(state) }
|
||||
}
|
||||
function prompt(request: LLMRequest): LanguageModelV3Prompt {
|
||||
const system = request.system
|
||||
.map((part) => part.text)
|
||||
.filter(Boolean)
|
||||
.join("\n\n")
|
||||
const pending: UserContent = []
|
||||
const messages = request.messages.flatMap((input, index) => {
|
||||
if (input.role !== "tool") return message(input, metadata)
|
||||
const lowered = toolMessage(input, metadata)
|
||||
if (input.role !== "tool") return message(input)
|
||||
const lowered = toolMessage(input)
|
||||
pending.push(...lowered.media)
|
||||
if (request.messages[index + 1]?.role === "tool" || pending.length === 0) return lowered.messages
|
||||
const media = [...pending]
|
||||
@@ -473,7 +459,7 @@ function prompt(request: LLMRequest, optionKey: string): LanguageModelV3Prompt {
|
||||
return [{ role: "system", content: system }, ...messages]
|
||||
}
|
||||
|
||||
function message(input: LLMRequest["messages"][number], metadata: MetadataOptions): LanguageModelV3Message[] {
|
||||
function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
|
||||
switch (input.role) {
|
||||
case "system":
|
||||
// The initial privileged prompt lives in `request.system` and is prepended above. A system message here is a
|
||||
@@ -493,32 +479,29 @@ function message(input: LLMRequest["messages"][number], metadata: MetadataOption
|
||||
case "user":
|
||||
return [{ role: "user", content: input.content.flatMap(userPart) }]
|
||||
case "assistant":
|
||||
return [{ role: "assistant", content: input.content.flatMap((part) => assistantPart(part, metadata)) }]
|
||||
return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
|
||||
case "tool":
|
||||
return toolMessage(input, metadata).messages
|
||||
return toolMessage(input).messages
|
||||
}
|
||||
}
|
||||
|
||||
function toolMessage(input: LLMRequest["messages"][number], metadata: MetadataOptions) {
|
||||
function toolMessage(input: LLMRequest["messages"][number]) {
|
||||
const media: UserContent = []
|
||||
const content = input.content.flatMap((part) => {
|
||||
if (part.type !== "tool-result" || part.result.type !== "content") return toolResultPart(part, metadata)
|
||||
if (part.type !== "tool-result" || part.result.type !== "content") return toolResultPart(part)
|
||||
const value = part.result.value.filter((item) => {
|
||||
if (item.type !== "file") return true
|
||||
if (!item.mime.startsWith("image/") && item.mime !== "application/pdf") return true
|
||||
media.push({ type: "file", mediaType: item.mime, data: fileData(item.uri), filename: item.name })
|
||||
return false
|
||||
})
|
||||
return toolResultPart(
|
||||
{
|
||||
...part,
|
||||
result:
|
||||
value.length === 0
|
||||
? { type: "text", value: "Media attached in the following user message." }
|
||||
: { ...part.result, value },
|
||||
},
|
||||
metadata,
|
||||
)
|
||||
return toolResultPart({
|
||||
...part,
|
||||
result:
|
||||
value.length === 0
|
||||
? { type: "text", value: "Media attached in the following user message." }
|
||||
: { ...part.result, value },
|
||||
})
|
||||
})
|
||||
return {
|
||||
messages: content.length ? ([{ role: "tool", content }] satisfies LanguageModelV3Message[]) : [],
|
||||
@@ -537,7 +520,7 @@ function userPart(part: ContentPart): UserContent {
|
||||
return []
|
||||
}
|
||||
|
||||
function assistantPart(part: ContentPart, metadata: MetadataOptions): AssistantContent {
|
||||
function assistantPart(part: ContentPart): AssistantContent {
|
||||
switch (part.type) {
|
||||
case "compaction":
|
||||
throw ProviderShared.unsupportedOperation({
|
||||
@@ -546,11 +529,11 @@ function assistantPart(part: ContentPart, metadata: MetadataOptions): AssistantC
|
||||
message: "AI SDK routes cannot replay native provider compaction state",
|
||||
})
|
||||
case "text":
|
||||
return [{ type: "text", text: part.text, providerOptions: metadata(part.providerMetadata) }]
|
||||
return [{ type: "text", text: part.text, providerOptions: metadataProviderOptions(part.providerMetadata) }]
|
||||
case "media":
|
||||
return [{ type: "file", mediaType: part.mediaType, data: fileData(part.data), filename: part.filename }]
|
||||
case "reasoning":
|
||||
return [{ type: "reasoning", text: part.text, providerOptions: metadata(part.providerMetadata) }]
|
||||
return [{ type: "reasoning", text: part.text, providerOptions: metadataProviderOptions(part.providerMetadata) }]
|
||||
case "tool-call":
|
||||
return [
|
||||
{
|
||||
@@ -559,11 +542,11 @@ function assistantPart(part: ContentPart, metadata: MetadataOptions): AssistantC
|
||||
toolName: part.name,
|
||||
input: part.input,
|
||||
providerExecuted: part.providerExecuted,
|
||||
providerOptions: metadata(part.providerMetadata),
|
||||
providerOptions: metadataProviderOptions(part.providerMetadata),
|
||||
},
|
||||
]
|
||||
case "tool-result":
|
||||
return toolResultPart(part, metadata)
|
||||
return toolResultPart(part)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +559,7 @@ function fileData(data: Extract<ContentPart, { type: "media" }>["data"]) {
|
||||
return url.protocol === "http:" || url.protocol === "https:" ? url : data
|
||||
}
|
||||
|
||||
function toolResultPart(part: ContentPart, metadata: MetadataOptions): ToolResultContent[] {
|
||||
function toolResultPart(part: ContentPart): ToolResultContent[] {
|
||||
if (part.type !== "tool-result") return []
|
||||
return [
|
||||
{
|
||||
@@ -584,7 +567,7 @@ function toolResultPart(part: ContentPart, metadata: MetadataOptions): ToolResul
|
||||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
output: toolOutput(part.result),
|
||||
providerOptions: metadata(part.providerMetadata),
|
||||
providerOptions: metadataProviderOptions(part.providerMetadata),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -641,6 +624,11 @@ function requestProviderOptions(
|
||||
return { [optionKey]: options }
|
||||
}
|
||||
|
||||
function metadataProviderOptions(input: ProviderMetadata | undefined): SharedV3ProviderOptions | undefined {
|
||||
if (!input) return undefined
|
||||
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
|
||||
}
|
||||
|
||||
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
|
||||
const state = { step: 0, toolNames: {} as Record<string, string> }
|
||||
return Stream.concat(
|
||||
|
||||
@@ -76,6 +76,7 @@ const layer = Layer.effect(
|
||||
...model,
|
||||
...(provider.canonical === undefined ? {} : { canonical: provider.canonical }),
|
||||
package: model.package ?? provider.package,
|
||||
compaction: model.compaction ?? provider.compaction,
|
||||
settings: Provider.mergeOverlay(provider.settings, model.settings),
|
||||
headers: Provider.mergeHeaders(provider.headers, model.headers),
|
||||
body: Provider.mergeOverlay(provider.body, model.body),
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ConfigAgentV1 } from "../../v1/config/agent.js"
|
||||
import { ConfigMigrateV1 } from "../../v1/config/migrate.js"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Permission } from "../../permission.js"
|
||||
import type { LocationMutation } from "../../location-mutation.js"
|
||||
import type { FileAccess } from "../../file-access.js"
|
||||
import type { ReadTool } from "../../tool/plugin/read.js"
|
||||
import type { EditTool } from "../../tool/plugin/edit.js"
|
||||
import { AbsolutePath } from "../../schema.js"
|
||||
@@ -27,10 +27,7 @@ const sourceDirectories = ["agent", "agents", "mode", "modes"] as const
|
||||
const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info)
|
||||
const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info)
|
||||
const decodeConfig = Schema.decodeUnknownOption(Info)
|
||||
type PathAction =
|
||||
| LocationMutation.ExternalDirectoryAuthorization["action"]
|
||||
| typeof ReadTool.name
|
||||
| typeof EditTool.name
|
||||
type PathAction = FileAccess.ExternalDirectoryAuthorization["action"] | typeof ReadTool.name | typeof EditTool.name
|
||||
const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[]
|
||||
const agentKeys = new Set(["variant", ...Object.keys(ConfigAgent.Info.fields)])
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export const Plugin = define({
|
||||
if (item.canonical !== undefined) provider.canonical = item.canonical
|
||||
if (item.name !== undefined) provider.name = item.name
|
||||
if (item.package !== undefined) provider.package = item.package
|
||||
if (item.compaction !== undefined) provider.compaction = { ...item.compaction }
|
||||
if (item.settings !== undefined) provider.settings = Provider.mergeOverlay(provider.settings, item.settings)
|
||||
if (item.headers !== undefined) provider.headers = Provider.mergeHeaders(provider.headers, item.headers)
|
||||
if (item.body !== undefined) provider.body = Provider.mergeOverlay(provider.body, item.body)
|
||||
@@ -76,6 +77,7 @@ export const Plugin = define({
|
||||
if (config.compatibility !== undefined)
|
||||
model.compatibility = { ...model.compatibility, ...config.compatibility }
|
||||
if (config.package !== undefined) model.package = config.package
|
||||
if (config.compaction !== undefined) model.compaction = { ...config.compaction }
|
||||
if (config.settings !== undefined) model.settings = Provider.mergeOverlay(model.settings, config.settings)
|
||||
if (config.headers !== undefined) model.headers = Provider.mergeHeaders(model.headers, config.headers)
|
||||
if (config.body !== undefined) model.body = Provider.mergeOverlay(model.body, config.body)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
export * as FileAccess from "./file-access.js"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Array, Context, Effect, Layer, Schema } from "effect"
|
||||
import path from "path"
|
||||
import { Location } from "./location.js"
|
||||
import { Permission } from "./permission.js"
|
||||
import { Project } from "./project.js"
|
||||
import { AbsolutePath } from "./schema.js"
|
||||
import type { SessionErrors } from "./session/error.js"
|
||||
import type { Tool } from "./tool.js"
|
||||
|
||||
export const Kind = Schema.Literals(["file", "directory"])
|
||||
export type Kind = typeof Kind.Type
|
||||
|
||||
export const ResolveInput = Schema.Struct({
|
||||
path: Schema.String,
|
||||
/** Selects the external approval boundary; it does not validate the target type. */
|
||||
kind: Kind.pipe(Schema.optional),
|
||||
})
|
||||
export type ResolveInput = typeof ResolveInput.Type
|
||||
|
||||
export interface ExternalDirectoryAuthorization {
|
||||
readonly action: "external_directory"
|
||||
/** Lexical directory used as the external approval boundary. */
|
||||
readonly directory: AbsolutePath
|
||||
readonly resource: string
|
||||
readonly save: string
|
||||
}
|
||||
|
||||
export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({
|
||||
action: input.action,
|
||||
resources: [input.resource],
|
||||
save: [input.save],
|
||||
})
|
||||
|
||||
export interface Target {
|
||||
readonly absolute: AbsolutePath
|
||||
/** Location-relative for internal paths, absolute for external paths. */
|
||||
readonly resource: string
|
||||
readonly externalDirectory?: ExternalDirectoryAuthorization
|
||||
}
|
||||
|
||||
export type Invocation = Pick<Tool.Context, "sessionID" | "agent" | "messageID" | "id">
|
||||
|
||||
export interface ReadOptions {
|
||||
/** A target already authorized by this invocation, used for filename recovery. */
|
||||
readonly siblingOf: Target
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
/** Resolve a lexical path and its permission resources, without requesting approval. */
|
||||
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
|
||||
/** Approve external directories in one batch, preserving first-seen resource order. */
|
||||
readonly authorizeExternal: (
|
||||
targets: readonly Target[],
|
||||
context: Invocation,
|
||||
metadata?: Permission.AssertInput["metadata"],
|
||||
) => Effect.Effect<void, Error | SessionErrors.NotFoundError>
|
||||
/** Resolve a read target and obtain external-directory approval before read approval. */
|
||||
readonly authorizeRead: (
|
||||
file: string,
|
||||
context: Invocation,
|
||||
options?: ReadOptions,
|
||||
) => Effect.Effect<Target, FSUtil.Error | Error | SessionErrors.NotFoundError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/FileAccess") {}
|
||||
|
||||
/** Expand a leading ~ and normalize Windows shell paths before lexical resolution. */
|
||||
export const resolvePath = (directory: string, input: string, home = Global.Path.home) => {
|
||||
const normalized = FSUtil.windowsPath(input)
|
||||
return path.resolve(
|
||||
directory,
|
||||
normalized === "~"
|
||||
? home
|
||||
: normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))
|
||||
? path.join(home, normalized.slice(2))
|
||||
: normalized,
|
||||
)
|
||||
}
|
||||
|
||||
const slash = (value: string) => value.replaceAll("\\", "/")
|
||||
const invocation = (context: Invocation) => ({
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source: { type: "tool" as const, messageID: context.messageID, id: context.id },
|
||||
})
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
const permission = yield* Permission.Service
|
||||
|
||||
const resolve = Effect.fn("FileAccess.resolve")(function* (input: ResolveInput) {
|
||||
const absolute = AbsolutePath.make(resolvePath(location.directory, input.path))
|
||||
const worktree = path.resolve(location.project.directory)
|
||||
const internal =
|
||||
FSUtil.contains(location.directory, absolute) ||
|
||||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
|
||||
if (internal) {
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(path.relative(location.directory, absolute) || "."),
|
||||
} satisfies Target
|
||||
}
|
||||
const type =
|
||||
input.kind === "directory"
|
||||
? "Directory"
|
||||
: input.kind === "file"
|
||||
? "File"
|
||||
: (yield* fs.stat(absolute).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined)))
|
||||
?.type
|
||||
const directory = AbsolutePath.make(type === "Directory" ? absolute : path.dirname(absolute))
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(absolute),
|
||||
externalDirectory: {
|
||||
action: "external_directory",
|
||||
directory,
|
||||
resource: slash(path.join(directory, "*")),
|
||||
save: slash(path.join((yield* Project.root(fs, directory)) ?? directory, "*")),
|
||||
},
|
||||
} satisfies Target
|
||||
})
|
||||
|
||||
const authorizeExternal = Effect.fn("FileAccess.authorizeExternal")(function* (
|
||||
targets: readonly Target[],
|
||||
context: Invocation,
|
||||
metadata?: Permission.AssertInput["metadata"],
|
||||
) {
|
||||
const external = Array.dedupeWith(
|
||||
targets.flatMap((target) => (target.externalDirectory ? [target.externalDirectory] : [])),
|
||||
(left, right) => left.resource === right.resource,
|
||||
)
|
||||
if (external.length === 0) return
|
||||
yield* permission.assert({
|
||||
action: "external_directory",
|
||||
resources: external.map((item) => item.resource),
|
||||
save: external.map((item) => item.save),
|
||||
...(metadata === undefined ? {} : { metadata }),
|
||||
...invocation(context),
|
||||
})
|
||||
})
|
||||
|
||||
const authorizeRead = Effect.fn("FileAccess.authorizeRead")(function* (
|
||||
file: string,
|
||||
context: Invocation,
|
||||
options?: ReadOptions,
|
||||
) {
|
||||
const target = yield* resolve({ path: file, kind: options ? "file" : undefined })
|
||||
const sibling = options && path.dirname(target.absolute) === path.dirname(options.siblingOf.absolute)
|
||||
|
||||
// Filename recovery shares the directory approval, but checks the recovered file's own read rules.
|
||||
if (!sibling) yield* authorizeExternal([target], context)
|
||||
yield* permission.assert({
|
||||
action: "read",
|
||||
resources: [target.resource],
|
||||
save: ["*"],
|
||||
...invocation(context),
|
||||
})
|
||||
return target
|
||||
})
|
||||
|
||||
return Service.of({ resolve, authorizeExternal, authorizeRead })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Permission.node] })
|
||||
@@ -7,11 +7,9 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Bom } from "@opencode-ai/util/bom"
|
||||
import { Environment } from "./environment/index.js"
|
||||
import type { Files } from "./environment/index.js"
|
||||
import type { FileAccess } from "./file-access.js"
|
||||
|
||||
export interface Target {
|
||||
readonly absolute: string
|
||||
readonly resource: string
|
||||
}
|
||||
export type Target = Pick<FileAccess.Target, "absolute" | "resource">
|
||||
|
||||
export interface WriteInput {
|
||||
readonly target: Target
|
||||
|
||||
@@ -42,6 +42,7 @@ export const layer = Layer.effect(
|
||||
"SessionRunnerModel.VariantUnavailableError",
|
||||
"SessionRunnerModel.UnsupportedPackageError",
|
||||
"SessionRunnerModel.UnresolvedProviderVariablesError",
|
||||
"SessionRunnerModel.UnsupportedCompactionError",
|
||||
],
|
||||
(error) => {
|
||||
const mapped: Error = input.model
|
||||
|
||||
+75
-64
@@ -9,6 +9,7 @@ import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { File } from "./file.js"
|
||||
import { KeyedMutex } from "./effect/keyed-mutex.js"
|
||||
import { VcsPatch } from "./vcs/patch.js"
|
||||
|
||||
export class Repository extends Schema.Class<Repository>("Git.Repository")({
|
||||
worktree: AbsolutePath,
|
||||
@@ -308,7 +309,7 @@ const layer = Layer.effect(
|
||||
operationName: OperationError["operation"],
|
||||
repository: Repository,
|
||||
args: string[],
|
||||
options?: { stdin?: string; env?: Record<string, string> },
|
||||
options?: { stdin?: string; env?: Record<string, string>; maxOutputBytes?: number },
|
||||
) {
|
||||
const result = yield* proc
|
||||
.run(
|
||||
@@ -317,7 +318,7 @@ const layer = Layer.effect(
|
||||
env: options?.env,
|
||||
extendEnv: true,
|
||||
}),
|
||||
{ stdin: options?.stdin },
|
||||
{ stdin: options?.stdin, maxOutputBytes: options?.maxOutputBytes },
|
||||
)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
@@ -331,7 +332,8 @@ const layer = Layer.effect(
|
||||
),
|
||||
)
|
||||
const text = result.stdout.toString("utf8")
|
||||
if (result.exitCode === 0) return { text, stderr: result.stderr.toString("utf8") }
|
||||
if (result.exitCode === 0)
|
||||
return { text, stderr: result.stderr.toString("utf8"), truncated: result.stdoutTruncated }
|
||||
return yield* new OperationError({
|
||||
operation: operationName,
|
||||
directory: repository.worktree,
|
||||
@@ -385,9 +387,7 @@ const layer = Layer.effect(
|
||||
maximumUntrackedFileBytes?: number
|
||||
}) {
|
||||
const list = (args: string[]) =>
|
||||
repositoryOperation("refresh", input.repository, args).pipe(
|
||||
Effect.map((result) => result.text.split("\0").filter(Boolean)),
|
||||
)
|
||||
repositoryOperation("refresh", input.repository, args).pipe(Effect.map((result) => nuls(result.text)))
|
||||
const [tracked, untracked] = yield* Effect.all(
|
||||
[
|
||||
list(["diff-files", "--name-only", "-z", "--", input.scope]),
|
||||
@@ -464,13 +464,7 @@ const layer = Layer.effect(
|
||||
directory: input.repository.worktree,
|
||||
message: result.stderr.toString("utf8").trim() || "Failed to check ignored paths",
|
||||
})
|
||||
return new Set(
|
||||
result.stdout
|
||||
.toString("utf8")
|
||||
.split("\0")
|
||||
.filter(Boolean)
|
||||
.map((file) => RelativePath.make(file)),
|
||||
)
|
||||
return new Set(nuls(result.stdout.toString("utf8")).map((file) => RelativePath.make(file)))
|
||||
})
|
||||
|
||||
const writeTree = Effect.fn("Git.tree.write")(function* (repository: Repository) {
|
||||
@@ -499,19 +493,23 @@ const layer = Layer.effect(
|
||||
to: TreeID
|
||||
}) {
|
||||
// Undo needs both paths of a rename, not only its destination.
|
||||
return (yield* repositoryOperation("list_files", input.repository, [
|
||||
"diff",
|
||||
"--name-only",
|
||||
"--no-renames",
|
||||
"-z",
|
||||
input.from,
|
||||
input.to,
|
||||
])).text
|
||||
.split("\0")
|
||||
.filter(Boolean)
|
||||
.map((file) => RelativePath.make(file))
|
||||
return nuls(
|
||||
(yield* repositoryOperation("list_files", input.repository, [
|
||||
"diff",
|
||||
"--name-only",
|
||||
"--no-renames",
|
||||
"-z",
|
||||
input.from,
|
||||
input.to,
|
||||
])).text,
|
||||
).map((file) => RelativePath.make(file))
|
||||
})
|
||||
|
||||
/**
|
||||
* Three batched invocations over the tree pair instead of three per file. An
|
||||
* explicit empty selection diffs nothing; an absent one diffs every changed path.
|
||||
* Patch output is capped like VCS diffs: files past the cap get an empty patch.
|
||||
*/
|
||||
const treeDiff = Effect.fn("Git.tree.diff")(function* (input: {
|
||||
repository: Repository
|
||||
from: TreeID
|
||||
@@ -519,49 +517,57 @@ const layer = Layer.effect(
|
||||
context?: number
|
||||
paths?: readonly RelativePath[]
|
||||
}) {
|
||||
const paths = input.paths ?? (yield* treeFiles(input))
|
||||
return yield* Effect.forEach(paths, (file) =>
|
||||
Effect.gen(function* () {
|
||||
const statusText = (yield* repositoryOperation("diff", input.repository, [
|
||||
if (input.paths?.length === 0) return []
|
||||
const args = ["--no-renames", input.from, input.to, "--", ...(input.paths ?? [])]
|
||||
// Patch headers have no -z form: unquoted paths keep chunksByFile matching non-ASCII names.
|
||||
const [names, numbers, patch] = yield* Effect.all(
|
||||
[
|
||||
repositoryOperation("diff", input.repository, ["diff", "--name-status", "-z", ...args]),
|
||||
repositoryOperation("diff", input.repository, ["diff", "--numstat", "-z", ...args]),
|
||||
repositoryOperation(
|
||||
"diff",
|
||||
"--name-status",
|
||||
"--no-renames",
|
||||
input.from,
|
||||
input.to,
|
||||
"--",
|
||||
file,
|
||||
])).text.trim()
|
||||
const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified"
|
||||
const stats = (yield* repositoryOperation("diff", input.repository, [
|
||||
"diff",
|
||||
"--numstat",
|
||||
"--no-renames",
|
||||
input.from,
|
||||
input.to,
|
||||
"--",
|
||||
file,
|
||||
])).text.split("\t")
|
||||
const binary = stats[0] === "-" || stats[1] === "-"
|
||||
const patch = binary
|
||||
? ""
|
||||
: (yield* repositoryOperation("diff", input.repository, [
|
||||
"diff",
|
||||
`--unified=${input.context ?? 3}`,
|
||||
"--no-renames",
|
||||
input.from,
|
||||
input.to,
|
||||
"--",
|
||||
file,
|
||||
])).text
|
||||
return {
|
||||
file,
|
||||
status,
|
||||
additions: binary ? 0 : Number(stats[0] ?? 0),
|
||||
deletions: binary ? 0 : Number(stats[1] ?? 0),
|
||||
patch,
|
||||
} satisfies File.Diff
|
||||
input.repository,
|
||||
["-c", "core.quotepath=false", "diff", "--no-ext-diff", `--unified=${input.context ?? 3}`, ...args],
|
||||
{ maxOutputBytes: VcsPatch.MAX_TOTAL_PATCH_BYTES },
|
||||
),
|
||||
],
|
||||
{ concurrency: 3 },
|
||||
)
|
||||
const statuses = nuls(names.text)
|
||||
const files = statuses.flatMap((code, index) => {
|
||||
const file = statuses[index + 1]
|
||||
if (index % 2 !== 0 || !file) return []
|
||||
return [
|
||||
{
|
||||
file: RelativePath.make(file),
|
||||
status: code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified",
|
||||
} as const,
|
||||
]
|
||||
})
|
||||
const stats = new Map(
|
||||
nuls(numbers.text).flatMap((line) => {
|
||||
const [additions, deletions, ...file] = line.split("\t")
|
||||
if (!additions || !deletions || file.length === 0) return []
|
||||
return [
|
||||
[
|
||||
file.join("\t"),
|
||||
additions === "-" || deletions === "-"
|
||||
? { binary: true, additions: 0, deletions: 0 }
|
||||
: { binary: false, additions: Number(additions), deletions: Number(deletions) },
|
||||
] as const,
|
||||
]
|
||||
}),
|
||||
)
|
||||
const patches = VcsPatch.chunksByFile(patch, (index) => files[index]?.file)
|
||||
return files.map((entry) => {
|
||||
const stat = stats.get(entry.file)
|
||||
return {
|
||||
...entry,
|
||||
additions: stat?.additions ?? 0,
|
||||
deletions: stat?.deletions ?? 0,
|
||||
patch: stat?.binary ? "" : (patches.get(entry.file) ?? VcsPatch.emptyPatch(entry.file)),
|
||||
} satisfies File.Diff
|
||||
})
|
||||
})
|
||||
|
||||
const hasEntry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) {
|
||||
@@ -733,6 +739,11 @@ function execute(cwd: string, proc: AppProcess.Interface, args: string[]) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Split NUL-terminated git output into its records. */
|
||||
function nuls(text: string) {
|
||||
return text.split("\0").filter(Boolean)
|
||||
}
|
||||
|
||||
function resolvePath(cwd: string, value: string) {
|
||||
const trimmed = value.replace(/[\r\n]+$/, "")
|
||||
if (!trimmed) return cwd
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Image } from "./image.js"
|
||||
import { LocationWatcher } from "./filesystem/location-watcher.js"
|
||||
import { Integration } from "./integration.js"
|
||||
import { Location } from "./location.js"
|
||||
import { LocationMutation } from "./location-mutation.js"
|
||||
import { FileAccess } from "./file-access.js"
|
||||
import { ModelResolver } from "./model-resolver.js"
|
||||
import { Mcp } from "./mcp/index.js"
|
||||
import { Permission } from "./permission.js"
|
||||
@@ -82,7 +82,7 @@ const nodes = [
|
||||
Skill.node,
|
||||
InstructionBuiltIns.node,
|
||||
InstructionDiscovery.node,
|
||||
LocationMutation.node,
|
||||
FileAccess.node,
|
||||
FileMutation.node,
|
||||
Formatter.node,
|
||||
Mcp.node,
|
||||
|
||||
@@ -1,130 +1,3 @@
|
||||
export * as LocationMutation from "./location-mutation.js"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import path from "path"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { FSUtil } from "@opencode-ai/util/fs-util"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { Location } from "./location.js"
|
||||
import { Project } from "./project.js"
|
||||
import { AbsolutePath } from "./schema.js"
|
||||
|
||||
export const Kind = Schema.Literals(["file", "directory"])
|
||||
export type Kind = typeof Kind.Type
|
||||
|
||||
/**
|
||||
* Mutation paths do not accept project references. A leading `~` expands to
|
||||
* the home directory; other relative paths resolve from the active Location.
|
||||
* Paths outside it and its non-root project worktree require separate
|
||||
* `external_directory` approval.
|
||||
*/
|
||||
export const ResolveInput = Schema.Struct({
|
||||
path: Schema.String,
|
||||
/** Selects the external approval boundary; it does not validate the target type. */
|
||||
kind: Kind.pipe(Schema.optional),
|
||||
})
|
||||
export type ResolveInput = typeof ResolveInput.Type
|
||||
|
||||
export interface ExternalDirectoryAuthorization {
|
||||
readonly action: "external_directory"
|
||||
/** Lexical directory used as the external approval boundary. */
|
||||
readonly directory: string
|
||||
/** `external_directory` permission resource. */
|
||||
readonly resource: string
|
||||
readonly save: string
|
||||
}
|
||||
|
||||
export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({
|
||||
action: input.action,
|
||||
resources: [input.resource],
|
||||
save: [input.save],
|
||||
})
|
||||
|
||||
export interface Target {
|
||||
/** Absolute lexical path. */
|
||||
readonly absolute: string
|
||||
/** Permission resource: Location-relative for internal paths, absolute for external paths. */
|
||||
readonly resource: string
|
||||
readonly externalDirectory?: ExternalDirectoryAuthorization
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
/**
|
||||
* Resolve a path and derive its permission resources. A leading `~` expands
|
||||
* to the home directory; other relative paths resolve from the Location.
|
||||
* Paths outside it and its non-root project worktree require separate
|
||||
* `external_directory` approval. This does not approve the mutation.
|
||||
*/
|
||||
readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error>
|
||||
}
|
||||
|
||||
/** Lexical absolute path, normalizing Windows shell paths and expanding `~` before resolution. */
|
||||
export const resolvePath = (directory: string, input: string, home = Global.Path.home) => {
|
||||
const normalized = FSUtil.windowsPath(input)
|
||||
return path.resolve(
|
||||
directory,
|
||||
normalized === "~"
|
||||
? home
|
||||
: normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))
|
||||
? path.join(home, normalized.slice(2))
|
||||
: normalized,
|
||||
)
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/LocationMutation") {}
|
||||
|
||||
const slash = (value: string) => value.replaceAll("\\", "/")
|
||||
|
||||
const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const location = yield* Location.Service
|
||||
|
||||
const resolve = Effect.fnUntraced(function* (input: ResolveInput) {
|
||||
const absolute = resolvePath(location.directory, input.path)
|
||||
const worktree = path.resolve(location.project.directory)
|
||||
const internal =
|
||||
FSUtil.contains(location.directory, absolute) ||
|
||||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
|
||||
if (internal) {
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(path.relative(location.directory, absolute) || "."),
|
||||
} satisfies Target
|
||||
}
|
||||
const type =
|
||||
input.kind === "directory"
|
||||
? "Directory"
|
||||
: input.kind === "file"
|
||||
? "File"
|
||||
: (yield* fs.stat(absolute).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined)))
|
||||
?.type
|
||||
const externalDirectory = type === "Directory" ? absolute : path.dirname(absolute)
|
||||
const externalResource = slash(path.join(externalDirectory, "*"))
|
||||
return {
|
||||
absolute,
|
||||
resource: slash(absolute),
|
||||
externalDirectory: {
|
||||
action: "external_directory",
|
||||
directory: externalDirectory,
|
||||
resource: externalResource,
|
||||
save: slash(
|
||||
path.join(
|
||||
(yield* Project.root(fs, AbsolutePath.make(externalDirectory))) ?? externalDirectory,
|
||||
"*",
|
||||
),
|
||||
),
|
||||
},
|
||||
} satisfies Target
|
||||
})
|
||||
|
||||
return Service.of({ resolve })
|
||||
}),
|
||||
)
|
||||
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [FSUtil.node, Location.node],
|
||||
})
|
||||
/** @deprecated Use FileAccess for path resolution and authorization. */
|
||||
export { FileAccess as LocationMutation } from "./file-access.js"
|
||||
export * from "./file-access.js"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export * as McpOAuth from "./oauth.js"
|
||||
|
||||
import { auth, parseErrorResponse, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import {
|
||||
auth,
|
||||
discoverOAuthServerInfo,
|
||||
parseErrorResponse,
|
||||
type OAuthClientProvider,
|
||||
type OAuthServerInfo,
|
||||
} from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
|
||||
import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js"
|
||||
import { Cause, Deferred, Effect } from "effect"
|
||||
@@ -10,6 +16,12 @@ import { OauthCallbackPage } from "../oauth/page.js"
|
||||
import type { Integration } from "../integration.js"
|
||||
import { ErrorSummary } from "../util/error-summary.js"
|
||||
|
||||
/**
|
||||
* opencode's OAuth Client ID Metadata Document. Authorization servers that support CIMD accept this URL as the
|
||||
* client_id and fetch it to learn our name and redirect URIs, so no per-server dynamic registration is needed.
|
||||
*/
|
||||
export const CLIENT_METADATA_URL = "https://opencode.ai/oauth/opencode/client.json"
|
||||
|
||||
/** Observe OAuth failures before the SDK handles them by invalidating credentials or redirecting. */
|
||||
export const loggedFetch = (fields: { readonly server: string; readonly directory?: string }) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -79,6 +91,10 @@ export interface Options {
|
||||
readonly state?: string
|
||||
/** Statically pre-registered client credentials from config; when set, the SDK skips dynamic registration. */
|
||||
readonly client?: { readonly id: string; readonly secret?: string }
|
||||
/** Use opencode's Client ID Metadata Document as the client_id instead of registering dynamically. */
|
||||
readonly clientMetadataUrl?: string
|
||||
/** Pre-fetched authorization server discovery so the SDK does not repeat it. */
|
||||
readonly discovery?: OAuthServerInfo
|
||||
/** Invoked by the SDK to drop credentials it has determined are invalid (e.g. a rejected refresh token). */
|
||||
readonly invalidate?: (scope: "all" | "client" | "tokens" | "verifier" | "discovery") => void | Promise<void>
|
||||
/** Receives the authorization URL so the caller can open a browser and capture the eventual code. */
|
||||
@@ -95,6 +111,8 @@ export const provider = (options: Options): OAuthClientProvider => {
|
||||
const client = options.client
|
||||
return {
|
||||
redirectUrl: options.redirectUrl,
|
||||
...(options.clientMetadataUrl ? { clientMetadataUrl: options.clientMetadataUrl } : {}),
|
||||
...(options.discovery ? { discoveryState: () => options.discovery } : {}),
|
||||
clientMetadata: {
|
||||
redirect_uris: [options.redirectUrl],
|
||||
client_name: "opencode",
|
||||
@@ -252,12 +270,32 @@ export const authorize = (input: {
|
||||
})
|
||||
yield* Effect.addFinalizer(() => Effect.sync(() => server.close()))
|
||||
|
||||
// Discover the authorization server up front so we can decide how to identify ourselves. CIMD only works
|
||||
// when the server advertises it, accepts public clients (our document declares no client secret), and the
|
||||
// redirect is our own loopback URL (a user-configured redirect_uri is not in the published document).
|
||||
// A configured client_id is pre-registered and always wins.
|
||||
const discovery = yield* Effect.tryPromise({
|
||||
try: () => discoverOAuthServerInfo(input.config.url, { fetchFn }),
|
||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
||||
})
|
||||
const cimd =
|
||||
!oauth?.client_id &&
|
||||
!oauth?.redirect_uri &&
|
||||
discovery.authorizationServerMetadata?.client_id_metadata_document_supported === true &&
|
||||
(discovery.authorizationServerMetadata.token_endpoint_auth_methods_supported?.includes("none") ?? false)
|
||||
yield* Effect.logInfo("mcp oauth client registration selected", {
|
||||
...fields,
|
||||
registration: oauth?.client_id ? "static" : cimd ? "cimd" : "dcr",
|
||||
})
|
||||
|
||||
let authorizationUrl: URL | undefined
|
||||
const oauthProvider = provider({
|
||||
redirectUrl: oauth?.redirect_uri ?? `http://127.0.0.1:${port}${redirectPath}`,
|
||||
scope: oauth?.scope,
|
||||
state,
|
||||
client: oauth?.client_id ? { id: oauth.client_id, secret: oauth.client_secret } : undefined,
|
||||
clientMetadataUrl: cimd ? CLIENT_METADATA_URL : undefined,
|
||||
discovery,
|
||||
onRedirect: (url) => {
|
||||
authorizationUrl = url
|
||||
return run(Effect.logInfo("mcp oauth awaiting authorization", fields))
|
||||
|
||||
@@ -52,10 +52,24 @@ export class UnresolvedProviderVariablesError extends Schema.TaggedError<Unresol
|
||||
}
|
||||
}
|
||||
|
||||
export class UnsupportedCompactionError extends Schema.TaggedError<UnsupportedCompactionError>()(
|
||||
"SessionRunnerModel.UnsupportedCompactionError",
|
||||
{
|
||||
providerID: Provider.ID,
|
||||
modelID: ID,
|
||||
route: Schema.String,
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Provider compaction is not supported by ${this.providerID}/${this.modelID} (${this.route})`
|
||||
}
|
||||
}
|
||||
|
||||
export type Error =
|
||||
| VariantUnavailableError
|
||||
| UnsupportedPackageError
|
||||
| UnresolvedProviderVariablesError
|
||||
| UnsupportedCompactionError
|
||||
| Integration.AuthorizationError
|
||||
|
||||
export interface Resolved {
|
||||
@@ -69,6 +83,8 @@ export interface Resolved {
|
||||
readonly cost: Info["cost"]
|
||||
/** Catalog token limits used by Core for context management. */
|
||||
readonly limit: Info["limit"]
|
||||
/** Model policy overrides the provider policy; omitted means local compaction. */
|
||||
readonly compaction?: Info["compaction"]
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
@@ -115,9 +131,20 @@ export const fromCatalogModel = (
|
||||
model: Info,
|
||||
credential?: Credential.Value,
|
||||
dependencies?: Dependencies,
|
||||
): Effect.Effect<LanguageModel, UnsupportedPackageError | UnresolvedProviderVariablesError> =>
|
||||
): Effect.Effect<
|
||||
LanguageModel,
|
||||
UnsupportedPackageError | UnresolvedProviderVariablesError | UnsupportedCompactionError
|
||||
> =>
|
||||
resolveCatalogModel(model, credential, dependencies).pipe(
|
||||
Effect.flatMap((resolved) => validateProviderVariables(model, resolved)),
|
||||
Effect.flatMap((resolved) => {
|
||||
// Reject provider compaction policies up front so the misconfiguration surfaces before any step runs.
|
||||
if (model.compaction?.mode !== "provider" || resolved.route.compact?.trigger || resolved.route.compact?.endpoint)
|
||||
return Effect.succeed(resolved)
|
||||
return Effect.fail(
|
||||
new UnsupportedCompactionError({ providerID: model.providerID, modelID: model.id, route: resolved.route.id }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const resolveCatalogModel = Effect.fn("ModelResolver.resolveCatalogModel")(function* (
|
||||
@@ -296,6 +323,7 @@ export const layer = Layer.effect(
|
||||
capabilities: selected.capabilities,
|
||||
cost: selected.cost,
|
||||
limit: selected.limit,
|
||||
compaction: selected.compaction,
|
||||
}
|
||||
})
|
||||
return Service.of({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user