mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-04 06:26:19 +00:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94c9f366ec | ||
|
|
96ecd60bdb | ||
|
|
15c4e5d537 | ||
|
|
5617044dc9 | ||
|
|
0048c97c27 | ||
|
|
f72a103a4b | ||
|
|
f74512bfe0 | ||
|
|
63b7de9434 | ||
|
|
e8c75c2de0 | ||
|
|
4ead2614a5 | ||
|
|
15c644ecb4 | ||
|
|
973a3b570b | ||
|
|
20591148f2 | ||
|
|
75e485c095 | ||
|
|
0d3ea96bb7 | ||
|
|
583ae4ff26 | ||
|
|
4aed5b1daa | ||
|
|
cf1acfd2fa | ||
|
|
6f97cea63b | ||
|
|
4270fcc9c2 | ||
|
|
c86ee984ba | ||
|
|
05deab90ed | ||
|
|
643cca88e3 | ||
|
|
54900febfc | ||
|
|
c73650371b | ||
|
|
15406155aa | ||
|
|
abfaa5210b | ||
|
|
fbf47807fb | ||
|
|
67146125dc | ||
|
|
8ba230e2fb | ||
|
|
12e1821cc7 | ||
|
|
63e66ca3e6 | ||
|
|
cc782a0cc8 |
No files matched your search
@@ -18,9 +18,20 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
# A pull request checks out its merge into the current base; the first parent is that base.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Run checks
|
||||
run: bun run check
|
||||
|
||||
# Every GUI package file (app, desktop, gui-extensions, ui, session-ui) a pull request adds or edits must be free of
|
||||
# oxlint problems, warn-level rules (anti-slop) included. Other packages are not affected.
|
||||
- name: Lint changed files
|
||||
if: github.event_name == 'pull_request'
|
||||
# Against the merge's first parent, so base-branch commits the pull request has not merged are not counted as its
|
||||
# changes (the event's base SHA can predate them).
|
||||
run: bun run lint:changed HEAD^1
|
||||
@@ -252,6 +252,13 @@ jobs:
|
||||
CI: true
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Run app component tests
|
||||
if: ${{ !cancelled() && env.E2E_ENABLED == 'true' }}
|
||||
run: bun --cwd packages/app test:components
|
||||
env:
|
||||
CI: true
|
||||
timeout-minutes: 15
|
||||
|
||||
- name: Upload Playwright artifacts
|
||||
if: always() && env.E2E_ENABLED == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -264,3 +271,5 @@ jobs:
|
||||
packages/app/e2e/playwright-report
|
||||
packages/session-ui/component-tests/test-results
|
||||
packages/session-ui/component-tests/playwright-report
|
||||
packages/app/component-tests/test-results
|
||||
packages/app/component-tests/playwright-report
|
||||
@@ -63,8 +63,60 @@
|
||||
"anti-slop-effect/prefer-effect-match": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/*.ts", "packages/gui-extensions/src/*.tsx"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "solid-js",
|
||||
"importNames": ["createEffect", "createRenderEffect", "createComputed"],
|
||||
"message": "Extension code runs side effects through the SDK: createKeyed(source, fn, { otherwise }) per provider generation or value, createMemo or a plain function for derived values, createLatest for async data, createVisitState for per-visit state, and the handler for logic a user action causes. An escape hatch needs an oxlint-disable comment with a reason."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/*/**"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "solid-js",
|
||||
"importNames": ["createEffect", "createRenderEffect", "createComputed"],
|
||||
"message": "Extension code runs side effects through the SDK: createKeyed(source, fn, { otherwise }) per provider generation or value, createMemo or a plain function for derived values, createLatest for async data, createVisitState for per-visit state, and the handler for logic a user action causes. An escape hatch needs an oxlint-disable comment with a reason."
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"regex": "^@opencode/(app|desktop)(/|$)",
|
||||
"message": "GUI extensions never import the app or desktop packages. Use the SDK."
|
||||
},
|
||||
{
|
||||
"regex": "^@/",
|
||||
"message": "GUI extensions never import app internals. Use the SDK."
|
||||
},
|
||||
{
|
||||
"group": ["../*/*", "!../*/contract", "!../sdk/*"],
|
||||
"message": "Import another extension only through its contract.ts."
|
||||
},
|
||||
{
|
||||
"regex": "\\.css$",
|
||||
"message": "Import CSS with ?inline and contribute it with ctx.add(Style, css)."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/gui-extensions/src/sdk/**"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
"zod": "catalog:",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0-rc.118",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
@@ -407,6 +407,7 @@
|
||||
"@types/node": "catalog:",
|
||||
"@types/which": "3.0.4",
|
||||
"drizzle-kit": "catalog:",
|
||||
"fast-check": "catalog:",
|
||||
},
|
||||
},
|
||||
"packages/desktop": {
|
||||
@@ -556,7 +557,7 @@
|
||||
"name": "@opencode/http-recorder",
|
||||
"version": "2.0.22",
|
||||
"dependencies": {
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.118",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
@@ -1148,10 +1149,10 @@
|
||||
"catalog": {
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@corvu/drawer": "0.2.4",
|
||||
"@effect/opentelemetry": "4.0.0-rc.112",
|
||||
"@effect/platform-node": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.112",
|
||||
"@effect/opentelemetry": "4.0.0-rc.118",
|
||||
"@effect/platform-node": "4.0.0-rc.118",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.118",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.118",
|
||||
"@hono/standard-validator": "0.2.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.13",
|
||||
@@ -1191,7 +1192,8 @@
|
||||
"dompurify": "3.4.14",
|
||||
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
||||
"drizzle-orm": "1.0.0-rc.5-169397b",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0-rc.118",
|
||||
"fast-check": "4.9.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"get-east-asian-width": "1.6.0",
|
||||
"hono": "4.10.7",
|
||||
@@ -1723,13 +1725,13 @@
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.12.0", "", {}, "sha512-mlUE+rZ8CatQekLhnaiN91Iemdd+e2gFKooGlnRB3oPTL3VghLfX24dx7HrzMNeC1JrIB/0kpsfyty3f5HNfxQ=="],
|
||||
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="],
|
||||
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.118", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.118" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-N5dvEeCj1W/9zgBvyyp/GucOKOEJdV7QNirWp+4g/axFKTB/cfV2gx/ZhMDUb69HzlyRmOelNvKEmB3RA0PdVw=="],
|
||||
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="],
|
||||
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.118", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.118", "undici": "^8.11.2" }, "peerDependencies": { "effect": "^4.0.0-rc.118", "redis": ">=5.0.0 <7.0.0" } }, "sha512-qdAxPZ+5bKxVnzkwziqcHZTHRHz2hyfFX6NhwEmm75CBpmaIzXIZR2LRqGd1/8eI3J3cOrXcAYzByhln7T1/EA=="],
|
||||
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.112", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.3" }, "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-ttjz0xKamFN7vL8pNDYVwddJLjZvqKePc05djlz2VcdaKbLsnYbtMnL1rbOfHgEnIUSHGh7FkjaN4DM1Ov81sQ=="],
|
||||
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.118", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.22.0" }, "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-pNbmIlxbAUxAthUNsp2xm1NLG9kxoYecsPkoYl9mNF2oxPcKDxAvkOar7tYnLSUpXJeATm4pxYVipdZjQfqO4Q=="],
|
||||
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.112", "", { "peerDependencies": { "effect": "^4.0.0-rc.112" } }, "sha512-EqR8pWZo3VzedRvP6qo5HcfqNmFvSa+sQLWQl3oBMx2KY1FCc8NYjep79wYjNkW0qABnffrPUXCMTOncd1A+5g=="],
|
||||
"@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-rc.118", "", { "peerDependencies": { "effect": "^4.0.0-rc.118" } }, "sha512-UF0oVOK7mQvEwpxySR9VTk5YoP5luRI//Z8NMOJKVG/SbZbsORGACd7Np+KZRiXXjgzSXEpfGb7Gs3G1cKfUCg=="],
|
||||
|
||||
"@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="],
|
||||
|
||||
@@ -2063,18 +2065,6 @@
|
||||
|
||||
"@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||
@@ -2179,6 +2169,8 @@
|
||||
|
||||
"@opencode-ai/pty-linux-x64-gnu": ["@opencode-ai/pty-linux-x64-gnu@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GhdrmbUzxGHRfWvt1qutgVD4HLb7aSBsgWGjFsc6k8HjO5ZbdaC3ScRyd512YAAG5KAROLKZ6+0SEBsm5xFYlA=="],
|
||||
|
||||
"@opencode-ai/pty-linux-x64-musl": ["@opencode-ai/pty-linux-x64-musl@0.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EbHchDsMmL5aOReIoo8NvQkKnyhyKmoL2RleF2JYF76va3FKuksmwsOBgQJh54i+QA4Fd0+9x54Q+2fJOzPiRQ=="],
|
||||
|
||||
"@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-beta-18050", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.111" } }, "sha512-/D6VXaWlytTXR3IOiMLIKuPcfp7FQNUzRPm9z3K7UBFd1Bw4q/WZksaf5RVcBGz+0YRxYMc1V4D7MFlceSgtyg=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.21", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-k6iHQ5C8wOPglk+LgFyYnst168cGMQYumgpbVoeXJ+iC1AtvwD5zmjuF8CxMze/y9G1K2bOeO6p9yRvA7eHZLA=="],
|
||||
@@ -3911,7 +3903,7 @@
|
||||
|
||||
"editorconfig": ["editorconfig@1.0.7", "", { "dependencies": { "@one-ini/wasm": "0.1.1", "commander": "^10.0.0", "minimatch": "^9.0.1", "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" } }, "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw=="],
|
||||
|
||||
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
|
||||
"effect": ["effect@4.0.0-rc.118", "", {}, "sha512-hxhpCd+swzrtu9lnyPpEFre2ODQzJ9qxgyL1oHIEY8NKnW4PxTbM1XzplMZXiAsLD62HO+KJc41Nqxr9T791ZQ=="],
|
||||
|
||||
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
|
||||
|
||||
@@ -4749,7 +4741,7 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
|
||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
@@ -4797,10 +4789,6 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
|
||||
|
||||
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
@@ -4843,8 +4831,6 @@
|
||||
|
||||
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||
|
||||
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
|
||||
|
||||
"node-html-parser": ["node-html-parser@7.1.0", "", { "dependencies": { "css-select": "^5.1.0", "he": "1.2.0" } }, "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ=="],
|
||||
|
||||
"node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
|
||||
@@ -5641,7 +5627,7 @@
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
"undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
@@ -6093,15 +6079,13 @@
|
||||
|
||||
"@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"@cloudflare/vite-plugin/vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="],
|
||||
|
||||
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@effect/platform-node-shared/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=="],
|
||||
"@effect/platform-node-shared/ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="],
|
||||
|
||||
"@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="],
|
||||
|
||||
@@ -6693,6 +6677,8 @@
|
||||
|
||||
"tw-to-css/tailwindcss": ["tailwindcss@3.3.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.18.2", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w=="],
|
||||
|
||||
"unifont/undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -40,6 +40,8 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
copyDesktopItems
|
||||
]
|
||||
++ lib.optionals stdenv.hostPlatform.isDarwin [
|
||||
darwin.cctools
|
||||
darwin.sigtool
|
||||
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
|
||||
darwin.autoSignDarwinBinariesHook
|
||||
];
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-onbxt1PCyU4Nh14S5gIeLH20OtbQ7v2ipRq1zqZkLWs=",
|
||||
"x86_64-linux": "sha256-2RlbJRTEKSliuUbAE2lAktX63JFR/RKAuLkcCou8wb4=",
|
||||
"aarch64-linux": "sha256-P7DAE018lTJNGmttg87U9oaTAuw/wHlnMStdnXYKCr0=",
|
||||
"aarch64-darwin": "sha256-N+NfV1ObOTnW+ez7As+CS+6ci/iRGCQ6cslvyQFCp6E="
|
||||
}
|
||||
|
||||
+8
-6
@@ -18,7 +18,8 @@
|
||||
"dev:www": "bun run --cwd services/www dev",
|
||||
"dev:storybook": "bun --cwd packages/storybook storybook",
|
||||
"bench:devex": "bun run --cwd packages/app test:bench:devex",
|
||||
"lint": "oxlint && ast-grep scan -c script/ast-grep/gui-extensions/sgconfig.yml",
|
||||
"lint": "oxlint && ast-grep scan -c script/ast-grep/gui-extensions/sgconfig.yml && bun script/sdk-docs.ts",
|
||||
"lint:changed": "bun script/lint-changed.ts",
|
||||
"lint:effect-patterns": "ast-grep scan -c script/ast-grep/sgconfig.yml packages/util/src packages/core/src packages/server/src packages/protocol/src packages/cli/src",
|
||||
"lint:effect-simplifications": "ast-grep scan -c script/ast-grep/effect-simplifications/sgconfig.yml --off=unused-suppression packages",
|
||||
"test:lint-rules": "ast-grep test -c script/ast-grep/sgconfig.yml",
|
||||
@@ -42,10 +43,10 @@
|
||||
"packages/stats/*"
|
||||
],
|
||||
"catalog": {
|
||||
"@effect/opentelemetry": "4.0.0-rc.112",
|
||||
"@effect/platform-node": "4.0.0-rc.112",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.112",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.112",
|
||||
"@effect/opentelemetry": "4.0.0-rc.118",
|
||||
"@effect/platform-node": "4.0.0-rc.118",
|
||||
"@effect/platform-node-shared": "4.0.0-rc.118",
|
||||
"@effect/sql-sqlite-bun": "4.0.0-rc.118",
|
||||
"@npmcli/arborist": "9.4.0",
|
||||
"@types/bun": "1.4.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -79,7 +80,8 @@
|
||||
"dompurify": "3.4.14",
|
||||
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
||||
"drizzle-orm": "1.0.0-rc.5-169397b",
|
||||
"effect": "4.0.0-rc.112",
|
||||
"effect": "4.0.0-rc.118",
|
||||
"fast-check": "4.9.0",
|
||||
"ai": "6.0.168",
|
||||
"cross-spawn": "7.0.6",
|
||||
"hono": "4.10.7",
|
||||
|
||||
@@ -27,6 +27,31 @@ Run `LLM.stream(...)` instead of `generate` when you want incremental `LLMEvent`
|
||||
`LLM.request(...)`. The event stream is provider-neutral — same shape across OpenAI Chat, OpenAI Responses,
|
||||
Anthropic Messages, Gemini, Bedrock Converse, and any OpenAI-compatible deployment.
|
||||
|
||||
### Google Interactions
|
||||
|
||||
`Google.configure({ apiKey }).interactions(modelID)` selects the Interactions API; `.model(modelID)` still selects
|
||||
GenerateContent. The package entrypoint is `@opencode/ai/providers/google/interactions`.
|
||||
|
||||
```ts
|
||||
const model = Google.configure({ apiKey }).interactions("gemini-3.8-flash")
|
||||
const response = yield* LLM.generate({
|
||||
model,
|
||||
prompt: "Say hello.",
|
||||
providerOptions: { thinkingLevel: "low", thinkingSummaries: "auto", store: true },
|
||||
})
|
||||
```
|
||||
|
||||
Interactions supports text output, streamed function calls, native tool results, thought signatures, and multimodal
|
||||
input. Full-history replay is the default (`store: false`); implicit caching works without retained interactions.
|
||||
For server-side continuation, set `store: true` on the predecessor, read `interactionId` from the final event's
|
||||
`providerMetadata.google`, and pass `previousInteractionId` on the next request with **only new messages**. Repeat
|
||||
the system instructions and tool declarations on each request. Set `store: true` on each response you intend to
|
||||
continue from. The package does not automatically select or persist continuation IDs.
|
||||
|
||||
Raw usage is preserved in `usage.providerMetadata.google`. `inputTokens` follows Google's top-level accounting;
|
||||
`contextTokens` uses its full `raw_prompt_token` count when supplied. These can differ substantially with server-side
|
||||
continuation. Explicit caches, hosted tools, and generated media are not supported by this initial protocol.
|
||||
|
||||
The same configured facade names image, video, speech, and transcription models. `Image.generate` resolves the
|
||||
provider's image route from the model and returns `Media.Asset`s with lazily decoded bytes:
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { OpenAI } from "@opencode/ai/providers"
|
||||
* hover imports and local values to see how the public API is typed.
|
||||
*/
|
||||
|
||||
const apiKey = Config.redacted("OPENAI_API_KEY")
|
||||
const apiKey = Config.Redacted("OPENAI_API_KEY")
|
||||
|
||||
// 1. Configure a provider. The configured facade records provider identity,
|
||||
// deployment options, authentication, and defaults. Per-modality selectors pick
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as path from "node:path"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import { Config, ConfigProvider, Effect, FileSystem, PlatformError, Redacted } from "effect"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
||||
import { FetchHttpClient, HttpClient, HttpClientRequest, type HttpClientResponse } from "effect/http"
|
||||
import * as ProviderShared from "../src/protocols/shared"
|
||||
import { CloudflareAIGateway } from "../src/providers/cloudflare-ai-gateway.js"
|
||||
import { CloudflareWorkersAI } from "../src/providers/cloudflare-workers-ai.js"
|
||||
@@ -383,7 +383,7 @@ const readEnvFile = Effect.fn("RecordingEnv.readFile")(function* () {
|
||||
})
|
||||
|
||||
const readConfigString = (provider: ConfigProvider.ConfigProvider, name: string) =>
|
||||
Config.string(name)
|
||||
Config.String(name)
|
||||
.parse(provider)
|
||||
.pipe(
|
||||
Effect.match({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
ChoiceQuestion,
|
||||
EvaluationInput,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export * as Media from "./media.js"
|
||||
|
||||
import { Effect, Encoding, FileSystem, Schema, SchemaGetter } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Effect, FileSystem, Schema, SchemaGetter } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { ProviderID } from "./schema/ids.js"
|
||||
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js"
|
||||
import { ProviderMetadata } from "./schema/options.js"
|
||||
@@ -140,7 +141,7 @@ export class Asset {
|
||||
inline(): Inline | undefined {
|
||||
const source = this.source
|
||||
if (source.type !== "bytes" && source.type !== "base64") return undefined
|
||||
const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Encoding.encodeBase64(source.data))
|
||||
const base64 = source.type === "base64" ? source.data : (this.#base64 ??= Base64.encode(source.data))
|
||||
const mime = this.mediaType.toLowerCase()
|
||||
return { mime, base64, dataUrl: `data:${mime};base64,${base64}` }
|
||||
}
|
||||
@@ -155,7 +156,7 @@ export class Asset {
|
||||
return Effect.fail(invalid(`Cannot materialize provider ref ${source.provider}:${source.id}`))
|
||||
const decoded =
|
||||
source.type === "base64"
|
||||
? Effect.fromResult(Encoding.decodeBase64(source.data)).pipe(
|
||||
? Effect.fromResult(Base64.decode(source.data)).pipe(
|
||||
Effect.mapError((cause) => invalid(`Media asset contains invalid base64 data`, cause)),
|
||||
)
|
||||
: download(source, this.headers)
|
||||
@@ -168,7 +169,7 @@ export class Asset {
|
||||
const source = this.source
|
||||
if (source.type === "base64") return Effect.succeed(source.data)
|
||||
if (this.#base64 !== undefined) return Effect.succeed(this.#base64)
|
||||
return this.bytes().pipe(Effect.map((data) => (this.#base64 = Encoding.encodeBase64(data))))
|
||||
return this.bytes().pipe(Effect.map((data) => (this.#base64 = Base64.encode(data))))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -183,7 +184,7 @@ export class Asset {
|
||||
toJSON() {
|
||||
const source = this.source
|
||||
return {
|
||||
source: source.type === "bytes" ? { ...source, data: Encoding.encodeBase64(source.data) } : source,
|
||||
source: source.type === "bytes" ? { ...source, data: Base64.encode(source.data) } : source,
|
||||
info: this.info,
|
||||
providerMetadata: this.providerMetadata,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
|
||||
import { JsonObject, ProviderShared } from "./shared.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
import { ResponsesHostedTools } from "./utils/responses-hosted-tools.js"
|
||||
|
||||
@@ -25,8 +25,6 @@ const WebExtractorItem = Schema.StructWithRest(
|
||||
)
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, WebExtractorItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
enable_thinking: Options.fields.enableThinking,
|
||||
previous_response_id: Options.fields.previousResponseId,
|
||||
conversation: Options.fields.conversation,
|
||||
@@ -52,7 +50,7 @@ export const protocol = Protocol.make({
|
||||
const opts = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))(req.providerOptions ?? {})
|
||||
const body = yield* OpenResponses.fromRequestWithAdapter(req, adapter)
|
||||
const choice = body.tool_choice
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
|
||||
return {
|
||||
...body,
|
||||
enable_thinking: opts.enableThinking,
|
||||
previous_response_id: opts.previousResponseId,
|
||||
@@ -62,7 +60,7 @@ export const protocol = Protocol.make({
|
||||
typeof choice === "object" && choice.type === "function"
|
||||
? { type: "allowed_tools" as const, mode: "required" as const, tools: [choice] }
|
||||
: choice,
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
stream: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import {
|
||||
AIError,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
@@ -610,7 +611,7 @@ interface ParserState {
|
||||
readonly reasoningRedactedContent: Readonly<Record<number, Uint8Array[]>>
|
||||
}
|
||||
|
||||
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => Encoding.encodeBase64(concatBytes(chunks))
|
||||
const encodeRedactedContent = (chunks: ReadonlyArray<Uint8Array>) => Base64.encode(concatBytes(chunks))
|
||||
|
||||
const step = (state: ParserState, event: BedrockEvent) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -659,7 +660,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
||||
const events: LLMEvent[] = []
|
||||
const redactedChunk = yield* (() => {
|
||||
if (reasoning.redactedContent === undefined) return Effect.succeed(undefined)
|
||||
return Effect.fromResult(Encoding.decodeBase64(reasoning.redactedContent)).pipe(
|
||||
return Effect.fromResult(Base64.decode(reasoning.redactedContent)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
import { Effect, Encoding, Stream } from "effect"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { AIError, AIErrorReason, InvalidProviderOutputError } from "../schema/index.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { ProviderShared } from "./shared.js"
|
||||
@@ -47,7 +48,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI
|
||||
route,
|
||||
classification: "incomplete-stream",
|
||||
message: `Incomplete Bedrock Converse event-stream frame: ${remaining.length} buffered bytes remain at end of stream`,
|
||||
body: Encoding.encodeBase64(remaining),
|
||||
body: Base64.encode(remaining),
|
||||
}),
|
||||
})
|
||||
return [state, []] as const
|
||||
@@ -68,7 +69,7 @@ const consumeFrames = (route: string) => (state: FrameBufferState, input: FrameI
|
||||
`Failed to decode Bedrock Converse event-stream frame: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
Encoding.encodeBase64(view.subarray(0, totalLength)),
|
||||
Base64.encode(view.subarray(0, totalLength)),
|
||||
error,
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { mergeJsonRecords, type OpenString } from "../schema/index.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
import { mergeJsonRecords, type OpenString } from "../schema/index.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -0,0 +1,561 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import {
|
||||
AIError,
|
||||
LLMEvent,
|
||||
ProviderID,
|
||||
Usage,
|
||||
type LLMRequest,
|
||||
type ProviderMetadata,
|
||||
type ToolResultPart,
|
||||
} from "../schema/index.js"
|
||||
import { Media } from "../media.js"
|
||||
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js"
|
||||
import { encodeJson } from "../utils/json.js"
|
||||
import { JsonObject, knownString, lenient, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { Lifecycle } from "./utils/lifecycle.js"
|
||||
import { MediaInput } from "./utils/media-input.js"
|
||||
import { ToolStream } from "./utils/tool-stream.js"
|
||||
|
||||
const ADAPTER = "google-interactions"
|
||||
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
// =============================================================================
|
||||
// Public Model Input
|
||||
// =============================================================================
|
||||
const ThinkingLevel = knownString<"minimal" | "low" | "medium" | "high">()
|
||||
const Options = Schema.Struct({
|
||||
previousInteractionId: lenient(Schema.String),
|
||||
store: lenient(Schema.Boolean),
|
||||
thinkingLevel: lenient(ThinkingLevel),
|
||||
thinkingSummaries: lenient(knownString<"auto" | "none">()),
|
||||
serviceTier: lenient(knownString<"standard" | "flex" | "priority">()),
|
||||
})
|
||||
export type OptionsInput = typeof Options.Encoded
|
||||
export type ProviderOptionsInput = OptionsInput
|
||||
const decodeOptions = ProviderShared.validateWith(Schema.decodeUnknownEffect(Options))
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Schema
|
||||
// =============================================================================
|
||||
const Text = Schema.Struct({ type: Schema.Literal("text"), text: Schema.String })
|
||||
const MediaContent = Schema.Struct({
|
||||
type: Schema.Literals(["image", "audio", "video", "document"]),
|
||||
data: Schema.optional(Schema.String),
|
||||
uri: Schema.optional(Schema.String),
|
||||
mime_type: Schema.String,
|
||||
})
|
||||
const Content = Schema.Union([Text, MediaContent])
|
||||
const InputStep = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literals(["user_input", "model_output"]), content: Schema.Array(Content) }),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("thought"),
|
||||
signature: Schema.optional(Schema.String),
|
||||
summary: Schema.optional(Schema.Array(Text)),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("function_call"),
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
arguments: Schema.Unknown,
|
||||
signature: Schema.optional(Schema.String),
|
||||
}),
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("function_result"),
|
||||
call_id: Schema.String,
|
||||
name: Schema.String,
|
||||
result: Schema.Unknown,
|
||||
is_error: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
])
|
||||
type InputStep = typeof InputStep.Type
|
||||
const ToolChoice = Schema.Union([
|
||||
Schema.Literals(["auto", "any", "none"]),
|
||||
Schema.Struct({ allowed_tools: Schema.Struct({ mode: Schema.Literal("any"), tools: Schema.Array(Schema.String) }) }),
|
||||
])
|
||||
const Body = Schema.Struct({
|
||||
model: Schema.String,
|
||||
input: Schema.Array(InputStep),
|
||||
stream: Schema.Literal(true),
|
||||
store: Schema.Boolean,
|
||||
previous_interaction_id: Schema.optional(Schema.String),
|
||||
system_instruction: Schema.optional(Schema.String),
|
||||
service_tier: Schema.optional(Schema.String),
|
||||
tools: Schema.optional(
|
||||
Schema.Array(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("function"),
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
parameters: JsonObject,
|
||||
}),
|
||||
),
|
||||
),
|
||||
generation_config: Schema.Struct({
|
||||
max_output_tokens: Schema.optional(Schema.Number),
|
||||
temperature: Schema.optional(Schema.Number),
|
||||
top_p: Schema.optional(Schema.Number),
|
||||
seed: Schema.optional(Schema.Number),
|
||||
stop_sequences: Schema.optional(Schema.Array(Schema.String)),
|
||||
thinking_level: Schema.optional(ThinkingLevel),
|
||||
thinking_summaries: Schema.optional(Schema.String),
|
||||
tool_choice: Schema.optional(ToolChoice),
|
||||
}),
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Streaming Event Schema
|
||||
// =============================================================================
|
||||
const RawUsage = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
total_input_tokens: optionalNull(Schema.Number),
|
||||
total_cached_tokens: optionalNull(Schema.Number),
|
||||
total_output_tokens: optionalNull(Schema.Number),
|
||||
total_thought_tokens: optionalNull(Schema.Number),
|
||||
total_tokens: optionalNull(Schema.Number),
|
||||
raw_prompt_token: optionalNull(Schema.Number),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
type RawUsage = typeof RawUsage.Type
|
||||
const OutputStep = Schema.Struct({
|
||||
type: Schema.String,
|
||||
id: Schema.optional(Schema.String),
|
||||
name: Schema.optional(Schema.String),
|
||||
arguments: Schema.optional(JsonObject),
|
||||
signature: Schema.optional(Schema.String),
|
||||
summary: Schema.optional(Schema.Array(Text)),
|
||||
content: Schema.optional(Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.optional(Schema.String) }))),
|
||||
})
|
||||
type OutputStep = typeof OutputStep.Type
|
||||
const Delta = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("arguments_delta"), arguments: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("thought_signature"), signature: Schema.String }),
|
||||
Schema.Struct({ type: Schema.Literal("thought_summary"), content: Text }),
|
||||
// Unknown output modalities must fail explicitly rather than disappearing from a successful response.
|
||||
Schema.Struct({ type: Schema.String }),
|
||||
])
|
||||
const Interaction = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
id: Schema.optional(Schema.String),
|
||||
status: Schema.String,
|
||||
usage: Schema.optional(RawUsage),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const Event = Schema.Union([
|
||||
Schema.Struct({ event_type: Schema.Literal("step.start"), index: Schema.Number, step: OutputStep }),
|
||||
Schema.Struct({ event_type: Schema.Literal("step.delta"), index: Schema.Number, delta: Delta }),
|
||||
Schema.Struct({ event_type: Schema.Literal("step.stop"), index: Schema.Number }),
|
||||
Schema.Struct({
|
||||
event_type: Schema.Literal("interaction.created"),
|
||||
interaction: Schema.Struct({ id: Schema.optional(Schema.String) }),
|
||||
}),
|
||||
Schema.Struct({
|
||||
event_type: Schema.Literal("interaction.status_update"),
|
||||
interaction_id: Schema.optional(Schema.String),
|
||||
status: Schema.String,
|
||||
}),
|
||||
Schema.Struct({ event_type: Schema.Literal("interaction.completed"), interaction: Interaction }),
|
||||
Schema.Struct({ event_type: Schema.Literal("error"), error: Schema.Unknown }),
|
||||
])
|
||||
type Event = typeof Event.Type
|
||||
|
||||
// =============================================================================
|
||||
// Parser State
|
||||
// =============================================================================
|
||||
interface ParserState {
|
||||
readonly route: string
|
||||
readonly metadataKey: string
|
||||
readonly lifecycle: Lifecycle.State
|
||||
readonly steps: Partial<Record<number, OutputStep>>
|
||||
readonly tools: ToolStream.State<number>
|
||||
readonly completed: boolean
|
||||
}
|
||||
type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
|
||||
// =============================================================================
|
||||
// Request Body Construction
|
||||
// =============================================================================
|
||||
const mediaContent = Effect.fn("GoogleInteractions.mediaContent")(function* (asset: Media.Asset) {
|
||||
if (
|
||||
asset.kind !== "image" &&
|
||||
asset.kind !== "audio" &&
|
||||
asset.kind !== "video" &&
|
||||
asset.mediaType !== "application/pdf" &&
|
||||
asset.mediaType !== "text/csv"
|
||||
)
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
`Google Interactions does not support ${asset.mediaType} document input`,
|
||||
)
|
||||
const type: (typeof MediaContent.Type)["type"] =
|
||||
asset.kind === "image" || asset.kind === "audio" || asset.kind === "video" ? asset.kind : "document"
|
||||
const uri = MediaInput.refID(asset, ProviderID.make("google"))
|
||||
if (uri !== undefined) return { type, uri, mime_type: asset.mediaType }
|
||||
const inline = yield* ProviderShared.requireInlineMedia("Google Interactions", asset)
|
||||
return { type, data: inline.base64, mime_type: inline.mime }
|
||||
})
|
||||
|
||||
const signature = (metadata: ProviderMetadata | undefined, key: string) => {
|
||||
const value = metadata?.[key]
|
||||
return ProviderShared.isRecord(value) && typeof value.interactionSignature === "string"
|
||||
? value.interactionSignature
|
||||
: undefined
|
||||
}
|
||||
|
||||
const lowerMessages = Effect.fn("GoogleInteractions.lowerMessages")(function* (request: LLMRequest) {
|
||||
const steps: InputStep[] = []
|
||||
const key = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
for (const message of request.messages) {
|
||||
if (message.role === "system") {
|
||||
const part = yield* ProviderShared.wrappedSystemUpdate("Google Interactions", message)
|
||||
steps.push({ type: "user_input", content: [{ type: "text", text: part.text }] })
|
||||
continue
|
||||
}
|
||||
const start = steps.length
|
||||
// Consecutive ordinary content remains one native message; tools and thoughts retain their chronology.
|
||||
const append = (content: typeof Content.Type) => {
|
||||
const type = message.role === "assistant" ? "model_output" : "user_input"
|
||||
const last = steps.at(-1)
|
||||
if (steps.length > start && last?.type === type)
|
||||
steps[steps.length - 1] = { type, content: [...last.content, content] }
|
||||
else steps.push({ type, content: [content] })
|
||||
}
|
||||
for (const part of message.content) {
|
||||
if (message.role === "tool") {
|
||||
if (part.type !== "tool-result")
|
||||
return yield* ProviderShared.unsupportedContent(ADAPTER, "tool", ["tool-result"])
|
||||
steps.push({
|
||||
type: "function_result",
|
||||
call_id: part.id,
|
||||
name: part.name,
|
||||
result: yield* lowerToolResult(part),
|
||||
is_error: part.result.type === "error" || undefined,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (part.type === "text") {
|
||||
append({ type: "text", text: part.text })
|
||||
continue
|
||||
}
|
||||
if (part.type === "media") {
|
||||
append(yield* mediaContent(part.media))
|
||||
continue
|
||||
}
|
||||
if (message.role === "assistant" && part.type === "reasoning") {
|
||||
steps.push({
|
||||
type: "thought",
|
||||
signature: signature(part.providerMetadata, key),
|
||||
summary: part.text ? [{ type: "text", text: part.text }] : undefined,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (message.role === "assistant" && part.type === "tool-call") {
|
||||
steps.push({
|
||||
type: "function_call",
|
||||
id: part.id,
|
||||
name: part.name,
|
||||
arguments: part.input,
|
||||
signature: signature(part.providerMetadata, key),
|
||||
})
|
||||
continue
|
||||
}
|
||||
return yield* ProviderShared.unsupportedContent(
|
||||
ADAPTER,
|
||||
message.role,
|
||||
message.role === "user" ? ["text", "media"] : ["text", "media", "reasoning", "tool-call"],
|
||||
)
|
||||
}
|
||||
}
|
||||
return steps
|
||||
})
|
||||
|
||||
const lowerToolResult = Effect.fn("GoogleInteractions.lowerToolResult")(function* (part: ToolResultPart) {
|
||||
if (part.result.type === "json" && ProviderShared.isRecord(part.result.value)) return part.result.value
|
||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||
|
||||
return yield* Effect.forEach(part.result.value, (item): Effect.Effect<typeof Content.Type, AIError> => {
|
||||
if (item.type === "text") return Effect.succeed({ type: "text", text: item.text })
|
||||
return mediaContent(ProviderShared.toolFileMedia(item).media)
|
||||
})
|
||||
})
|
||||
|
||||
const fromRequest = Effect.fn("GoogleInteractions.fromRequest")(function* (request: LLMRequest) {
|
||||
const options = yield* decodeOptions(request.providerOptions ?? {})
|
||||
const flattened = ProviderShared.flattenToolRequest(request)
|
||||
if (flattened.tools.some((tool) => tool.native !== undefined))
|
||||
return yield* ProviderShared.invalidRequest("Google Interactions hosted tools are not supported")
|
||||
if (
|
||||
request.generation?.topK !== undefined ||
|
||||
request.generation?.frequencyPenalty !== undefined ||
|
||||
request.generation?.presencePenalty !== undefined
|
||||
)
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
"Google Interactions does not support topK, frequencyPenalty, or presencePenalty",
|
||||
)
|
||||
const choice =
|
||||
request.toolChoice === undefined
|
||||
? undefined
|
||||
: yield* ProviderShared.matchToolChoice(ADAPTER, request.toolChoice, {
|
||||
auto: () => "auto" as const,
|
||||
none: () => "none" as const,
|
||||
required: () => "any" as const,
|
||||
tool: (name) => ({ allowed_tools: { mode: "any" as const, tools: [name] } }),
|
||||
})
|
||||
return {
|
||||
model: request.model.id,
|
||||
input: yield* lowerMessages(flattened.request),
|
||||
stream: true as const,
|
||||
// Full-history replay need not create retained provider resources. Continuation callers opt in to storage.
|
||||
store: options.store ?? false,
|
||||
previous_interaction_id: options.previousInteractionId,
|
||||
system_instruction: request.system.length ? ProviderShared.joinText(request.system) : undefined,
|
||||
service_tier: options.serviceTier,
|
||||
tools: flattened.tools.length
|
||||
? flattened.tools.map((tool) => ({
|
||||
type: "function" as const,
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
}))
|
||||
: undefined,
|
||||
generation_config: {
|
||||
max_output_tokens: request.generation?.maxTokens,
|
||||
temperature: request.generation?.temperature,
|
||||
top_p: request.generation?.topP,
|
||||
seed: request.generation?.seed,
|
||||
stop_sequences: request.generation?.stop,
|
||||
thinking_level: options.thinkingLevel,
|
||||
thinking_summaries: options.thinkingSummaries,
|
||||
tool_choice: choice,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Stream Parsing
|
||||
// =============================================================================
|
||||
const metadata = (state: ParserState, step: OutputStep): ProviderMetadata => ({
|
||||
[state.metadataKey]: { interactionSignature: step.signature },
|
||||
})
|
||||
const mapUsage = (usage: RawUsage | undefined, key: string) => {
|
||||
if (!usage) return undefined
|
||||
const input = usage.total_input_tokens ?? undefined
|
||||
const cached = usage.total_cached_tokens ?? undefined
|
||||
const reasoning = usage.total_thought_tokens ?? undefined
|
||||
const output =
|
||||
usage.total_output_tokens === undefined || usage.total_output_tokens === null
|
||||
? undefined
|
||||
: usage.total_output_tokens + (reasoning ?? 0)
|
||||
return new Usage({
|
||||
contextTokens: usage.raw_prompt_token ?? input,
|
||||
inputTokens: input,
|
||||
outputTokens: output,
|
||||
nonCachedInputTokens: ProviderShared.subtractTokens(input, cached),
|
||||
cacheReadInputTokens: cached,
|
||||
reasoningTokens: reasoning,
|
||||
totalTokens: usage.total_tokens ?? undefined,
|
||||
providerMetadata: { [key]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
const onStart = Effect.fn("GoogleInteractions.onStart")(function* (
|
||||
state: ParserState,
|
||||
index: number,
|
||||
step: OutputStep,
|
||||
) {
|
||||
const events: LLMEvent[] = []
|
||||
let lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
let tools = state.tools
|
||||
const id = String(index)
|
||||
if (step.type === "thought") {
|
||||
lifecycle = Lifecycle.reasoningStart(lifecycle, events, id, metadata(state, step))
|
||||
for (const part of step.summary ?? []) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, id, part.text)
|
||||
} else if (step.type === "model_output") {
|
||||
lifecycle = Lifecycle.textStart(lifecycle, events, id)
|
||||
for (const part of step.content ?? []) {
|
||||
if (part.type !== "text")
|
||||
return yield* ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
`Unsupported Interactions output: ${part.type}`,
|
||||
encodeJson(step),
|
||||
)
|
||||
if (part.text) lifecycle = Lifecycle.textDelta(lifecycle, events, id, part.text)
|
||||
}
|
||||
} else if (step.type === "function_call") {
|
||||
if (!step.id || !step.name)
|
||||
return yield* ProviderShared.eventError(ADAPTER, "Interactions function call lacks id or name", encodeJson(step))
|
||||
tools = ToolStream.start(tools, index, {
|
||||
id: step.id,
|
||||
name: step.name,
|
||||
providerMetadata: metadata(state, step),
|
||||
input: step.arguments && Object.keys(step.arguments).length ? encodeJson(step.arguments) : "",
|
||||
})
|
||||
events.push(LLMEvent.toolInputStart({ id: step.id, name: step.name, providerMetadata: metadata(state, step) }))
|
||||
} else
|
||||
return yield* ProviderShared.eventError(ADAPTER, `Unsupported Interactions step: ${step.type}`, encodeJson(step))
|
||||
return [{ ...state, lifecycle, tools, steps: { ...state.steps, [index]: step } }, events] satisfies StepResult
|
||||
})
|
||||
|
||||
const onDelta = Effect.fn("GoogleInteractions.onDelta")(function* (
|
||||
state: ParserState,
|
||||
index: number,
|
||||
delta: typeof Delta.Type,
|
||||
) {
|
||||
const step = state.steps[index]
|
||||
if (!step)
|
||||
return yield* ProviderShared.eventError(ADAPTER, "Interactions delta without step.start", encodeJson(delta))
|
||||
const events: LLMEvent[] = []
|
||||
if (delta.type === "text" && "text" in delta && step.type === "model_output")
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, String(index), delta.text) },
|
||||
events,
|
||||
] satisfies StepResult
|
||||
if (delta.type === "thought_summary" && "content" in delta && step.type === "thought")
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, String(index), delta.content.text) },
|
||||
events,
|
||||
] satisfies StepResult
|
||||
if (delta.type === "thought_signature" && "signature" in delta) {
|
||||
const next = { ...step, signature: delta.signature }
|
||||
const tool = state.tools[index]
|
||||
return [
|
||||
{
|
||||
...state,
|
||||
steps: { ...state.steps, [index]: next },
|
||||
tools: tool ? { ...state.tools, [index]: { ...tool, providerMetadata: metadata(state, next) } } : state.tools,
|
||||
},
|
||||
events,
|
||||
] satisfies StepResult
|
||||
}
|
||||
if (delta.type === "arguments_delta" && "arguments" in delta && step.type === "function_call") {
|
||||
const result = ToolStream.appendExisting(
|
||||
ADAPTER,
|
||||
state.tools,
|
||||
index,
|
||||
delta.arguments,
|
||||
"Interactions arguments without function call",
|
||||
)
|
||||
if (ToolStream.isError(result)) return yield* result
|
||||
return [{ ...state, tools: result.tools }, result.events] satisfies StepResult
|
||||
}
|
||||
return yield* ProviderShared.eventError(ADAPTER, `Unsupported Interactions delta: ${delta.type}`, encodeJson(delta))
|
||||
})
|
||||
|
||||
const onStop = Effect.fn("GoogleInteractions.onStop")(function* (state: ParserState, index: number) {
|
||||
const step = state.steps[index]
|
||||
if (!step) return yield* ProviderShared.eventError(ADAPTER, "Interactions step.stop without step.start")
|
||||
const events: LLMEvent[] = []
|
||||
if (step.type === "thought")
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.reasoningEnd(state.lifecycle, events, String(index), metadata(state, step)) },
|
||||
events,
|
||||
] satisfies StepResult
|
||||
if (step.type === "model_output")
|
||||
return [
|
||||
{ ...state, lifecycle: Lifecycle.textEnd(state.lifecycle, events, String(index)) },
|
||||
events,
|
||||
] satisfies StepResult
|
||||
const result = yield* ToolStream.finish(ADAPTER, state.tools, index)
|
||||
return [{ ...state, tools: result.tools }, result.events ?? []] satisfies StepResult
|
||||
})
|
||||
|
||||
const step = Effect.fn("GoogleInteractions.step")(function* (state: ParserState, event: Event) {
|
||||
switch (event.event_type) {
|
||||
case "step.start":
|
||||
return yield* onStart(state, event.index, event.step)
|
||||
case "step.delta":
|
||||
return yield* onDelta(state, event.index, event.delta)
|
||||
case "step.stop":
|
||||
return yield* onStop(state, event.index)
|
||||
case "interaction.created":
|
||||
case "interaction.status_update":
|
||||
return [state, []] satisfies StepResult
|
||||
case "error":
|
||||
return yield* new AIError({
|
||||
reason: classifyProviderFailure({
|
||||
message: providerErrorMessage(encodeJson(event)) ?? "Google Interactions stream error",
|
||||
data: event.error,
|
||||
rawBody: encodeJson(event),
|
||||
}),
|
||||
})
|
||||
case "interaction.completed": {
|
||||
const interaction = event.interaction
|
||||
if (interaction.status === "failed" || interaction.status === "cancelled")
|
||||
return yield* new AIError({
|
||||
reason: classifyProviderFailure({
|
||||
message: `Google Interactions ${interaction.status}`,
|
||||
data: interaction,
|
||||
rawBody: encodeJson(event),
|
||||
}),
|
||||
})
|
||||
if (!["completed", "requires_action", "incomplete"].includes(interaction.status))
|
||||
return yield* ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
`Unexpected terminal Interactions status: ${interaction.status}`,
|
||||
encodeJson(event),
|
||||
)
|
||||
const pending = yield* ToolStream.finishAll(ADAPTER, state.tools)
|
||||
const events = [...pending.events]
|
||||
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
|
||||
reason: {
|
||||
normalized:
|
||||
interaction.status === "requires_action"
|
||||
? "tool-calls"
|
||||
: interaction.status === "incomplete"
|
||||
? "length"
|
||||
: "stop",
|
||||
raw: interaction.status,
|
||||
},
|
||||
usage: mapUsage(interaction.usage, state.metadataKey),
|
||||
providerMetadata: { [state.metadataKey]: { interactionId: interaction.id } },
|
||||
})
|
||||
return [{ ...state, lifecycle, tools: pending.tools, completed: true }, events] satisfies StepResult
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Protocol And Route
|
||||
// =============================================================================
|
||||
export const protocol = Protocol.make({
|
||||
id: ADAPTER,
|
||||
sanitizer: "gemini",
|
||||
body: { schema: Body, from: fromRequest },
|
||||
stream: {
|
||||
event: Protocol.jsonEvent(Event),
|
||||
initial: (request): ParserState => ({
|
||||
route: `${request.model.provider}/${ADAPTER}`,
|
||||
metadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
|
||||
lifecycle: Lifecycle.initial(),
|
||||
steps: {},
|
||||
tools: ToolStream.empty<number>(),
|
||||
completed: false,
|
||||
}),
|
||||
step,
|
||||
terminal: (event) => event.event_type === "interaction.completed",
|
||||
onHalt: (state) =>
|
||||
state.completed
|
||||
? Effect.succeed([])
|
||||
: Effect.fail(
|
||||
ProviderShared.eventError(ADAPTER, "Google Interactions stream ended before interaction.completed"),
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
export const route = Route.make({
|
||||
id: ADAPTER,
|
||||
provider: "google",
|
||||
providerMetadataKey: "google",
|
||||
protocol,
|
||||
endpoint: Endpoint.path("/interactions", { baseURL: DEFAULT_BASE_URL }),
|
||||
auth: Auth.none,
|
||||
framing: Framing.sse,
|
||||
})
|
||||
|
||||
export * as GoogleInteractions from "./google-interactions.js"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as AnthropicMessages from "./anthropic-messages.js"
|
||||
export * as BedrockConverse from "./bedrock-converse.js"
|
||||
export * as CohereChat from "./cohere-chat.js"
|
||||
export * as Gemini from "./gemini.js"
|
||||
export * as GoogleInteractions from "./google-interactions.js"
|
||||
export * as MistralChat from "./mistral-chat.js"
|
||||
export * as OpenAIChat from "./openai-chat.js"
|
||||
export * as OpenAIImages from "./openai-images.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Protocol } from "../route/protocol.js"
|
||||
import { HttpTransport } from "../route/transport/index.js"
|
||||
import { LLMEvent, LLMRequest, Message, ToolResultPart } from "../schema/index.js"
|
||||
import { OpenResponses } from "./open-responses.js"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
@@ -44,13 +44,6 @@ const ImageItem = Schema.Struct({
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, ImageItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
|
||||
const MessageAnnotations = Schema.Struct({
|
||||
content: Schema.Array(Schema.Struct({ annotations: optionalArray(JsonObject) })),
|
||||
})
|
||||
@@ -62,12 +55,13 @@ interface ParserState extends OpenResponses.ParserState {
|
||||
const adapter = {
|
||||
id: ADAPTER,
|
||||
name: NAME,
|
||||
nativeTool: (native) => ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(native.meta),
|
||||
restoreHostedToolItem: (item: unknown) => (Schema.is(ImageItem)(item) ? item : undefined),
|
||||
} satisfies OpenResponses.ProviderAdapter
|
||||
|
||||
const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
const key = request.model.route.providerMetadataKey ?? String(request.model.provider)
|
||||
const projected = ProviderShared.flattenToolRequest(
|
||||
return yield* OpenResponses.fromRequestWithAdapter(
|
||||
LLMRequest.update(request, {
|
||||
messages: request.messages.map((message) =>
|
||||
Message.make({
|
||||
@@ -93,23 +87,8 @@ const fromRequest = Effect.fn("MetaResponses.fromRequest")(function* (request: L
|
||||
}),
|
||||
),
|
||||
}),
|
||||
adapter,
|
||||
)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(Body))({
|
||||
...(yield* OpenResponses.lowerConversation(projected.request, adapter)),
|
||||
...OpenResponses.lowerGeneration(request),
|
||||
tools:
|
||||
projected.tools.length === 0
|
||||
? undefined
|
||||
: yield* Effect.forEach(projected.tools, (tool) =>
|
||||
Effect.gen(function* () {
|
||||
if (tool.native === undefined) return yield* OpenResponses.lowerTool(NAME, tool)
|
||||
return yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(NativeTool))(tool.native.meta)
|
||||
}),
|
||||
),
|
||||
tool_choice:
|
||||
OpenResponses.allowedToolChoice(request) ??
|
||||
(request.toolChoice ? yield* OpenResponses.lowerToolChoice(NAME, request.toolChoice) : undefined),
|
||||
})
|
||||
})
|
||||
|
||||
const HOSTED_TOOLS = {
|
||||
@@ -135,7 +114,7 @@ const HOSTED_TOOLS = {
|
||||
"Meta returned an image without data",
|
||||
ProviderShared.encodeJson(raw),
|
||||
)
|
||||
const data = yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
const data = yield* Effect.fromResult(Base64.decode(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(
|
||||
ADAPTER,
|
||||
@@ -222,7 +201,7 @@ const step = Effect.fn("MetaResponses.step")(function* (state: ParserState, inpu
|
||||
|
||||
export const protocol = Protocol.make({
|
||||
id: ADAPTER,
|
||||
body: { schema: Body, from: fromRequest },
|
||||
body: { schema: OpenResponses.OpenResponsesBody, from: fromRequest },
|
||||
stream: {
|
||||
event: OpenResponses.protocol.stream.event,
|
||||
initial: (request): ParserState => ({ ...OpenResponses.initial(request, adapter), completedItems: new Set() }),
|
||||
@@ -231,6 +210,6 @@ export const protocol = Protocol.make({
|
||||
},
|
||||
})
|
||||
|
||||
export const httpTransport = HttpTransport.sseJson.with<Schema.Schema.Type<typeof Body>>()
|
||||
export const httpTransport = OpenResponses.httpTransport
|
||||
|
||||
export * as MetaResponses from "./meta-responses.js"
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import type { HttpContext } from "../schema/index.js"
|
||||
import {
|
||||
|
||||
@@ -168,10 +168,20 @@ export const ConfigurationUpdate = Schema.Struct({
|
||||
type: Schema.Literal("configuration_update"),
|
||||
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
|
||||
})
|
||||
type ConfigurationUpdate = Schema.Schema.Type<typeof ConfigurationUpdate>
|
||||
export type ConfigurationUpdate = Schema.Schema.Type<typeof ConfigurationUpdate>
|
||||
|
||||
export const HostedToolReplay = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
type: Schema.String,
|
||||
id: Schema.String,
|
||||
}),
|
||||
[JsonObject],
|
||||
)
|
||||
export type HostedToolReplayItem = Schema.Schema.Type<typeof HostedToolReplay>
|
||||
|
||||
export const InputItem = Schema.Union([
|
||||
CompactionItem,
|
||||
ConfigurationUpdate,
|
||||
Schema.Struct({ type: Schema.tag("message"), role: Schema.tag("system"), content: Schema.String }),
|
||||
Schema.Struct({ type: Schema.tag("message"), role: Schema.tag("developer"), content: Schema.String }),
|
||||
Schema.Struct({
|
||||
@@ -204,24 +214,9 @@ export const InputItem = Schema.Union([
|
||||
output: OpenResponsesFunctionCallOutput,
|
||||
}),
|
||||
HostedToolItem,
|
||||
HostedToolReplay,
|
||||
])
|
||||
type OpenResponsesInputItem = Schema.Schema.Type<typeof InputItem>
|
||||
export type HostedToolReplayItem = {
|
||||
readonly type: string
|
||||
readonly id: string
|
||||
readonly [key: string]: unknown
|
||||
}
|
||||
type LoweredInputItem =
|
||||
| OpenResponsesInputItem
|
||||
| HostedToolReplayItem
|
||||
| ConfigurationUpdate
|
||||
| {
|
||||
readonly type: "message"
|
||||
readonly id?: string
|
||||
readonly role: "assistant"
|
||||
readonly content: ReadonlyArray<{ readonly type: "output_text"; readonly text: string }>
|
||||
readonly phase?: MessagePhase | null
|
||||
}
|
||||
|
||||
// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold
|
||||
// multiple streamed summary parts into the same item before flushing.
|
||||
@@ -239,6 +234,14 @@ export const Tool = Schema.Struct({
|
||||
strict: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
|
||||
export const HostedTool = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
type: Schema.String,
|
||||
}),
|
||||
[JsonObject],
|
||||
)
|
||||
export type HostedTool = Schema.Schema.Type<typeof HostedTool>
|
||||
|
||||
export const ToolChoice = Schema.Union([
|
||||
Schema.Literals(["auto", "none", "required"]),
|
||||
Schema.Struct({ type: Schema.tag("function"), name: Schema.String }),
|
||||
@@ -257,7 +260,7 @@ export const coreFields = {
|
||||
model: Schema.String,
|
||||
input: Schema.Array(InputItem),
|
||||
instructions: Schema.optional(Schema.String),
|
||||
tools: optionalArray(Tool),
|
||||
tools: optionalArray(Schema.Union([Tool, HostedTool])),
|
||||
tool_choice: Schema.optional(ToolChoice),
|
||||
store: Schema.optional(Schema.Boolean),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
@@ -292,7 +295,7 @@ export const coreFields = {
|
||||
frequency_penalty: Schema.optional(Schema.Number),
|
||||
}
|
||||
|
||||
const OpenResponsesBody = Schema.Struct({
|
||||
export const OpenResponsesBody = Schema.Struct({
|
||||
...coreFields,
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
@@ -397,9 +400,7 @@ export const decodeChannelEvent = (frame: string) =>
|
||||
export interface ProviderAdapter {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly nativeTool?: (
|
||||
native: NonNullable<ToolDefinition["native"]>,
|
||||
) => Effect.Effect<{ readonly type: string }, AIError>
|
||||
readonly nativeTool?: (native: NonNullable<ToolDefinition["native"]>) => Effect.Effect<HostedTool, AIError>
|
||||
readonly lowerMedia?: (input: {
|
||||
readonly part: MediaPart
|
||||
readonly media: Media.Inline | undefined
|
||||
@@ -455,8 +456,10 @@ export const lowerTool = Effect.fn("OpenResponses.lowerTool")(function* (protoco
|
||||
})
|
||||
|
||||
export const lowerTools = (tools: ReadonlyArray<ToolDefinition>, adapter: ProviderAdapter) =>
|
||||
Effect.forEach(tools, (tool) =>
|
||||
tool.native !== undefined && adapter.nativeTool ? adapter.nativeTool(tool.native) : lowerTool(adapter.name, tool),
|
||||
Effect.forEach(
|
||||
tools,
|
||||
(tool): Effect.Effect<Schema.Schema.Type<typeof Tool> | HostedTool, AIError> =>
|
||||
tool.native !== undefined && adapter.nativeTool ? adapter.nativeTool(tool.native) : lowerTool(adapter.name, tool),
|
||||
)
|
||||
|
||||
export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
@@ -592,7 +595,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
request: LLMRequest,
|
||||
adapter: ProviderAdapter,
|
||||
) {
|
||||
const input: LoweredInputItem[] = []
|
||||
const input: OpenResponsesInputItem[] = []
|
||||
const providerMetadataKey = metadataKey(request.model)
|
||||
|
||||
for (const message of request.messages) {
|
||||
@@ -827,11 +830,7 @@ export const fromRequestWithAdapter = Effect.fn("OpenResponses.fromRequestWithAd
|
||||
}
|
||||
})
|
||||
|
||||
const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesBody))
|
||||
|
||||
export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
return yield* decodeBody(yield* fromRequestWithAdapter(request, BASE_ADAPTER))
|
||||
})
|
||||
export const fromRequest = (request: LLMRequest) => fromRequestWithAdapter(request, BASE_ADAPTER)
|
||||
|
||||
// =============================================================================
|
||||
// Stream Parsing
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Headers } from "effect/http"
|
||||
import { Route } from "../route/client.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
@@ -103,15 +104,8 @@ const OpenAIResponsesToolChoice = Schema.Union([
|
||||
Schema.Struct({ type: Schema.tag("image_generation") }),
|
||||
])
|
||||
|
||||
const OpenAIResponsesInputItem = Schema.Union([
|
||||
OpenResponses.InputItem,
|
||||
OpenAIResponsesHostedToolItem,
|
||||
OpenResponses.ConfigurationUpdate,
|
||||
])
|
||||
|
||||
const OpenAIResponsesCoreFields = {
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(OpenAIResponsesInputItem),
|
||||
tools: optionalArray(OpenAIResponsesTools),
|
||||
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
||||
context_management: Schema.optional(
|
||||
@@ -134,7 +128,7 @@ export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
|
||||
export const CompactionTrigger = Schema.Struct({ type: Schema.Literal("compaction_trigger") })
|
||||
const CheckpointBody = Schema.Struct({
|
||||
...OpenAIResponsesBody.fields,
|
||||
input: Schema.Array(Schema.Union([OpenAIResponsesInputItem, CompactionTrigger])),
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, CompactionTrigger])),
|
||||
})
|
||||
|
||||
const adapter = {
|
||||
@@ -202,15 +196,13 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>, tool
|
||||
: { type: "function" as const, name },
|
||||
})
|
||||
|
||||
const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIResponsesBody))
|
||||
|
||||
const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
const management = yield* ProviderShared.validateWith(
|
||||
Schema.decodeUnknownEffect(Schema.UndefinedOr(ContextManagement)),
|
||||
)(request.providerOptions?.contextManagement)
|
||||
const options = OpenResponsesOptions.resolve(request)
|
||||
const updates = resolveEffortUpdates(request, options.reasoningEffort)
|
||||
return yield* decodeBody({
|
||||
return {
|
||||
...(yield* OpenResponses.lowerConversation(updates.request, adapter)),
|
||||
...OpenResponses.lowerGeneration(request, { ...options, reasoningEffort: updates.effort }),
|
||||
context_management: management?.map((edit) => ({ type: edit.type, compact_threshold: edit.compactThreshold })),
|
||||
@@ -220,7 +212,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
|
||||
? undefined
|
||||
: (OpenResponses.allowedToolChoice(request) ??
|
||||
(request.toolChoice ? yield* lowerToolChoice(request.toolChoice, request.tools) : undefined)),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const checkpointBody = {
|
||||
@@ -249,7 +241,7 @@ const checkpointBody = {
|
||||
const hostedToolResult = Effect.fn("OpenAIResponses.hostedToolResult")(function* (item: ResponsesHostedTools.Item) {
|
||||
const isError = item.error !== undefined && item.error !== null
|
||||
if (item.type === "image_generation_call" && item.result) {
|
||||
yield* Effect.fromResult(Encoding.decodeBase64(item.result)).pipe(
|
||||
yield* Effect.fromResult(Base64.decode(item.result)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.eventError(ADAPTER, "OpenAI Responses returned invalid image base64", undefined, cause),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Media } from "../media.js"
|
||||
import {
|
||||
InvalidProviderOutputError,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AwsV4Signer } from "aws4fetch"
|
||||
import { Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, type AuthInput } from "../../route/auth.js"
|
||||
import { AIError, AuthenticationError } from "../../schema/index.js"
|
||||
import { ProviderShared } from "../shared.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding, Schema } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import type { MediaPart } from "../../schema/index.js"
|
||||
import { ProviderShared } from "../shared.js"
|
||||
|
||||
@@ -78,12 +79,12 @@ function documentName(filename: string | undefined, names: Set<string>) {
|
||||
|
||||
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
|
||||
const media = yield* ProviderShared.requireInlineMedia("Bedrock Converse", part.media)
|
||||
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
|
||||
const bytes = yield* Effect.fromResult(Base64.decode(media.base64)).pipe(
|
||||
Effect.mapError((cause) =>
|
||||
ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause),
|
||||
),
|
||||
)
|
||||
return Encoding.encodeBase64(bytes)
|
||||
return Base64.encode(bytes)
|
||||
})
|
||||
|
||||
// Route by MIME. Known image/document formats lower into a typed block; anything
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../../generation.js"
|
||||
import type { Media } from "../../media.js"
|
||||
import { MediaProtocol } from "../../route/media-protocol.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Encoding } from "effect"
|
||||
import { Effect } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Media } from "../../media.js"
|
||||
import type { MediaProtocol } from "../../route/media-protocol.js"
|
||||
import { mergeJsonRecords, type AIError, type ProviderID } from "../../schema/index.js"
|
||||
@@ -10,7 +11,7 @@ export const inlineBytes = (route: string, asset: Media.Asset): Effect.Effect<Ui
|
||||
if (asset.source.type === "bytes") return Effect.succeed(asset.source.data)
|
||||
const inline = asset.inline()
|
||||
if (!inline) return Effect.fail(ProviderShared.inlineRequired(route, asset))
|
||||
return Effect.fromResult(Encoding.decodeBase64(inline.base64)).pipe(
|
||||
return Effect.fromResult(Base64.decode(inline.base64)).pipe(
|
||||
Effect.mapError((cause) => ProviderShared.invalidRequest(`${route} media contains invalid base64 data`, cause)),
|
||||
)
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export const decodedAsset = (
|
||||
mediaType: string | undefined,
|
||||
options?: Media.AssetOptions,
|
||||
) =>
|
||||
Effect.fromResult(Encoding.decodeBase64(data)).pipe(
|
||||
Effect.fromResult(Base64.decode(data)).pipe(
|
||||
Effect.mapError((cause) => invalid(`${label} contains invalid base64 data`, cause)),
|
||||
Effect.map((bytes) => Media.bytes(bytes, mediaType, options)),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -31,19 +31,12 @@ const XAIResponsesHostedToolItem = Schema.Union([
|
||||
),
|
||||
])
|
||||
|
||||
const XAIResponsesBody = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, XAIResponsesHostedToolItem])),
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
|
||||
const adapter = {
|
||||
id: ADAPTER,
|
||||
name: NAME,
|
||||
restoreHostedToolItem: (item: unknown) => (Schema.is(XAIResponsesHostedToolItem)(item) ? item : undefined),
|
||||
} satisfies OpenResponses.ProviderAdapter
|
||||
|
||||
const decodeBody = ProviderShared.validateWith(Schema.decodeUnknownEffect(XAIResponsesBody))
|
||||
const fromRequest = Effect.fn("XAIResponses.fromRequest")(function* (request: LLMRequest) {
|
||||
if (request.providerOptions?.contextManagement !== undefined)
|
||||
return yield* ProviderShared.unsupportedOperation({
|
||||
@@ -53,7 +46,7 @@ const fromRequest = Effect.fn("XAIResponses.fromRequest")(function* (request: LL
|
||||
message:
|
||||
"xAI requires explicit compaction through LLMClient.compact; automatic context management is not supported",
|
||||
})
|
||||
return yield* decodeBody(yield* OpenResponses.fromRequestWithAdapter(request, adapter))
|
||||
return yield* OpenResponses.fromRequestWithAdapter(request, adapter)
|
||||
})
|
||||
|
||||
const HOSTED_TOOLS = {
|
||||
@@ -83,7 +76,7 @@ const step = (state: OpenResponses.ParserState, input: OpenResponses.Event) => {
|
||||
export const protocol = Protocol.make({
|
||||
id: ADAPTER,
|
||||
body: {
|
||||
schema: XAIResponsesBody,
|
||||
schema: OpenResponses.OpenResponsesBody,
|
||||
from: fromRequest,
|
||||
},
|
||||
stream: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import type { Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schema } from "effect"
|
||||
import type { HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientResponse } from "effect/http"
|
||||
import { ImageModel, ImageResponse, type ImageRequestFor } from "../image.js"
|
||||
import { MediaProtocol } from "../route/media-protocol.js"
|
||||
import { MediaRoute } from "../route/media.js"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MediaRoute } from "../route/media.js"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { ProviderID, type ModelID } from "../schema/index.js"
|
||||
import { Gemini } from "../protocols/gemini.js"
|
||||
import { GoogleInteractions } from "../protocols/google-interactions.js"
|
||||
import { GoogleImages } from "../protocols/google-images.js"
|
||||
import { GoogleSpeech } from "../protocols/google-speech.js"
|
||||
import { GoogleTranscription } from "../protocols/google-transcription.js"
|
||||
@@ -16,15 +17,16 @@ export type { GoogleTranscriptionOptions } from "../protocols/google-transcripti
|
||||
export type { GoogleVideoOptions } from "../protocols/google-video.js"
|
||||
export type GeminiOptionsInput = Gemini.OptionsInput
|
||||
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput
|
||||
export type GoogleInteractionsOptionsInput = GoogleInteractions.OptionsInput
|
||||
|
||||
export const id = ProviderID.make("google")
|
||||
|
||||
export const routes = [Gemini.route]
|
||||
export const routes = [Gemini.route, GoogleInteractions.route]
|
||||
|
||||
export type Config = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: Gemini.ProviderOptionsInput
|
||||
readonly providerOptions?: Gemini.ProviderOptionsInput & GoogleInteractions.ProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
@@ -45,12 +47,19 @@ const configuredRoute = (input: Config) => {
|
||||
return Gemini.route.with({ ...rest, endpoint: { baseURL }, auth: auth(input) })
|
||||
}
|
||||
|
||||
const interactionsRoute = (input: Config) => {
|
||||
const { apiKey: _, auth: _auth, baseURL, ...rest } = input
|
||||
return GoogleInteractions.route.with({ ...rest, endpoint: { baseURL }, auth: auth(input) })
|
||||
}
|
||||
|
||||
export const configure = (input: Config = {}) => {
|
||||
const route = configuredRoute(input)
|
||||
const media = MediaRoute.deployment(input, auth(input))
|
||||
return {
|
||||
id,
|
||||
model: (modelID: string | ModelID) => route.model<Gemini.ProviderOptionsInput>({ id: modelID }),
|
||||
interactions: (modelID: string | ModelID) =>
|
||||
interactionsRoute(input).model<GoogleInteractions.ProviderOptionsInput>({ id: modelID }),
|
||||
image: (modelID: string | ModelID) => GoogleImages.model({ ...media, id: modelID }),
|
||||
video: (modelID: string | ModelID) => GoogleVideo.model({ ...media, id: modelID }),
|
||||
speech: (modelID: string | ModelID) => GoogleSpeech.model({ ...media, id: modelID }),
|
||||
@@ -73,6 +82,7 @@ export const model: ProviderPackage.Definition<Settings, Gemini.ProviderOptionsI
|
||||
}).model(modelID)
|
||||
|
||||
export const image = provider.image
|
||||
export const interactions = provider.interactions
|
||||
export const video = provider.video
|
||||
export const speech = provider.speech
|
||||
export const transcription = provider.transcription
|
||||
@@ -0,0 +1,21 @@
|
||||
import { configure } from "../google.js"
|
||||
import type { ProviderPackage } from "../../provider-package.js"
|
||||
import type { GoogleInteractions } from "../../protocols/google-interactions.js"
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
GoogleInteractions.ProviderOptionsInput & {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
}
|
||||
|
||||
export const model: ProviderPackage.Definition<Settings, GoogleInteractions.ProviderOptionsInput>["model"] = (
|
||||
modelID,
|
||||
{ apiKey, baseURL, body, headers, ...providerOptions },
|
||||
) =>
|
||||
configure({
|
||||
apiKey,
|
||||
baseURL,
|
||||
headers: headers === undefined ? undefined : { ...headers },
|
||||
http: body === undefined ? undefined : { body: { ...body } },
|
||||
providerOptions,
|
||||
}).interactions(modelID)
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
EvaluationAnswer,
|
||||
EvaluationInput,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Config, Effect, Option, Redacted } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { AuthenticationError, AIError, type HttpOptions } from "../schema/index.js"
|
||||
|
||||
export class MissingCredentialError extends Error {
|
||||
@@ -95,7 +95,7 @@ export const optional = (secret: Secret | undefined, source = "optional value")
|
||||
export const config = (name: string) =>
|
||||
credential(
|
||||
Effect.gen(function* () {
|
||||
const secret = yield* Config.option(Config.redacted(name))
|
||||
const secret = yield* Config.option(Config.Redacted(name))
|
||||
if (Option.isSome(secret) && Redacted.value(secret.value) !== "") return secret.value
|
||||
return yield* Effect.fail(new MissingCredentialError(name, `${name} is not set`))
|
||||
}),
|
||||
|
||||
@@ -358,7 +358,6 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
input: MakeTransportInput<Body, Prepared, Frame, Event, State>,
|
||||
): Route<Body, Prepared> {
|
||||
const protocol = input.protocol
|
||||
const encodeBody = Schema.encodeSync(Schema.fromJsonString(protocol.body.schema))
|
||||
const decodeEventEffect = Schema.decodeUnknownEffect(protocol.stream.event)
|
||||
const decodeEvent = (route: string) => (frame: Frame) =>
|
||||
decodeEventEffect(frame).pipe(
|
||||
@@ -417,7 +416,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
|
||||
request,
|
||||
endpoint: routeInput.endpoint,
|
||||
auth: routeInput.auth ?? Auth.none,
|
||||
encodeBody,
|
||||
encodeBody: ProviderShared.encodeJson,
|
||||
middleware: options?.http,
|
||||
webSocket: options?.webSocket,
|
||||
}),
|
||||
@@ -576,9 +575,7 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest, options
|
||||
const resolved = prepareRequest(request)
|
||||
const route = resolved.model.route
|
||||
|
||||
const body = yield* route.body
|
||||
.from(resolved)
|
||||
.pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))))
|
||||
const body = yield* route.body.from(resolved)
|
||||
const prepared = yield* route.prepareTransport(body, resolved, options)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Context, type Effect } from "effect"
|
||||
import type { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import type { HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import type { AIError } from "../schema/errors.js"
|
||||
|
||||
// The service tag lives in its own leaf module so `Media.Asset` (imported by the schema layer) can require the
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
HttpClientError,
|
||||
HttpClientRequest,
|
||||
HttpClientResponse,
|
||||
} from "effect/unstable/http"
|
||||
} from "effect/http"
|
||||
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js"
|
||||
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js"
|
||||
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Effect, Stream } from "effect"
|
||||
import { makeParser, type Event } from "effect/unstable/encoding/Sse"
|
||||
import { makeParser, type Event } from "effect/encoding/Sse"
|
||||
import { AIError, InvalidProviderOutputError } from "../schema/index.js"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Clock, Duration, Effect, Schema, type Stream } from "effect"
|
||||
import { HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClientResponse } from "effect/http"
|
||||
import type { Snapshot, Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import type { AuthInput } from "./auth.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Duration, Effect, Schedule, Schema, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest, type HttpClientResponse } from "effect/http"
|
||||
import { Auth, type AuthInput } from "./auth.js"
|
||||
import { Endpoint } from "./endpoint.js"
|
||||
import { RequestExecutorService, type Interface } from "./executor-service.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Clock, Duration, Effect, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Auth } from "../auth.js"
|
||||
import { render as renderEndpoint } from "../endpoint.js"
|
||||
import { Framing } from "../framing.js"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Effect, Scope, Stream } from "effect"
|
||||
import type { Headers } from "effect/unstable/http"
|
||||
import type { Headers } from "effect/http"
|
||||
import type { AIError, HttpContext } from "../../schema/index.js"
|
||||
|
||||
export interface WebSocketChannelExecutor {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cause, Effect, Queue, Stream } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Headers } from "effect/http"
|
||||
import { Socket } from "effect/socket"
|
||||
import {
|
||||
AIError,
|
||||
AIErrorReason,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import { withStatics } from "@opencode/schema/schema"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import {
|
||||
CacheHint,
|
||||
@@ -28,7 +29,7 @@ export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
|
||||
|
||||
const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
|
||||
|
||||
export const SystemPart = Object.assign(systemPartSchema, {
|
||||
export const SystemPart = withStatics(systemPartSchema, {
|
||||
make: makeSystemPart,
|
||||
content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
|
||||
if (input === undefined) return []
|
||||
@@ -76,7 +77,7 @@ const toolResultValueSchema = Schema.Union([
|
||||
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
|
||||
const isToolResultValue = Schema.is(toolResultValueSchema)
|
||||
|
||||
export const ToolResultValue = Object.assign(toolResultValueSchema, {
|
||||
export const ToolResultValue = withStatics(toolResultValueSchema, {
|
||||
is: isToolResultValue,
|
||||
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
|
||||
if (isToolResultValue(value)) return value
|
||||
@@ -90,7 +91,7 @@ export interface ToolOutput {
|
||||
readonly content: ReadonlyArray<Tool.Content>
|
||||
}
|
||||
|
||||
export const ToolOutput = Object.assign(
|
||||
export const ToolOutput = withStatics(
|
||||
Schema.Struct({
|
||||
structured: Schema.Unknown,
|
||||
content: Schema.Array(Tool.Content),
|
||||
@@ -127,7 +128,7 @@ const toolResultText = (value: unknown) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const ToolCallPart = Object.assign(
|
||||
export const ToolCallPart = withStatics(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-call"),
|
||||
id: Schema.String,
|
||||
@@ -145,7 +146,7 @@ export const ToolCallPart = Object.assign(
|
||||
)
|
||||
export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
|
||||
|
||||
export const ToolResultPart = Object.assign(
|
||||
export const ToolResultPart = withStatics(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("tool-result"),
|
||||
id: Schema.String,
|
||||
@@ -209,7 +210,7 @@ const compactionPartSchema = Schema.Struct({
|
||||
)
|
||||
.annotate({ identifier: "LLM.Content.Compaction" })
|
||||
export type CompactionPart = typeof compactionPartSchema.Type
|
||||
export const CompactionPart = Object.assign(compactionPartSchema, {
|
||||
export const CompactionPart = withStatics(compactionPartSchema, {
|
||||
make: (input: Omit<CompactionPart, "type" | "encrypted" | "text"> & CompactionContent): CompactionPart =>
|
||||
Schema.decodeUnknownSync(compactionPartSchema)({ type: "compaction", ...input }),
|
||||
})
|
||||
@@ -326,7 +327,7 @@ export type ToolNamespaceEntryInput = ToolNamespaceInput & { readonly type: "nam
|
||||
|
||||
export const ToolNamespace: Schema.Codec<ToolNamespace> & {
|
||||
readonly make: (input: ToolNamespace | ToolNamespaceInput) => ToolNamespace
|
||||
} = Object.assign(
|
||||
} = withStatics(
|
||||
Schema.Struct({
|
||||
type: Schema.Literal("namespace"),
|
||||
name: Schema.String,
|
||||
@@ -346,7 +347,7 @@ export type ToolEntry = ToolDefinition | ToolNamespace
|
||||
export type ToolEntryInput = ToolDefinition.Input | ToolNamespaceEntryInput
|
||||
export const ToolEntry: Schema.Codec<ToolEntry> & {
|
||||
readonly make: (input: ToolEntryInput) => ToolEntry
|
||||
} = Object.assign(
|
||||
} = withStatics(
|
||||
Schema.Union([ToolDefinition, ToolNamespace]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
Schema.annotate({ identifier: "LLM.ToolEntry" }),
|
||||
|
||||
+32
-3
@@ -1,4 +1,4 @@
|
||||
import { Effect, JsonSchema, Schema } from "effect"
|
||||
import { Effect, JsonSchema, Schema, SchemaAST } from "effect"
|
||||
import { Tool } from "@opencode/schema/tool"
|
||||
import type {
|
||||
ToolCallPart,
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ToolOutput as ToolOutputType,
|
||||
} from "./schema/index.js"
|
||||
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema/index.js"
|
||||
import { isRecord } from "./utils/record.js"
|
||||
|
||||
/**
|
||||
* Schema constraint for tool parameters / success values: no decoding or
|
||||
@@ -200,7 +201,7 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
|
||||
_definition: new ToolDefinition({
|
||||
name: "",
|
||||
description: config.description,
|
||||
inputSchema: toJsonSchema(config.parameters),
|
||||
inputSchema: emptyInputJsonSchema(config.parameters) ?? toJsonSchema(config.parameters),
|
||||
outputSchema: toJsonSchema(config.success),
|
||||
}),
|
||||
}
|
||||
@@ -230,8 +231,36 @@ export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass>
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Describes a no-argument Effect tool input as an empty object, or returns
|
||||
* `undefined` when the input takes arguments.
|
||||
*
|
||||
* Effect emits `{ not: { type: "null" } }` for `Struct({})`, but providers
|
||||
* require tool parameters to describe an object. Emptiness is decided by the
|
||||
* encoded side, which is what the model sends, so a transformation from an
|
||||
* empty struct qualifies while one into an empty struct does not. The schema is
|
||||
* inlined rather than a root `$ref`, and the title, description, and other
|
||||
* metadata Effect emits are retained. Checked encoded structs are left as
|
||||
* Effect emits them, because their constraints or JSON Schema overrides could
|
||||
* contradict the closed object shape.
|
||||
*/
|
||||
export const emptyInputJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema | undefined => {
|
||||
const encoded = SchemaAST.toEncoded(schema.ast)
|
||||
if (
|
||||
!SchemaAST.isObjects(encoded) ||
|
||||
encoded.propertySignatures.length > 0 ||
|
||||
encoded.indexSignatures.length > 0 ||
|
||||
encoded.checks !== undefined
|
||||
)
|
||||
return undefined
|
||||
const { not, ...metadata } = Schema.toJsonSchemaDocument(schema, { referencePolicy: () => undefined }).schema
|
||||
// Rewrite only the shape Effect is known to emit for an empty struct.
|
||||
if (!isRecord(not) || not.type !== "null" || Object.keys(not).length !== 1) return undefined
|
||||
return { ...metadata, type: "object", properties: {}, additionalProperties: false }
|
||||
}
|
||||
|
||||
const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
|
||||
const document = Schema.toJsonSchemaDocument(schema)
|
||||
const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" })
|
||||
if (Object.keys(document.definitions).length === 0) return document.schema
|
||||
return { ...document.schema, $defs: document.definitions }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type LanguageModel = {
|
||||
declare const auth: Auth.Definition
|
||||
declare const optionalAuthModel: LanguageModelFactory<BaseOptions, "optional", LanguageModel>
|
||||
declare const requiredAuthModel: LanguageModelFactory<BaseOptions, "required", LanguageModel>
|
||||
const configApiKey = Config.redacted("OPENAI_API_KEY")
|
||||
const configApiKey = Config.Redacted("OPENAI_API_KEY")
|
||||
|
||||
OpenAIChat.route.model({ id: "gpt-4.1-mini" })
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { LLM } from "../src/index.js"
|
||||
import { Auth } from "../src/route/auth.js"
|
||||
import * as OpenAIChat from "../src/protocols/openai-chat.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { FetchHttpClient } from "effect/http"
|
||||
import { LLM, LLMRequest, Message } from "../src/index.js"
|
||||
import { LLMClient } from "../src/route/client.js"
|
||||
import { OpenAI } from "../src/providers.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Ref, Schema } from "effect"
|
||||
import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import {
|
||||
LLM,
|
||||
LLMRequest,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Evaluation, EvaluationClient } from "../src/experimental.js"
|
||||
import { OpenCodeZen, OpenRouter, TypeSafeAI, VercelAIGateway } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Deferred, Effect, Fiber, Layer, Ref, Stream } from "effect"
|
||||
import { Headers, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { Headers, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import { LLM, AIError, HttpContext, InvalidProviderOutputError, TransportError, isRetryable } from "../src/index.js"
|
||||
import {
|
||||
LLMClient,
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:google-interactions",
|
||||
"provider:google",
|
||||
"protocol:google-interactions"
|
||||
],
|
||||
"name": "google-interactions/continues-tool-results-with-previous-interaction-id",
|
||||
"recordedAt": "2026-10-03T18:10:17.900Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"user_input\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"stream\":true,\"store\":true,\"system_instruction\":\"Use get_weather for weather questions. Answer concisely after receiving the result.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"generation_config\":{\"max_output_tokens\":2048,\"thinking_level\":\"low\",\"tool_choice\":\"any\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"id\":\"v1_ChdCMFhCYXRqeURhVDdxdHNQNWJLTXNBURIXQjBYQmF0anlEYVQ3cXRzUDViS01zQVE\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"interaction_id\":\"v1_ChdCMFhCYXRqeURhVDdxdHNQNWJLTXNBURIXQjBYQmF0anlEYVQ3cXRzUDViS01zQVE\",\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EoACCv0BAWkUfRMR9cx79IeL9V4HvDAHann4OEdOjz7FebGRwlCFGJHqLGG1lG1d+9xzzyPXN0d60gjMwbeix1bE4cCckDeMV0vAkOfR/epPhvmcfI+vWK1nURu+3UcgUBQ9on4VlZFcnOg7SoihuvxuQB4qqT65EAyqi8ybd3P8NB+V/FNcTBvMmJuNNCNxI5KgwsHjdT+xLdssTieljIG3H99rSz3/Nv90dYEJIr0WsQ1a3ZevpD6gPts/8Id/Z92zQ3aq4LAr47rdItCVF5h63Lchi9QFrav5LGf6lNVAOnlHIyXBrlAnVbdLsAdzlemlHY+oIWNMncL/NKayCt+qvQ==\",\"type\":\"thought_signature\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":0,\"event_type\":\"step.stop\"}\n\nevent: step.start\ndata: {\"index\":1,\"step\":{\"id\":\"call_326996\",\"signature\":\"AWkUfRMR9cx79IeL9V4HvDAHann4OEdOjz7FebGRwlCFGJHqLGG1lG1d+9xzzyPXN0d60gjMwbeix1bE4cCckDeMV0vAkOfR/epPhvmcfI+vWK1nURu+3UcgUBQ9on4VlZFcnOg7SoihuvxuQB4qqT65EAyqi8ybd3P8NB+V/FNcTBvMmJuNNCNxI5KgwsHjdT+xLdssTieljIG3H99rSz3/Nv90dYEJIr0WsQ1a3ZevpD6gPts/8Id/Z92zQ3aq4LAr47rdItCVF5h63Lchi9QFrav5LGf6lNVAOnlHIyXBrlAnVbdLsAdzlemlHY+oIWNMncL/NKayCt+qvQ==\",\"type\":\"function_call\",\"name\":\"get_weather\",\"arguments\":{}},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"type\":\"arguments_delta\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":1,\"event_type\":\"step.stop\"}\n\nevent: interaction.completed\ndata: {\"interaction\":{\"id\":\"v1_ChdCMFhCYXRqeURhVDdxdHNQNWJLTXNBURIXQjBYQmF0anlEYVQ3cXRzUDViS01zQVE\",\"status\":\"requires_action\",\"usage\":{\"total_tokens\":114,\"total_input_tokens\":68,\"input_tokens_by_modality\":[{\"modality\":\"text\",\"tokens\":68}],\"total_cached_tokens\":0,\"total_output_tokens\":16,\"total_tool_use_tokens\":0,\"total_thought_tokens\":30,\"raw_prompt_token\":107,\"model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":107}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":22}],\"thoughts_tokens_details\":[{\"modality\":\"text\",\"tokens\":30}]}],\"non_grounding_model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":107}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":22}],\"thoughts_tokens_details\":[{\"modality\":\"text\",\"tokens\":30}]}]},\"created\":\"2026-10-03T18:10:16Z\",\"updated\":\"2026-10-03T18:10:16Z\",\"service_tier\":\"standard\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.completed\"}\n\nevent: done\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"function_result\",\"call_id\":\"call_326996\",\"name\":\"get_weather\",\"result\":{\"temperature\":22,\"condition\":\"sunny\"}}],\"stream\":true,\"store\":false,\"previous_interaction_id\":\"v1_ChdCMFhCYXRqeURhVDdxdHNQNWJLTXNBURIXQjBYQmF0anlEYVQ3cXRzUDViS01zQVE\",\"system_instruction\":\"Use get_weather for weather questions. Answer concisely after receiving the result.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"generation_config\":{\"max_output_tokens\":2048,\"thinking_level\":\"low\",\"tool_choice\":\"none\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EmkKZwFpFH0TchP60hnCGy9QgdyJKikXfeP9DBx/BVTE92JiMUcdYnAQ7vGOt2zL8v0GQMbCshndVE0+WeHt0QXyzeeMeNFH/UQ5bxo3Vs+T3H1e6Fq3LFIjDuvYvMYMRLk44oULO1EhoCg=\",\"type\":\"thought_signature\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":0,\"event_type\":\"step.stop\"}\n\nevent: step.start\ndata: {\"index\":1,\"step\":{\"type\":\"model_output\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"text\":\"The weather in Paris\",\"type\":\"text\"},\"event_type\":\"step.delta\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"text\":\" is sunny with a temperature of 22°C.\",\"type\":\"text\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":1,\"event_type\":\"step.stop\"}\n\nevent: interaction.completed\ndata: {\"interaction\":{\"status\":\"completed\",\"usage\":{\"total_tokens\":115,\"total_input_tokens\":99,\"input_tokens_by_modality\":[{\"modality\":\"text\",\"tokens\":99}],\"total_cached_tokens\":0,\"total_output_tokens\":16,\"total_tool_use_tokens\":0,\"total_thought_tokens\":0,\"raw_prompt_token\":134,\"model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":134}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":20}]}],\"non_grounding_model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":134}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":20}]}]},\"created\":\"2026-10-03T18:10:17Z\",\"updated\":\"2026-10-03T18:10:17Z\",\"service_tier\":\"standard\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.completed\"}\n\nevent: done\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions/v1_ChdCMFhCYXRqeURhVDdxdHNQNWJLTXNBURIXQjBYQmF0anlEYVQ3cXRzUDViS01zQVE",
|
||||
"headers": {},
|
||||
"body": ""
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:google-interactions",
|
||||
"provider:google",
|
||||
"protocol:google-interactions"
|
||||
],
|
||||
"name": "google-interactions/replays-native-tool-results-and-signatures-statelessly",
|
||||
"recordedAt": "2026-10-03T18:10:15.040Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"user_input\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]}],\"stream\":true,\"store\":false,\"system_instruction\":\"Use get_weather for weather questions. Answer concisely after receiving the result.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"generation_config\":{\"max_output_tokens\":2048,\"thinking_level\":\"low\",\"thinking_summaries\":\"auto\",\"tool_choice\":{\"allowed_tools\":{\"mode\":\"any\",\"tools\":[\"get_weather\"]}}}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EmkKZwFpFH0TU3VE2ypZTy8W0/W4ikeJNHd8J/UqUDCBIILV/U6sCJUEUat3oneLPVOSvuy+mLi2ByVh2ZlkcxEiY4DsOcsHKSF6q1WBk9YuCqE+W8BMNjZoRwBQeyDeq6cJpXDVkFtBYbg=\",\"type\":\"thought_signature\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":0,\"event_type\":\"step.stop\"}\n\nevent: step.start\ndata: {\"index\":1,\"step\":{\"id\":\"call_1850980\",\"signature\":\"AWkUfRNTdUTbKllPLxbT9biKR4k0d3wn9SpQMIEggtX9TqwIlQRRq3eid4s9U5K+7L6YuLYHJWHZmWRzESJjgOw5ywcpIXqrVYGT1i4KoT5bwEw2NmhHAFB7IN6rpwmlcNWQW0FhuA==\",\"type\":\"function_call\",\"name\":\"get_weather\",\"arguments\":{}},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\",\"type\":\"arguments_delta\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":1,\"event_type\":\"step.stop\"}\n\nevent: interaction.completed\ndata: {\"interaction\":{\"status\":\"requires_action\",\"usage\":{\"total_tokens\":84,\"total_input_tokens\":68,\"input_tokens_by_modality\":[{\"modality\":\"text\",\"tokens\":68}],\"total_cached_tokens\":0,\"total_output_tokens\":16,\"total_tool_use_tokens\":0,\"total_thought_tokens\":0,\"raw_prompt_token\":107,\"model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":107}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":22}]}],\"non_grounding_model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":107}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":22}]}]},\"created\":\"2026-10-03T18:10:14Z\",\"updated\":\"2026-10-03T18:10:14Z\",\"service_tier\":\"standard\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.completed\"}\n\nevent: done\ndata: [DONE]\n\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"user_input\",\"content\":[{\"type\":\"text\",\"text\":\"What is the weather in Paris?\"}]},{\"type\":\"thought\",\"signature\":\"EmkKZwFpFH0TU3VE2ypZTy8W0/W4ikeJNHd8J/UqUDCBIILV/U6sCJUEUat3oneLPVOSvuy+mLi2ByVh2ZlkcxEiY4DsOcsHKSF6q1WBk9YuCqE+W8BMNjZoRwBQeyDeq6cJpXDVkFtBYbg=\"},{\"type\":\"function_call\",\"id\":\"call_1850980\",\"name\":\"get_weather\",\"arguments\":{\"city\":\"Paris\"},\"signature\":\"AWkUfRNTdUTbKllPLxbT9biKR4k0d3wn9SpQMIEggtX9TqwIlQRRq3eid4s9U5K+7L6YuLYHJWHZmWRzESJjgOw5ywcpIXqrVYGT1i4KoT5bwEw2NmhHAFB7IN6rpwmlcNWQW0FhuA==\"},{\"type\":\"function_result\",\"call_id\":\"call_1850980\",\"name\":\"get_weather\",\"result\":{\"temperature\":22,\"condition\":\"sunny\"}}],\"stream\":true,\"store\":false,\"system_instruction\":\"Use get_weather for weather questions. Answer concisely after receiving the result.\",\"tools\":[{\"type\":\"function\",\"name\":\"get_weather\",\"description\":\"Get current weather for a city.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"],\"additionalProperties\":false}}],\"generation_config\":{\"max_output_tokens\":2048,\"thinking_level\":\"low\",\"thinking_summaries\":\"auto\",\"tool_choice\":\"none\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EmkKZwFpFH0TRPN/gVeX86bn3+mP2Y1jv3jX464uSnSB+RuBqyig1Xfg52RBzk5GzUt98niutpbttYfMLkXURehjrP1AJUJzQbGLMqmqKD5iEp3bgLq46B3Ov+RcO539Fcz6+qCpPx+VaMQ=\",\"type\":\"thought_signature\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":0,\"event_type\":\"step.stop\"}\n\nevent: step.start\ndata: {\"index\":1,\"step\":{\"type\":\"model_output\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"text\":\"The weather in Paris\",\"type\":\"text\"},\"event_type\":\"step.delta\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"text\":\" is sunny with a temperature of 22°C.\",\"type\":\"text\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":1,\"event_type\":\"step.stop\"}\n\nevent: interaction.completed\ndata: {\"interaction\":{\"status\":\"completed\",\"usage\":{\"total_tokens\":108,\"total_input_tokens\":92,\"input_tokens_by_modality\":[{\"modality\":\"text\",\"tokens\":92}],\"total_cached_tokens\":0,\"total_output_tokens\":16,\"total_tool_use_tokens\":0,\"total_thought_tokens\":0,\"raw_prompt_token\":104,\"model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":104}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":20}]}],\"non_grounding_model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":104}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":20}]}]},\"created\":\"2026-10-03T18:10:15Z\",\"updated\":\"2026-10-03T18:10:15Z\",\"service_tier\":\"standard\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.completed\"}\n\nevent: done\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:google-interactions",
|
||||
"provider:google",
|
||||
"protocol:google-interactions"
|
||||
],
|
||||
"name": "google-interactions/streams-reasoning-and-retains-thought-signatures",
|
||||
"recordedAt": "2026-10-03T18:08:37.674Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"user_input\",\"content\":[{\"type\":\"text\",\"text\":\"Find the smallest positive integer that leaves remainder 1 modulo 7, 2 modulo 9, and 3 modulo 11. Explain briefly.\"}]}],\"stream\":true,\"store\":false,\"generation_config\":{\"max_output_tokens\":4096,\"thinking_level\":\"high\",\"thinking_summaries\":\"auto\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"content\":{\"text\":\"**Analyzing Congruences**\\n\\nI'm tackling a system of congruences to find the smallest positive integer x. Currently, I'm reviewing the Chinese Remainder Theorem as a potential solution, or considering a direct congruence-based approach. The congruences are as follows: $x \\\\equiv 1 \\\\pmod{7}$, $x \\\\equiv 2 \\\\pmod{9}$, $x \\\\equiv 3 \\\\pmod{11}$. I will use these congruences to find a solution.\\n\\n\\n\",\"type\":\"text\"},\"type\":\"thought_summary\"},\"event_type\":\"step.delta\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"content\":{\"text\":\"**Combining Congruences**\\n\\nI've made progress by combining the first two congruences. I determined that x is congruent to 29 modulo 63. Now, I am focused on the remaining congruence, attempting to solve for k in the expression 63k + 29 ≡ 3 (mod 11). I simplified the expression to 8k ≡ 7 (mod 11) and am looking to multiply both sides to eliminate the coefficient of k.\\n\\n\\n\",\"type\":\"text\"},\"type\":\"thought_summary\"},\"event_type\":\"step.delta\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EskbCsYbAWkUfRM84T8fIy7KmpVRW3jt54Qo5USy0D9bJVmz6itgVBPMK3oD6Lx2AlpdFNnJ4oKCuDmIBs7ok77Tx10Ou1qfT0CsnJ391kx2pc1xAKdJJxvv2nBkHzVWb2/BORG0YNsdE96ulew1vC6x4mu/33XglL06m1hNhK8DPXGdn5lWMhKOqb9L9oBhMxazHUMlwStX67RdjURSVw/lodq/6Ao6PPTguHE9gEyh/iWwALZJCKkZw0LNgdWSRa/Ke4tfr8bncWcIGqz8bmxlaSUVB70m3o7TqvX6l4OyxEG4EwI8ixAKsxIicmxzzIqtGlWGqV/1ahnfIEktee0gF+cH/LcrBjUDblnOGUeKNCZlz7MBVIoRlWx5al5xcX1Vb63SekRlAuBqHqdEioYvMwm52QXzeXfNMDf+ixMhtpQ2fpAjG7PM+D0y+m2V9+3iUI/63e44RjWW2kZhgLrCQ9KSWrRoSsohGVS9zvGQBaVc6vb1QyX56A80eLKbqcGhWSHc+gp1L2lCM1MwBsvl9wM+Szzs8O61UBQJD4C/Pvyed1hxwt0Ebku70VyfTPJGLufWDisMOrruBmQ8MU+OyFk4N1Qm2ZkhOw2tE3yi9lf2ih0mfuj2a/nj81GIbvbSA+r+Oh3lhgOX7IlzidD6BJgSgWY4bG83a1twEVJyvBKZBIIDYKumDSz7tX9gWeKa5b90+RKGohyeM4Kp3yxuPGDPW6jxSHa6hch837OagyVjO/EVkB1LruWIAVAvFG9oAPYNesB2xFeeseXCnAQaNYbw6KMkH85kNBREvFZU1QpFHFWQ9gAxWfSTxCjnkqGao1In/b7wQVopgMVthrCr5VScf8HArkDdD8R9pnN9iD6he0mjmsFnw3mA80KxcWugmKiYkWrIfpEdbNB5FsyCffW+Wt9WmmFnH168qkhbkp5uXk3s8/bJFKmeijyNVWIG9LlLnZ6tpDkq8UqZSriapLq2byI1fk+Mu3UszAeen4qSDXQ32xGqKSr+SG/qpqVlFrvYr2PePjaZUljSqrH0CQSdqMvmlnM9E3XO4rQNtKcmg14BZ4FpVh7m/kCM0zfCfjSdX/BwWibEV4loM+eqLYyX7aul+chBDiQeO3iGkGWkfpycax2NDTYEMfimjuAJlWXsAuhJcdzLdeRucaZwJ0Fwpo0P153I8GIo7AQckiVaeBZRBY9fTEoz8khALMGRGSMrFDqqJ+1dDVdNxAh2txUeOneNi/D0rfM1W4yLJaB6Djl48VHoM1DguwSNDj4yE32Ku4XpXasoSd5/jRmmT8FKvk+FF4jxbJZcYHyTLcnyFCvDfrSBqdJAbdaLgkvRz9Gl1JAs3WPKR+CvIjbuf1cWxj8V8fyngf239aHVfePTXFXPi+M7uRo+923rjW7XxJortd68jHa7GpaLGUKVXgtKBXkAKCrRNRRwIJjODieyGYkj+GdUE0ikYCwbUPx8ZamGY3hjP8GjaVHK+9pUP7CddlUn3H3lIFbcXxQUtCOEFmwg9s9JcJSWLaoX0Vg0KIC1hYtamEkdiJSDbQWURPgzTD/llV86iDx/yaDgXWxaT4DkyNRfVcK2AFiDHQjHhRGd8KDizdpyCfAJZwTlF/nAPz4/FA1/bQ/cT5MHuebzbWbzkYDkDBzTXp2ND6VF50a3XUoKGV3LC6AEsUrUIDksKatHh0hLe+j/YMwOluyvI1WqfAHjCnAqjl/l29S/gFpAKMMjSS60OQzgp/hhORd+gDb8vj/9AS19B8uowIz8KnJkMHv57W3CKPROz2gBMl1VQYm5rRIayBn1fEtpwldDScyhFNtdx7G/ypjQLpgVqzdZTy1dZqHJwA1lOtoMfG1kz/SIDxF56kzUpUcc9Smz/hF3hhGn7TYsQZtcMRB1/W8JCq4qvUle0MIAOXn4XukmQXae6M25aHfHUad7H4BGrFP+P0398drrRQGaqSzos4Hpl1Ev6+3t4mlC81aeRi12asZoIuYTs+RxH4wSe/tbfX+579MuSm07iQGSSmbo8r9xcEREPjEJZ+hNl1YlZB3x/fpzMfVW9L0omiF+rPcTiiILmQh32LRkJ8ZDiAw4HXTZHYKR1sGe+NBqCjT5Y1ZGzQE+4RntZQ7L39qDN8N6VK1gSnL1pEsHui+kLBh8OF+VZWWw2625evbyZ4TPTOccgGf6oOnqNKNn1R6yu7ixK/eOVl1hWUbFZBaWcBvIw1IT4kjA63Q9C9cTqs2abxMzVdP4vdN1M5PC7vNoD9CYBS6GXqw+8B5ODgs6o7ktSNkeUas8XWFxb8xq+m/X112ujTuIcv7loDijzdmKwZq1YLhVEfPTtgGDxZLlGAwIbCvxK2WPjT7h9o+aDZpX3mf7Qwm6+mBMboz4i8b0QnSCIiSBsVx/tHbwA/bH8VwaIqqhZssQMhFCu3RoNlEQ5O9oV4qONmPDhuRNpt/ArBiCyCQ+N4LFvwVQr7t5y7ioW7JVCCoBSjBlQ3b2EEbH/4tbgt9VkCIXN5qSjcu/rmgMQvpYa4Mud6Hq4Z3GNe7jTgc084nD4E2pR5yqxkjgI7axtkHbDLcNKa2yNW7OwIrg0j+iK6AW6TMd/M3oXOH26FAUfdLi72tl6jcS3osbQZ+/8jed+frQdx2GGxT/NLvfLq002T+rgq4InFPcrzXHItfk6pNfAOnYPyB8zw8INSSpa5TOk11CKQ0eOv4SLDbuf8ffRNfOA4cl/dm4M6bANzx9R1RWdRt1bf05NPtzPN7lH32CtCzajFA1wmI6JpcXerG0QosKWjfltSdYTOE2+8SNW2xP3pTuQUpi7X5ewXeJZ06rtXcS09URjZCRd1Vob6zDavL1chuJOmK0eoZpCS0IlaLzIZsiaQXpQeAVVMA8+oUX/JHovkYD+iWy+Q12TtuIR6sOFR6upajX677lFRY+P8NjoharzWKBhZwkxII31I9aU/T59G5/qmWvi+HUiz7Ke/cbIceQeHejjbcyh4coq9K15SvthpAIkO9WohOXsJBIEGBUNA6bzree4nHOkPD9A34FXjjs6dFnbc9S9Edu8PlfKJ5E6I1z680J3UkoA749fHfK8wV5lIVqd81LLYQweT7U5ue2AQmAECxcXW0cBDsMv3mQL9wBcdKdxFOb9ZEKkako1REjm5RuIlJ9tcc9X5ZSE1vCh/enj3Lj1UrY2qNeTYCsSoSEoEmIyarDzG9q4Nd5K5fTW8IHKgDbwA3Al4Nu8fQLlc9OnHp8zUXXr6tfVrn/5LfqdjZANCJuIamnlg/kVBpp6364KbdapnI1B1FoEhKUZbqSx9IxgDpj1bY+0R9Apms4vV7bLFQQks8HsBFxZ80dlVxOt//YqOXy6x4oCmg8x5Gxb6z6oHybcO34LUIUHOHuC+Dcc0B+lAQjINHEn395KNcgZiO57Line truncated
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"prefix:google-interactions",
|
||||
"provider:google",
|
||||
"protocol:google-interactions"
|
||||
],
|
||||
"name": "google-interactions/streams-text-and-reports-usage",
|
||||
"recordedAt": "2026-10-03T18:08:32.334Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://generativelanguage.googleapis.com/v1beta/interactions",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"gemini-3.8-flash\",\"input\":[{\"type\":\"user_input\",\"content\":[{\"type\":\"text\",\"text\":\"Reply with exactly one word: hello\"}]}],\"stream\":true,\"store\":false,\"generation_config\":{\"max_output_tokens\":2048,\"thinking_level\":\"low\"}}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "event: interaction.created\ndata: {\"interaction\":{\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.created\"}\n\nevent: interaction.status_update\ndata: {\"status\":\"in_progress\",\"event_type\":\"interaction.status_update\"}\n\nevent: step.start\ndata: {\"index\":0,\"step\":{\"type\":\"thought\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":0,\"delta\":{\"signature\":\"EmkKZwFpFH0TKZDuA7i8rhztLxjVI9+u+bpJI/x6/j60nie/dFgj5TyzAJCDDZQYfmeXEBrujJcdKT5FiKPuuMYWq1shTTILJ1JQpG2WkGAk6rAxxgxp7Ac0+rporJ7knHFF/jVWQD+AYGY=\",\"type\":\"thought_signature\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":0,\"event_type\":\"step.stop\"}\n\nevent: step.start\ndata: {\"index\":1,\"step\":{\"type\":\"model_output\"},\"event_type\":\"step.start\"}\n\nevent: step.delta\ndata: {\"index\":1,\"delta\":{\"text\":\"hello\",\"type\":\"text\"},\"event_type\":\"step.delta\"}\n\nevent: step.stop\ndata: {\"index\":1,\"event_type\":\"step.stop\"}\n\nevent: interaction.completed\ndata: {\"interaction\":{\"status\":\"completed\",\"usage\":{\"total_tokens\":9,\"total_input_tokens\":8,\"input_tokens_by_modality\":[{\"modality\":\"text\",\"tokens\":8}],\"total_cached_tokens\":0,\"total_output_tokens\":1,\"total_tool_use_tokens\":0,\"total_thought_tokens\":0,\"raw_prompt_token\":39,\"model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":39}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":5}]}],\"non_grounding_model_invocation_token_counts\":[{\"prompt_tokens_details\":[{\"modality\":\"text\",\"tokens\":39}],\"candidates_tokens_details\":[{\"modality\":\"text\",\"tokens\":5}]}]},\"created\":\"2026-10-03T18:08:32Z\",\"updated\":\"2026-10-03T18:08:32Z\",\"service_tier\":\"standard\",\"object\":\"interaction\",\"model\":\"gemini-3.8-flash\"},\"event_type\":\"interaction.completed\"}\n\nevent: done\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient, Media } from "../src/index.js"
|
||||
import { BlackForestLabs, Fal, Google, OpenAI, Replicate, Stability, XAI, ZAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Effect, Fiber, Layer, Ref } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import { LLMClient, RequestExecutor } from "../../src/route.js"
|
||||
import type { Service as LLMClientService } from "../../src/route/client.js"
|
||||
import type { Service as RequestExecutorService } from "../../src/route/executor.js"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
|
||||
import { NodeFileSystem } from "@effect/platform-node"
|
||||
import { Effect, Ref, Schema } from "effect"
|
||||
import { FileSystem } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { AIError, Media, Message } from "../src/index.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
import { dynamicResponse, scriptedResponses } from "./lib/http.js"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { AIError, LLMEvent, Media, SpeechEvent, TranscriptionEvent } from "../src/index.js"
|
||||
import { RequestExecutor } from "../src/route.js"
|
||||
import { AI } from "../src/promise.js"
|
||||
|
||||
@@ -430,6 +430,7 @@ describe("provider package entrypoints", () => {
|
||||
|
||||
test("maps Google package settings onto the Gemini model", async () => {
|
||||
const Google = await import("@opencode/ai/providers/google")
|
||||
const GoogleInteractions = await import("@opencode/ai/providers/google/interactions")
|
||||
const selected = Google.model("gemini-2.5-flash", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://generativelanguage.test/v1beta",
|
||||
@@ -443,6 +444,16 @@ describe("provider package entrypoints", () => {
|
||||
expect(selected.route.defaults.headers).toEqual({ "x-application": "opencode" })
|
||||
expect(selected.route.defaults.http?.body).toEqual({ safetySettings: [] })
|
||||
expect(selected.route.defaults.providerOptions).toEqual({ thinkingConfig: { thinkingBudget: 1_024 } })
|
||||
const interactions = GoogleInteractions.model("gemini-3.8-flash", {
|
||||
apiKey: "fixture",
|
||||
baseURL: "https://generativelanguage.test/v1beta",
|
||||
thinkingLevel: "low",
|
||||
store: true,
|
||||
})
|
||||
expect(interactions.route.id).toBe("google-interactions")
|
||||
expect(interactions.route.endpoint.baseURL).toBe("https://generativelanguage.test/v1beta")
|
||||
expect(interactions.route.defaults.providerOptions).toEqual({ thinkingLevel: "low", store: true })
|
||||
expect(Google.configure().interactions("gemini-3.8-flash").route.protocol).toBe("google-interactions")
|
||||
})
|
||||
|
||||
test("selects Vertex entrypoints with the same model contract", async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, LLMRequest, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { Alibaba } from "../../src/providers.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
CacheHint,
|
||||
LLM,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { EventStreamCodec } from "@smithy/eventstream-codec"
|
||||
import { fromUtf8, toUtf8 } from "@smithy/util-utf8"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Encoding, Ref, Schema, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Effect, Ref, Schema, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
CacheHint,
|
||||
@@ -866,7 +867,7 @@ describe("Bedrock Converse route", () => {
|
||||
reason: { _tag: "InvalidProviderOutput", classification: "incomplete-stream" },
|
||||
message: `Incomplete Bedrock Converse event-stream frame: ${partial.length} buffered bytes remain at end of stream`,
|
||||
})
|
||||
expect(error.reason.body).toBe(Encoding.encodeBase64(partial))
|
||||
expect(error.reason.body).toBe(Base64.encode(partial))
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, Message } from "../../src/index.js"
|
||||
import { AmazonBedrockMantle } from "../../src/providers.js"
|
||||
import { model } from "../../src/providers/amazon-bedrock/mantle.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMEvent } from "../../src/index.js"
|
||||
import { CloudflareAIGateway } from "../../src/providers/cloudflare-ai-gateway.js"
|
||||
import { CloudflareWorkersAI } from "../../src/providers/cloudflare-workers-ai.js"
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMEvent, Message } from "../../src/index.js"
|
||||
import { Google } from "../../src/providers.js"
|
||||
import { LLMClient, RequestExecutor } from "../../src/route.js"
|
||||
import { recordedTests } from "../recorded-test.js"
|
||||
import { weatherTool } from "../recorded-scenarios.js"
|
||||
|
||||
const model = Google.configure({ apiKey: process.env.GEMINI_API_KEY ?? "fixture" }).interactions("gemini-3.8-flash")
|
||||
const recorded = recordedTests({
|
||||
prefix: "google-interactions",
|
||||
provider: "google",
|
||||
protocol: "google-interactions",
|
||||
requires: ["GEMINI_API_KEY"],
|
||||
})
|
||||
const InteractionMetadata = Schema.Struct({ interactionId: Schema.String })
|
||||
|
||||
describe("Google Interactions recorded", () => {
|
||||
recorded.effect("streams text and reports usage", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt: "Reply with exactly one word: hello",
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: { thinkingLevel: "low" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.text.trim().toLowerCase()).toBe("hello")
|
||||
expect(response.events.some(LLMEvent.is.textDelta)).toBe(true)
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
expect(response.events.filter(LLMEvent.is.finish)).toHaveLength(1)
|
||||
expect(response.usage?.inputTokens).toBeGreaterThan(0)
|
||||
expect(response.usage?.contextTokens).toBeGreaterThan(0)
|
||||
expect(response.usage?.providerMetadata?.google).toMatchObject({ total_input_tokens: expect.any(Number) })
|
||||
}),
|
||||
)
|
||||
|
||||
recorded.effect(
|
||||
"streams reasoning and retains thought signatures",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model,
|
||||
prompt:
|
||||
"Find the smallest positive integer that leaves remainder 1 modulo 7, 2 modulo 9, and 3 modulo 11. Explain briefly.",
|
||||
generation: { maxTokens: 4096 },
|
||||
providerOptions: { thinkingLevel: "high", thinkingSummaries: "auto" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.reasoning.length).toBeGreaterThan(0)
|
||||
expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
|
||||
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
providerMetadata: { google: { interactionSignature: expect.any(String) } },
|
||||
}),
|
||||
]),
|
||||
)
|
||||
expect(response.text.length).toBeGreaterThan(0)
|
||||
expect(response.finishReason.normalized).toBe("stop")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
|
||||
recorded.effect(
|
||||
"replays native tool results and signatures statelessly",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model,
|
||||
system: "Use get_weather for weather questions. Answer concisely after receiving the result.",
|
||||
prompt: "What is the weather in Paris?",
|
||||
tools: [weatherTool],
|
||||
toolChoice: { type: "tool", name: weatherTool.name },
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: { thinkingLevel: "low", thinkingSummaries: "auto" },
|
||||
})
|
||||
const first = yield* LLMClient.generate(request)
|
||||
|
||||
expect(first.toolCalls).toHaveLength(1)
|
||||
expect(first.events.some(LLMEvent.is.toolInputDelta)).toBe(true)
|
||||
expect(first.finishReason.normalized).toBe("tool-calls")
|
||||
const call = first.toolCalls[0]
|
||||
if (!call) throw new Error("Missing recorded weather tool call")
|
||||
expect(call.name).toBe(weatherTool.name)
|
||||
expect(call.input).toEqual({ city: "Paris" })
|
||||
expect(call.providerMetadata?.google).toHaveProperty("interactionSignature")
|
||||
const second = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model,
|
||||
system: request.system,
|
||||
tools: [weatherTool],
|
||||
toolChoice: "none",
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: { thinkingLevel: "low", thinkingSummaries: "auto" },
|
||||
messages: [
|
||||
...request.messages,
|
||||
first.message,
|
||||
Message.tool({ id: call.id, name: call.name, result: { temperature: 22, condition: "sunny" } }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(second.text.toLowerCase()).toContain("sunny")
|
||||
expect(second.text).toContain("22")
|
||||
expect(second.toolCalls).toHaveLength(0)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
|
||||
recorded.effect(
|
||||
"continues tool results with previous interaction id",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const request = LLM.request({
|
||||
model,
|
||||
system: "Use get_weather for weather questions. Answer concisely after receiving the result.",
|
||||
prompt: "What is the weather in Paris?",
|
||||
tools: [weatherTool],
|
||||
toolChoice: "required",
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: { thinkingLevel: "low", store: true },
|
||||
})
|
||||
const first = yield* LLMClient.generate(request)
|
||||
const metadata = yield* Schema.decodeUnknownEffect(InteractionMetadata)(
|
||||
first.events.find(LLMEvent.is.finish)?.providerMetadata?.google,
|
||||
)
|
||||
const executor = yield* RequestExecutor.Service
|
||||
const cleanup = executor
|
||||
.execute(
|
||||
HttpClientRequest.delete(
|
||||
`https://generativelanguage.googleapis.com/v1beta/interactions/${metadata.interactionId}`,
|
||||
).pipe(HttpClientRequest.setHeader("x-goog-api-key", process.env.GEMINI_API_KEY ?? "fixture")),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
expect(first.toolCalls).toHaveLength(1)
|
||||
const call = first.toolCalls[0]
|
||||
if (!call) throw new Error("Missing recorded weather tool call")
|
||||
const second = yield* LLMClient.generate(
|
||||
LLM.request({
|
||||
model,
|
||||
system: request.system,
|
||||
tools: [weatherTool],
|
||||
toolChoice: "none",
|
||||
generation: { maxTokens: 2048 },
|
||||
providerOptions: { thinkingLevel: "low", previousInteractionId: metadata.interactionId, store: false },
|
||||
messages: [
|
||||
Message.tool({ id: call.id, name: call.name, result: { temperature: 22, condition: "sunny" } }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(second.text.toLowerCase()).toContain("sunny")
|
||||
expect(second.text).toContain("22")
|
||||
expect(second.toolCalls).toHaveLength(0)
|
||||
expect(second.finishReason.normalized).toBe("stop")
|
||||
}).pipe(Effect.ensuring(cleanup))
|
||||
}),
|
||||
120_000,
|
||||
)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LanguageModel, LLM, Message, ToolCallPart } from "../../src/index.js"
|
||||
import { GoogleVertex, GoogleVertexChat, GoogleVertexMessages, GoogleVertexResponses } from "../../src/providers.js"
|
||||
import { LLMClient } from "../../src/route.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient, LLM, LLMEvent, LLMRequest, Media, Message, ToolDefinition } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/meta.js"
|
||||
import { MetaMessages } from "../../src/protocols/meta-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient } from "../../src/index.js"
|
||||
import { Meta } from "../../src/providers/index.js"
|
||||
import { OpenAIChat } from "../../src/protocols/openai-chat.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { LLM } from "../../src/index.js"
|
||||
import { MiniMax } from "../../src/providers.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMEvent, Message, SystemPart, ToolDefinition, Media } from "../../src/index.js"
|
||||
import { Mistral } from "../../src/providers/index.js"
|
||||
import { MistralChat } from "../../src/protocols/index.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { Moonshot } from "../../src/providers.js"
|
||||
import { AnthropicMessages } from "../../src/protocols/anthropic-messages.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, Message, ToolDefinition } from "../../src/index.js"
|
||||
import {
|
||||
AmazonBedrock,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Ref, Schema, Stream } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
HttpOptions,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { LLM, LLMRequest, Message, ToolCallPart, ToolChoice, ToolDefinition } from "../../src/index.js"
|
||||
import { Auth, LLMClient } from "../../src/route.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Stream } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Socket } from "effect/socket"
|
||||
import { LLM, LLMRequest, Message, ToolRuntime } from "../../src/index.js"
|
||||
import {
|
||||
LLMClient,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { ConfigProvider, Effect, Layer, Logger, Ref, Schema, Stream } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import {
|
||||
Media,
|
||||
LLM,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Headers, HttpClientRequest } from "effect/unstable/http"
|
||||
import { Headers, HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient } from "../../src/index.js"
|
||||
import { XAI } from "../../src/providers.js"
|
||||
import { Auth } from "../../src/route.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Image, ImageClient } from "../../src/index.js"
|
||||
import { ZAI } from "../../src/providers.js"
|
||||
import { it } from "../lib/effect.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from "bun:test"
|
||||
import { ConfigProvider, Effect } from "effect"
|
||||
import { Headers } from "effect/unstable/http"
|
||||
import { Headers } from "effect/http"
|
||||
import { Auth, LLM, LLMClient, Message, ReasoningPart, ToolDefinition } from "../../src/index.js"
|
||||
import { ZAI, ZAICodingPlan } from "../../src/providers.js"
|
||||
import { compileRequest } from "../../src/route/client.js"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpRecorder } from "@opencode/http-recorder"
|
||||
import { NodeSocket } from "@effect/platform-node"
|
||||
import { Layer } from "effect"
|
||||
import { Socket } from "effect/unstable/socket"
|
||||
import { Socket } from "effect/socket"
|
||||
import * as path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { LLMClient, RequestExecutor } from "../src/route.js"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Encoding, Layer, Stream } from "effect"
|
||||
import { Effect, Layer, Stream } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { Speech, SpeechClient, SpeechEvent } from "../src/index.js"
|
||||
import { Cartesia, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
@@ -47,7 +48,7 @@ describe("Speech", () => {
|
||||
JSON.stringify({
|
||||
candidates: [
|
||||
{
|
||||
content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Encoding.encodeBase64(bytes) } }] },
|
||||
content: { parts: [{ inlineData: { mimeType: "audio/wav", data: Base64.encode(bytes) } }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
@@ -244,7 +245,7 @@ describe("Speech", () => {
|
||||
Effect.gen(function* () {
|
||||
const record = (bytes: ReadonlyArray<number>, character: string, start: number) =>
|
||||
JSON.stringify({
|
||||
audio_base64: Encoding.encodeBase64(Uint8Array.from(bytes)),
|
||||
audio_base64: Base64.encode(Uint8Array.from(bytes)),
|
||||
alignment: {
|
||||
characters: [character],
|
||||
character_start_times_seconds: [start],
|
||||
|
||||
@@ -1,14 +1,201 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Effect, Schema, SchemaGetter } from "effect"
|
||||
import { LLM } from "../src/index.js"
|
||||
import { OpenAIChat } from "../src/protocols.js"
|
||||
import { AnthropicMessages, Gemini, OpenAIChat, OpenAIResponses } from "../src/protocols.js"
|
||||
import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema.js"
|
||||
import { Tool, toDefinitions } from "../src/tool.js"
|
||||
import { Auth } from "../src/route.js"
|
||||
import { compileRequest } from "../src/route/client.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
const Token = Schema.Struct({ token: Schema.String })
|
||||
const token = {
|
||||
type: "object",
|
||||
properties: { token: { type: "string" } },
|
||||
required: ["token"],
|
||||
additionalProperties: false,
|
||||
}
|
||||
const fromEmpty = {
|
||||
decode: SchemaGetter.transform(() => ({ token: "default" })),
|
||||
encode: SchemaGetter.transform(() => ({})),
|
||||
}
|
||||
// Callers send `{}`; the handler receives a token.
|
||||
const defaulted = Schema.Struct({}).pipe(Schema.decodeTo(Token, fromEmpty))
|
||||
// Callers send a token; the handler receives an empty struct.
|
||||
const reversed = Token.pipe(
|
||||
Schema.decodeTo(Schema.Struct({}), {
|
||||
decode: SchemaGetter.transform(() => ({})),
|
||||
encode: SchemaGetter.transform(() => ({ token: "default" })),
|
||||
}),
|
||||
)
|
||||
|
||||
describe("tool schema projections", () => {
|
||||
test("only normalizes typed empty input structs, preserving raw schemas and output schemas", () => {
|
||||
const schema = { not: { type: "null" } }
|
||||
const definitions = Tool.toDefinitions({
|
||||
typed: Tool.make({
|
||||
description: "Typed",
|
||||
parameters: Schema.Struct({}),
|
||||
success: Schema.Struct({}),
|
||||
execute: () => Effect.succeed({}),
|
||||
}),
|
||||
raw: Tool.make({ description: "Raw", jsonSchema: schema, execute: () => Effect.succeed({}) }),
|
||||
})
|
||||
expect(definitions[0]?.inputSchema).toEqual({ type: "object", properties: {}, additionalProperties: false })
|
||||
expect(definitions[0]?.outputSchema).toEqual(schema)
|
||||
expect(definitions[1]?.inputSchema).toEqual(schema)
|
||||
})
|
||||
|
||||
test("retains empty input descriptions and preserves named outputs and explicit raw schemas", () => {
|
||||
const parameters = Schema.Struct({}).annotate({ identifier: "Ping", description: "No arguments" })
|
||||
const raw = { $ref: "#/$defs/Raw", $defs: { Raw: { not: { type: "null" }, description: "Raw schema" } } }
|
||||
const definitions = Tool.toDefinitions({
|
||||
typed: Tool.make({ description: "Ping", parameters, success: parameters, execute: () => Effect.succeed({}) }),
|
||||
raw: Tool.make({ description: "Raw", jsonSchema: raw, execute: () => Effect.succeed({}) }),
|
||||
})
|
||||
expect(definitions[0]?.inputSchema).toEqual({
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
description: "No arguments",
|
||||
})
|
||||
expect(definitions[0]?.outputSchema).toEqual({
|
||||
$ref: "#/$defs/Ping",
|
||||
$defs: { Ping: { not: { type: "null" }, description: "No arguments" } },
|
||||
})
|
||||
expect(definitions[1]?.inputSchema).toEqual(raw)
|
||||
})
|
||||
|
||||
test("normalizes inputs whose encoded side is an unchecked empty struct", () => {
|
||||
const definitions = Tool.toDefinitions({
|
||||
defaulted: Tool.make({ description: "Defaulted", parameters: defaulted, success: defaulted }),
|
||||
named: Tool.make({
|
||||
description: "Named",
|
||||
parameters: defaulted.annotate({ identifier: "Defaulted" }),
|
||||
success: Schema.String,
|
||||
}),
|
||||
encodedMetadata: Tool.make({
|
||||
description: "Encoded metadata",
|
||||
parameters: Schema.Struct({})
|
||||
.annotate({ identifier: "Ping", title: "Ping", description: "No arguments" })
|
||||
.pipe(Schema.decodeTo(Token, fromEmpty)),
|
||||
success: Schema.String,
|
||||
}),
|
||||
decodedCheck: Tool.make({
|
||||
description: "Decoded check",
|
||||
parameters: defaulted.check(Schema.makeFilter(() => true, { toJsonSchema: () => ({ minProperties: 1 }) })),
|
||||
success: Schema.String,
|
||||
}),
|
||||
reversed: Tool.make({ description: "Reversed", parameters: reversed, success: Schema.String }),
|
||||
encodedCheck: Tool.make({
|
||||
description: "Encoded check",
|
||||
parameters: Schema.Struct({}).check(Schema.isMinProperties(1)),
|
||||
success: Schema.String,
|
||||
}),
|
||||
})
|
||||
expect(definitions[0]?.inputSchema).toEqual(empty)
|
||||
expect(definitions[0]?.outputSchema).toEqual({ not: { type: "null" } })
|
||||
expect(definitions[1]?.inputSchema).toEqual(empty)
|
||||
expect(definitions[2]?.inputSchema).toEqual({ ...empty, title: "Ping", description: "No arguments" })
|
||||
expect(definitions[3]?.inputSchema).toEqual(empty)
|
||||
expect(definitions[4]?.inputSchema).toEqual(token)
|
||||
expect(definitions[5]?.inputSchema).toEqual({ not: { type: "null" }, minProperties: 1 })
|
||||
})
|
||||
|
||||
const empty = { type: "object", properties: {}, additionalProperties: false }
|
||||
const nonempty = {
|
||||
type: "object",
|
||||
properties: { query: { type: "string" } },
|
||||
required: ["query"],
|
||||
additionalProperties: false,
|
||||
}
|
||||
const raw = { type: "object", properties: { value: { type: "number" } }, additionalProperties: true }
|
||||
const names = ["ping", "lookup", "raw", "reversed"]
|
||||
const schemas = [empty, nonempty, raw, token]
|
||||
|
||||
for (const scenario of [
|
||||
{
|
||||
route: OpenAIChat.route,
|
||||
tools: schemas.map((parameters, index) => ({
|
||||
type: "function",
|
||||
function: { name: names[index], parameters },
|
||||
})),
|
||||
},
|
||||
{
|
||||
route: OpenAIResponses.route,
|
||||
tools: schemas.map((parameters, index) => ({
|
||||
type: "function",
|
||||
name: names[index],
|
||||
parameters,
|
||||
})),
|
||||
},
|
||||
{
|
||||
route: AnthropicMessages.route,
|
||||
tools: schemas.map((input_schema, index) => ({
|
||||
name: names[index],
|
||||
input_schema,
|
||||
})),
|
||||
},
|
||||
{
|
||||
route: Gemini.route,
|
||||
tools: [
|
||||
{
|
||||
functionDeclarations: schemas.map((parametersJsonSchema, index) => ({
|
||||
name: names[index],
|
||||
parametersJsonSchema,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
]) {
|
||||
for (const input of [
|
||||
{ name: "plain", schema: Schema.Struct({}) },
|
||||
{ name: "described", schema: Schema.Struct({}).annotate({ description: "No arguments" }) },
|
||||
{ name: "named", schema: Schema.Struct({}).annotate({ identifier: "Ping" }) },
|
||||
{
|
||||
name: "named and described",
|
||||
schema: Schema.Struct({}).annotate({ identifier: "Ping", description: "No arguments" }),
|
||||
},
|
||||
{ name: "encoded", schema: defaulted },
|
||||
{ name: "named encoded", schema: defaulted.annotate({ identifier: "Ping" }) },
|
||||
]) {
|
||||
it.effect(
|
||||
`${scenario.route.id} prepares ${input.name} empty Effect Struct tools alongside nonempty and raw schemas`,
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: scenario.route.with({ auth: Auth.bearer("test") }).model({ id: "test-model" }),
|
||||
prompt: "Use a tool.",
|
||||
tools: Tool.toDefinitions({
|
||||
ping: Tool.make({
|
||||
description: "Ping",
|
||||
parameters: input.schema,
|
||||
success: Schema.String,
|
||||
execute: () => Effect.succeed("pong"),
|
||||
}),
|
||||
lookup: Tool.make({
|
||||
description: "Lookup",
|
||||
parameters: Schema.Struct({ query: Schema.String }),
|
||||
success: Schema.String,
|
||||
execute: (input) => Effect.succeed(input.query),
|
||||
}),
|
||||
raw: Tool.make({ description: "Raw", jsonSchema: raw, execute: () => Effect.succeed("raw") }),
|
||||
reversed: Tool.make({
|
||||
description: "Reversed",
|
||||
parameters: reversed,
|
||||
success: Schema.String,
|
||||
execute: () => Effect.succeed("reversed"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(prepared.body.tools).toMatchObject(scenario.tools)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("moonshot strips $ref siblings and converts tuple arrays to a schema object", () => {
|
||||
expect(
|
||||
ToolSchemaProjection.moonshot({
|
||||
@@ -91,7 +278,12 @@ describe("tool schema projections", () => {
|
||||
const exclusive = { oneOf: [{ type: "object" }, { type: "object", required: ["a"] }] }
|
||||
const object = { type: "object", properties: {} }
|
||||
|
||||
expect(yield* parameters(parameterless)).toEqual({ type: "object", description: "No input." })
|
||||
expect(yield* parameters(parameterless)).toEqual({
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
description: "No input.",
|
||||
})
|
||||
expect(yield* parameters({})).toEqual({ type: "object" })
|
||||
expect(yield* parameters({ description: "Query", properties: { q: { type: "string" } } })).toEqual({
|
||||
type: "object",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
||||
import * as TestClock from "effect/testing/TestClock"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { Media, Transcription, TranscriptionClient, type TranscriptionEvent } from "../src/index.js"
|
||||
import { AssemblyAI, Deepgram, ElevenLabs, Google, OpenAI } from "../src/providers.js"
|
||||
import { it } from "./lib/effect.js"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
|
||||
const modules = {
|
||||
fixture: source("../../gui-extensions/src/browser/panel.fixture.tsx"),
|
||||
host: source("../src/runtime/extension/host.tsx"),
|
||||
@@ -13,49 +14,74 @@ const modules = {
|
||||
fileRenderer: source("../../gui-extensions/src/file/renderer.tsx"),
|
||||
}
|
||||
|
||||
story.beforeEach(async ({ mount, page }) => {
|
||||
// Any story loads the app styles; the fixture mounts the real side region and extensions beside it.
|
||||
await mount("ui-line-comment--editor")
|
||||
await page.evaluate(async (modules) => {
|
||||
const [{ mountBrowserRegion }, host, panels, language, browser, file] = await Promise.all([
|
||||
import(modules.fixture),
|
||||
import(modules.host),
|
||||
import(modules.panels),
|
||||
import(modules.language),
|
||||
import(modules.browser),
|
||||
import(modules.file),
|
||||
])
|
||||
|
||||
mountBrowserRegion({
|
||||
LanguageProvider: language.LanguageProvider,
|
||||
ExtensionHostProvider: host.ExtensionHostProvider,
|
||||
useExtensionHost: host.useExtensionHost,
|
||||
createRegion: panels.createRegion,
|
||||
definitions: [
|
||||
{ ...browser.default, renderer: () => import(modules.browserRenderer) },
|
||||
{ ...file.default, renderer: () => import(modules.fileRenderer) },
|
||||
],
|
||||
})
|
||||
}, modules)
|
||||
})
|
||||
|
||||
story("keeps a restored browser tab selected and undrawn until the desktop's first inventory", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-region-fixture")
|
||||
const tabs = root.getByRole("tab")
|
||||
const tree = root.getByTestId("tree")
|
||||
await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible()
|
||||
await expect(tabs).toHaveText(["alpha.ts"])
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
// Beta was left on its browser tab, which the desktop has not reported yet.
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("selected")).toHaveText(/^browser:tab_/)
|
||||
await expect(tabs).toHaveText(["beta.ts"])
|
||||
// No fallback tab was selected, so the file tab's selection never switched the tree to All files.
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
await root.getByRole("button", { name: "First inventory", exact: true }).click()
|
||||
await expect(tabs).toHaveText(["beta.ts", "Preview"])
|
||||
await expect(root.getByRole("tab", { name: "Preview", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
})
|
||||
|
||||
story(
|
||||
"keeps a restored browser tab selected and undrawn until the desktop's first inventory",
|
||||
async ({ mount, page }) => {
|
||||
// Any story loads the app styles; the fixture mounts the real side region and extensions beside it.
|
||||
await mount("ui-line-comment--editor")
|
||||
await page.evaluate(async (modules) => {
|
||||
const [{ mountBrowserRegion }, host, panels, language, browser, file] = await Promise.all([
|
||||
import(modules.fixture),
|
||||
import(modules.host),
|
||||
import(modules.panels),
|
||||
import(modules.language),
|
||||
import(modules.browser),
|
||||
import(modules.file),
|
||||
])
|
||||
mountBrowserRegion({
|
||||
LanguageProvider: language.LanguageProvider,
|
||||
ExtensionHostProvider: host.ExtensionHostProvider,
|
||||
useExtensionHost: host.useExtensionHost,
|
||||
createRegion: panels.createRegion,
|
||||
definitions: [
|
||||
{ ...browser.default, renderer: () => import(modules.browserRenderer) },
|
||||
{ ...file.default, renderer: () => import(modules.fileRenderer) },
|
||||
],
|
||||
})
|
||||
}, modules)
|
||||
"keeps the browser tabs while the pane's Ipc is away and registers them again when it returns",
|
||||
async ({ page }) => {
|
||||
const root = page.getByTestId("browser-region-fixture")
|
||||
const tabs = root.getByRole("tab")
|
||||
const tree = root.getByTestId("tree")
|
||||
await expect(root.getByText("Registrations: 1", { exact: true })).toBeVisible()
|
||||
await expect(tabs).toHaveText(["alpha.ts"])
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
// Beta was left on its browser tab, which the desktop has not reported yet.
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByText("Registrations: 2", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("selected")).toHaveText(/^browser:tab_/)
|
||||
await expect(tabs).toHaveText(["beta.ts"])
|
||||
// No fallback tab was selected, so the file tab's selection never switched the tree to All files.
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
await root.getByRole("button", { name: "First inventory", exact: true }).click()
|
||||
await expect(tabs).toHaveText(["beta.ts", "Preview"])
|
||||
|
||||
// The pane's main extension reloads: every binding goes with it, and the strip keeps the tab it will restore.
|
||||
await root.getByRole("button", { name: "Pane away", exact: true }).click()
|
||||
await expect(tabs).toHaveText(["beta.ts", "Preview"])
|
||||
await expect(root.getByRole("tab", { name: "Preview", exact: true })).toHaveAttribute("aria-selected", "true")
|
||||
await expect(tree).toHaveText('{"tab":"changes"}')
|
||||
|
||||
// Both attachments register again at once, without a retry timer; Beta hands main the tab to restore.
|
||||
await root.getByRole("button", { name: "Pane back", exact: true }).click()
|
||||
await expect(root.getByText("Registrations: 4", { exact: true })).toBeVisible()
|
||||
await expect(root.getByText("Beta restores: 1", { exact: true })).toBeVisible()
|
||||
await expect(tabs).toHaveText(["beta.ts", "Preview"])
|
||||
},
|
||||
)
|
||||
@@ -2,23 +2,25 @@ import { fileURLToPath } from "node:url"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
|
||||
const modules = {
|
||||
fixture: source("../../gui-extensions/src/browser/panel.fixture.tsx"),
|
||||
surface: source("../src/runtime/extension/surface.tsx"),
|
||||
embeds: source("../src/runtime/extension/embeds.tsx"),
|
||||
language: source("../src/runtime/i18n/language.tsx"),
|
||||
}
|
||||
|
||||
story.beforeEach(async ({ mount, page }) => {
|
||||
// Any story loads the app styles; the fixture mounts the pane beside it on the real host surface.
|
||||
// Any story loads the app styles; the fixture mounts the pane beside it on the real host embeds.
|
||||
await mount("ui-line-comment--editor")
|
||||
await page.evaluate(async (modules) => {
|
||||
const [{ mountBrowserPane }, { createSurfaces }, language] = await Promise.all([
|
||||
const [{ mountBrowserPane }, { createEmbeds }, language] = await Promise.all([
|
||||
import(modules.fixture),
|
||||
import(modules.surface),
|
||||
import(modules.embeds),
|
||||
import(modules.language),
|
||||
])
|
||||
|
||||
mountBrowserPane({
|
||||
createSurfaces,
|
||||
createEmbeds,
|
||||
LanguageProvider: language.LanguageProvider,
|
||||
UiI18nBridge: language.UiI18nBridge,
|
||||
useLanguage: language.useLanguage,
|
||||
@@ -26,6 +28,7 @@ story.beforeEach(async ({ mount, page }) => {
|
||||
}, modules)
|
||||
await expect(page.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
|
||||
story("hides a native page when another takes the pane, the pane hides, or it unmounts", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const alpha = root.getByTestId("native-Alpha")
|
||||
@@ -50,11 +53,14 @@ story("hides the native view immediately while the pane stays mounted", async ({
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const toggle = root.getByRole("button", { name: "Toggle Review tab", exact: true })
|
||||
await expect(toggle).toBeEnabled()
|
||||
|
||||
// Read in the same task as the click so a deferred animation-frame hide cannot pass.
|
||||
const visible = await toggle.evaluate((element) => {
|
||||
element.dispatchEvent(new MouseEvent("click", { bubbles: true }))
|
||||
|
||||
return document.querySelector('[data-testid="native-Alpha"]')?.getAttribute("data-visible")
|
||||
})
|
||||
|
||||
expect(visible).toBe("false")
|
||||
await expect(root.locator("#browser-panel")).toHaveCount(1)
|
||||
await toggle.click()
|
||||
@@ -108,6 +114,19 @@ story("comments on a picked element over a still of the page", async ({ page })
|
||||
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
|
||||
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
|
||||
// The pane stays mounted when Beta is routed, and its picker then listens to Beta's page.
|
||||
await root.getByRole("button", { name: "Beta", exact: true }).click()
|
||||
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
|
||||
await picker.click()
|
||||
await root.getByRole("button", { name: "Pick element", exact: true }).click()
|
||||
await expect(editor).toBeFocused()
|
||||
await editor.fill("Beta's button too")
|
||||
await editor.press("Enter")
|
||||
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([
|
||||
"button.primary @e7: Make this the primary colour",
|
||||
"button.primary @e7: Beta's button too",
|
||||
])
|
||||
})
|
||||
|
||||
story("cancels the picker and a comment with Escape", async ({ page }) => {
|
||||
@@ -157,6 +176,7 @@ story("keeps the comment editor and its actions inside the page", async ({ page
|
||||
.poll(async () => {
|
||||
const surface = await root.locator('[data-component="browser-comment"]').boundingBox()
|
||||
const box = await editor.boundingBox()
|
||||
|
||||
return !!surface && !!box && box.y + box.height <= surface.y + surface.height
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { Definition } from "@opencode/gui-extensions/sdk"
|
||||
import { expect, story } from "../../storybook/playwright/story"
|
||||
|
||||
const source = (path: string) => `/@fs/${fileURLToPath(new URL(path, import.meta.url)).replaceAll("\\", "/")}`
|
||||
|
||||
const modules = {
|
||||
fixture: source("./extension-host.fixture.tsx"),
|
||||
builtins: source("../../gui-extensions/src/renderer.ts"),
|
||||
}
|
||||
|
||||
story.beforeEach(async ({ mount }) => {
|
||||
// Any story loads the app; the fixture mounts the real host beside it.
|
||||
await mount("ui-line-comment--editor")
|
||||
})
|
||||
|
||||
// The built-in renderer composition as a graph: `requires` edges must not form a cycle, and every optional (`uses`)
|
||||
// edge gets one row that boots the real host with that provider disabled. The first row disables nothing.
|
||||
story("built-ins: no requires cycle, and each consumer activates without each optional provider", async ({ page }) => {
|
||||
const result = await page.evaluate(async (modules) => {
|
||||
const [{ mountExtensions, until }, { builtins }] = await Promise.all([
|
||||
import(modules.fixture),
|
||||
import(modules.builtins),
|
||||
])
|
||||
|
||||
// The window this host serves has no OS, so OS-specific built-ins are left out as the app leaves them out.
|
||||
const composition: readonly Definition[] = builtins
|
||||
const definitions = composition.filter((definition) => !definition.os)
|
||||
const ids = (tokens: Definition["provides"]) => Object.values(tokens ?? {}).map((token) => token.id)
|
||||
const providerOf = (token: string) => definitions.find((definition) => ids(definition.provides).includes(token))?.id
|
||||
|
||||
const requires = new Map(
|
||||
definitions.map((definition) => [
|
||||
definition.id,
|
||||
ids(definition.requires).flatMap((token) => providerOf(token) ?? []),
|
||||
]),
|
||||
)
|
||||
|
||||
const cycles = definitions.flatMap((definition) => {
|
||||
const walk = (id: string, path: readonly string[]): string[][] =>
|
||||
path.includes(id)
|
||||
? id === definition.id
|
||||
? [[...path, id]]
|
||||
: []
|
||||
: (requires.get(id) ?? []).flatMap((next) => walk(next, [...path, id]))
|
||||
|
||||
return walk(definition.id, [])
|
||||
})
|
||||
|
||||
// A renderer that uses the Ipc its own main entry provides depends on no other extension.
|
||||
const edges = definitions.flatMap((consumer) =>
|
||||
ids(consumer.uses).flatMap((token) => {
|
||||
const provider = providerOf(token)
|
||||
|
||||
return provider && provider !== consumer.id ? [{ consumer: consumer.id, provider, token }] : []
|
||||
}),
|
||||
)
|
||||
|
||||
const rows = [{ consumer: "*", provider: "", token: "" }, ...edges]
|
||||
const outcomes = []
|
||||
|
||||
for (const row of rows) {
|
||||
const host = mountExtensions({ definitions, disabled: row.provider ? [row.provider] : [] })
|
||||
host.release()
|
||||
await until(() => host.ready())
|
||||
const consumers = row.consumer === "*" ? definitions.map((definition) => definition.id) : [row.consumer]
|
||||
outcomes.push({
|
||||
...row,
|
||||
results: consumers.map((id) => ({ id, status: host.status(id), failure: host.failure(id)?.error ?? null })),
|
||||
})
|
||||
host.unmount()
|
||||
}
|
||||
|
||||
return { cycles, outcomes }
|
||||
}, modules)
|
||||
|
||||
expect(result.cycles).toEqual([])
|
||||
expect(result.outcomes).toEqual(
|
||||
result.outcomes.map((outcome) => ({
|
||||
...outcome,
|
||||
results: outcome.results.map((item) => ({ id: item.id, status: "active", failure: null })),
|
||||
})),
|
||||
)
|
||||
})
|
||||
Loaded 100 of 953 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user