Compare commits

...

43 Commits

Author SHA1 Message Date
Kit Langton 7e52e16cbe refactor(core): narrow shell spawn handling 2026-08-08 13:19:34 -04:00
Kit Langton 49d68f9a90 refactor(core): hide shell platform failures 2026-08-08 13:10:44 -04:00
Kit Langton 5bd88822c7 fix(core): settle shell spawn failures 2026-08-08 13:02:58 -04:00
opencode-agent[bot] 7afe537c69 fix(ai): align chat assistant content (#41210)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-08-07 23:59:46 -05:00
opencode-agent[bot] e566c082f1 chore: generate 2026-08-08 04:47:57 +00:00
opencode-agent[bot] bc93da4a46 fix(core): reload changed MCP config (#41204)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 23:46:48 -05:00
opencode-agent[bot] 4194d55522 chore: generate 2026-08-08 04:21:03 +00:00
Aiden Cline f0d1029719 Add v2 branch to workflow triggers (#41207) 2026-08-07 23:20:00 -05:00
opencode-agent[bot] b274db88b0 chore(core): format package (#41205)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 23:18:03 -05:00
Kit Langton 9b7b402737 feat(server): run modal sandboxes on the vm runtime (#41177) 2026-08-07 23:21:05 -04:00
James Long 5e6370363b fix(tui): refine provider failure presentation (#41179) 2026-08-07 23:11:32 -04:00
Kit Langton dd6020656e fix(merman): tighten flowchart spacing (#41191) 2026-08-07 23:00:44 -04:00
Kit Langton 8c758e443b fix(core): reuse shared patch diff (#41186) 2026-08-07 22:50:59 -04:00
Kit Langton 0df6aed6ca fix(merman): derive neutral diagram palette (#41181) 2026-08-07 22:43:58 -04:00
Kit Langton aa05fd23b3 refactor(core): remove legacy account runtime schemas (#41173) 2026-08-07 22:40:53 -04:00
Kit Langton fd09760903 fix(tui): open sessions by exact ID (#41180) 2026-08-07 22:29:09 -04:00
Kit Langton 3a4772abb4 fix(merman): separate sibling subgraphs (#41178) 2026-08-07 22:28:12 -04:00
Kit Langton 2cf65c025a fix(merman): support undirected edges and place multiline state labels (#41171) 2026-08-07 21:49:04 -04:00
Kit Langton f3f1204802 feat(tui): render Mermaid diagrams (#41113) 2026-08-07 20:59:50 -04:00
opencode-agent[bot] b3aea1c39d feat(console): add workspace unblock endpoint (#41170)
Co-authored-by: Frank Wang <83515+fwang@users.noreply.github.com>
2026-08-07 20:59:40 -04:00
opencode-agent[bot] 79a493e7f5 fix(tui): show external worktree session labels (#41147)
Co-authored-by: James Long <17031+jlongster@users.noreply.github.com>
2026-08-07 20:52:44 -04:00
Kit Langton 952d4d446f feat(server): add modal environment driver (#41118) 2026-08-07 19:36:55 -04:00
opencode-agent[bot] 0b84e24e65 fix(tui): standardize compact terminology (#41141) 2026-08-07 16:26:14 -04:00
opencode-agent[bot] 3776975d5c fix(tui): unify integration connection copy (#41137) 2026-08-07 16:11:51 -04:00
James Long d2c99ba97c chore: improve incremental typecheck performance (#40925)
Co-authored-by: exe.dev user <exedev@jlongster-site.exe.xyz>
2026-08-07 15:34:36 -04:00
Aiden Cline 6f3a3600b9 fix(ai): forward chat cache keys (#41131) 2026-08-07 14:06:37 -05:00
Aiden Cline 9ca650f97c refactor(ai): promote prompt cache key (#39965) 2026-08-07 13:43:00 -05:00
opencode-agent[bot] db3b54a30d fix(ai): preserve Gemini agent loop parity (#41109)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 12:27:22 -05:00
Kit Langton b4f769f695 fix(core): normalize tool images once (#41097) 2026-08-07 13:02:13 -04:00
Kit Langton e5ef00b8b8 fix(core): bound project filesystem watches (#41096) 2026-08-07 12:38:12 -04:00
Kit Langton 917d6449e3 refactor(core): move exec tools onto environment (#41095) 2026-08-07 11:59:59 -04:00
Kit Langton db31c42e39 refactor(core): move mutation path onto environment (#41091) 2026-08-07 11:42:37 -04:00
Kit Langton c79ced174e fix(core): reload changed skill sources (#40954) 2026-08-07 15:35:23 +00:00
Kit Langton 8ba8af1dd9 refactor(core): move read tool onto environment (#41084) 2026-08-07 11:22:50 -04:00
opencode-agent[bot] 6e82f5d3b9 fix(core): connect custom providers (#40761)
Co-authored-by: Dax Raad <826656+thdxr@users.noreply.github.com>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-07 10:14:14 -05:00
opencode-agent[bot] 48d1a6e5b9 fix(tui): mute expanded thinking content (#41082)
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-08-07 11:12:51 -04:00
opencode-agent[bot] bc47030d4d fix(core): serialize edit and patch transactions (#40641)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-08-07 10:07:48 -05:00
Kit Langton e6d20440f9 test: fix cross-platform unit failures (#41075) 2026-08-07 10:53:39 -04:00
Kit Langton c9cbd2b1f4 feat(core): add local environment driver (#41076) 2026-08-07 10:53:35 -04:00
Aiden Cline 292dfa3036 feat(core): add restart continuation message (#40989) 2026-08-07 09:52:51 -05:00
Kit Langton 3bb0d7fda0 feat(core): add workspace environment foundation (#40967) 2026-08-07 10:28:27 -04:00
opencode-agent[bot] 8977881e09 feat(tui): queue prompts with option enter (#40922)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-08-07 10:22:18 -04:00
Shoubhit Dash e6c9b6bef7 feat(core): add firecrawl web search (#41042) 2026-08-07 16:51:09 +05:30
515 changed files with 30083 additions and 25681 deletions
+1
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- dev - dev
- v2
jobs: jobs:
generate: generate:
-1
View File
@@ -251,4 +251,3 @@ opencode-drive stop --name demo
```bash ```bash
opencode-drive dir --name demo opencode-drive dir --name demo
``` ```
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+57
View File
@@ -395,6 +395,7 @@
"ignore": "7.0.5", "ignore": "7.0.5",
"immer": "11.1.4", "immer": "11.1.4",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"mime-types": "3.0.2",
"tree-sitter-bash": "0.25.0", "tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0", "turndown": "7.2.0",
@@ -570,6 +571,20 @@
"@typescript/native-preview": "catalog:", "@typescript/native-preview": "catalog:",
}, },
}, },
"packages/merman": {
"name": "@opencode-ai/merman",
"version": "0.0.0",
"dependencies": {
"@opencode-ai/plugin": "workspace:*",
"@opentui/core": "catalog:",
"string-width": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.18.8", "version": "1.18.8",
@@ -678,6 +693,7 @@
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
"modal": "0.9.0",
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/bun": "catalog:", "@tsconfig/bun": "catalog:",
@@ -880,6 +896,7 @@
"dependencies": { "dependencies": {
"@opencode-ai/client": "workspace:*", "@opencode-ai/client": "workspace:*",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/merman": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*", "@opencode-ai/schema": "workspace:*",
"@opencode-ai/simulation": "workspace:*", "@opencode-ai/simulation": "workspace:*",
@@ -1511,6 +1528,18 @@
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -1719,6 +1748,10 @@
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="], "@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
@@ -1813,6 +1846,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="], "@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
@@ -2063,6 +2098,8 @@
"@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"], "@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"],
"@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"],
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"], "@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
@@ -3297,6 +3334,8 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"abort-controller-x": ["abort-controller-x@0.5.0", "", {}, "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -3537,6 +3576,10 @@
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
"cbor-x": ["cbor-x@1.6.5", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -4569,6 +4612,8 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
@@ -4795,6 +4840,8 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"modal": ["modal@0.9.0", "", { "dependencies": { "cbor-x": "^1.6.0", "long": "^5.3.1", "nice-grpc": "^2.1.12", "protobufjs": "^7.5.0", "smol-toml": "^1.3.3", "uuid": "^11.1.0" } }, "sha512-kCXcdJkhbJorf/q/6T9Wdlg6in9JmRnCNQnV6rVBMyeqNV/iXI6BYk4IzY4cvZ6dbauNeDMjk/Q08cbxvoIaXg=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="], "motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
@@ -4833,6 +4880,10 @@
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="], "nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nice-grpc": ["nice-grpc@2.1.16", "", { "dependencies": { "@grpc/grpc-js": "^1.14.0", "abort-controller-x": "^0.5.0", "nice-grpc-common": "^2.0.3" } }, "sha512-Cl3Pn00212Hl8/U6bpgMxmhZj5lyv3nWoJov4cd3FjWarktrMHP4DNvSjCnDwkMWYx4W1tyscEia4JX6Y4GVCQ=="],
"nice-grpc-common": ["nice-grpc-common@2.0.3", "", { "dependencies": { "ts-error": "^1.0.6" } }, "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="], "nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -5631,6 +5682,8 @@
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
@@ -6589,6 +6642,8 @@
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], "builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -6733,6 +6788,8 @@
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"modal/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], "motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="], "nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
+4 -3
View File
@@ -368,11 +368,12 @@ Other provider exports listed above remain direct facades until they explicitly
## Provider options & HTTP overlays ## Provider options & HTTP overlays
Three escape hatches in order of stability: Request options in order of stability:
1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop). 1. **`generation`** — portable knobs (`maxTokens`, `temperature`, `topP`, `topK`, penalties, seed, stop).
2. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `promptCacheKey`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing). 2. **`promptCacheKey`** — stable cache affinity lowered by every protocol that supports it.
3. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist. 3. **`providerOptions: { <provider>: {...} }`** — typed-at-the-facade provider-specific knobs (OpenAI `store`, Anthropic `thinking`, Gemini `thinkingConfig`, OpenRouter routing).
4. **`http: { body, headers, query }`** — last-resort serializable overlays merged into the final HTTP request. Reach for this only when a stable typed path doesn't yet exist.
Route/provider defaults are overridden by request-level values for each axis. Route/provider defaults are overridden by request-level values for each axis.
+4 -5
View File
@@ -33,9 +33,10 @@ const model = OpenAI.configure({
// //
// - `generation`: common controls such as max tokens, temperature, topP/topK, // - `generation`: common controls such as max tokens, temperature, topP/topK,
// penalties, seed, and stop sequences. // penalties, seed, and stop sequences.
// - `promptCacheKey`: stable cache affinity for protocols that support it.
// - `providerOptions`: namespaced provider-native behavior. For example, // - `providerOptions`: namespaced provider-native behavior. For example,
// OpenAI cache keys and store behavior, Anthropic thinking, Gemini thinking // OpenAI store behavior, Anthropic thinking, Gemini thinking config, or
// config, or OpenRouter routing/reasoning. // OpenRouter routing/reasoning.
// - `http`: last-resort serializable overlays for final request body, headers, // - `http`: last-resort serializable overlays for final request body, headers,
// and query params. Prefer typed `providerOptions` when a field is stable. // and query params. Prefer typed `providerOptions` when a field is stable.
// //
@@ -45,9 +46,7 @@ const request = LLM.request({
system: "You are concise and practical.", system: "You are concise and practical.",
prompt: "Tell me a joke", prompt: "Tell me a joke",
generation: { maxTokens: 80, temperature: 0.7 }, generation: { maxTokens: 80, temperature: 0.7 },
providerOptions: { promptCacheKey: "tutorial-joke",
openai: { promptCacheKey: "tutorial-joke" },
},
}) })
// 3. `generate` sends the request and collects the event stream into one // 3. `generate` sends the request and collects the event stream into one
+2 -1
View File
@@ -133,7 +133,8 @@ const countHints = (request: LLMRequest) =>
export const applyCachePolicy = (request: LLMRequest): LLMRequest => { export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
return request
const policy = resolve(request.cache) const policy = resolve(request.cache)
if (!policy.tools && !policy.system && !policy.messages) return request if (!policy.tools && !policy.system && !policy.messages) return request
+37 -5
View File
@@ -25,8 +25,20 @@ import { ToolSchemaProjection } from "./utils/tool-schema"
const ADAPTER = "gemini" const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES) const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
// Google documents this sentinel for replaying Gemini 3 function calls after their original signature was lost.
const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
// Gemini 3 rejects replayed function calls without a thought signature. Google's SDKs avoid that in normal chats by
// retaining complete model responses, but OpenCode reconstructs durable history and may encounter an unsigned call
// from an older or external session. Model IDs are open-ended, so unknown Gemini aliases inherit current behavior.
const requiresThoughtSignatureFallback = (modelID: string) => {
if (!/(^|\/)gemini-/i.test(modelID)) return false
if (/(^|\/)gemini-(?:1|2)(?:[.-]|$)/i.test(modelID)) return false
if (/(^|\/)gemini-pro(?:-vision)?$/i.test(modelID)) return false
return !/(^|\/)gemini-robotics-er-1\.5(?:[.-]|$)/i.test(modelID)
}
export interface OptionsInput { export interface OptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
readonly cachedContent?: string readonly cachedContent?: string
@@ -145,6 +157,9 @@ const GeminiGenerationConfig = Schema.Struct({
temperature: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number),
topP: Schema.optional(Schema.Number), topP: Schema.optional(Schema.Number),
topK: Schema.optional(Schema.Number), topK: Schema.optional(Schema.Number),
frequencyPenalty: Schema.optional(Schema.Number),
presencePenalty: Schema.optional(Schema.Number),
seed: Schema.optional(Schema.Number),
stopSequences: optionalArray(Schema.String), stopSequences: optionalArray(Schema.String),
thinkingConfig: Schema.optional(GeminiThinkingConfig), thinkingConfig: Schema.optional(GeminiThinkingConfig),
}) })
@@ -202,11 +217,13 @@ interface ParserState {
// keys on non-object scalars. Mirrors OpenCode's historical Gemini rules. // keys on non-object scalars. Mirrors OpenCode's historical Gemini rules.
// //
// 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect: // 2. Project — lossy mapping from JSON Schema to Gemini's schema dialect:
// drop empty objects, derive `nullable: true` from `type: [..., "null"]`, // drop empty root parameter schemas while preserving nested empty objects,
// coerce `const` to `[const]` enum, recurse properties/items, propagate // expand type arrays into `anyOf`, derive `nullable: true` from null members,
// coerce `const` to `[const]` enum, recurse properties/items, and propagate
// only an allowlisted set of keys (description, required, format, type, // only an allowlisted set of keys (description, required, format, type,
// properties, items, allOf, anyOf, oneOf, minLength). Anything outside the // nullable, enum, properties, items, allOf, anyOf, oneOf, minLength).
// allowlist (e.g. `additionalProperties`, `$ref`) is silently dropped. // Anything outside the allowlist (e.g. `additionalProperties`, `$ref`) is
// silently dropped.
// //
// Sanitize runs first, then project. The implementation lives in // Sanitize runs first, then project. The implementation lives in
// `utils/gemini-tool-schema` so this protocol keeps the same shape as the other // `utils/gemini-tool-schema` so this protocol keeps the same shape as the other
@@ -282,6 +299,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
if (message.role === "assistant") { if (message.role === "assistant") {
const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = [] const parts: Array<Schema.Schema.Type<typeof GeminiContentPart>> = []
// Parallel Gemini 3 calls may carry one signature on the first call; unsigned sibling calls are valid.
let hasSignedToolCall = false
for (const part of message.content) { for (const part of message.content) {
if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"])) if (!ProviderShared.supportsContent(part, ["text", "reasoning", "tool-call"]))
return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"]) return yield* ProviderShared.unsupportedContent("Gemini", "assistant", ["text", "reasoning", "tool-call"])
@@ -294,7 +313,17 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
continue continue
} }
if (part.type === "tool-call") { if (part.type === "tool-call") {
parts.push(lowerToolCall(part)) const lowered = lowerToolCall(part)
const signature = lowered.thoughtSignature
parts.push({
...lowered,
thoughtSignature:
signature ??
(requiresThoughtSignatureFallback(request.model.id) && !hasSignedToolCall
? SKIP_THOUGHT_SIGNATURE_VALIDATOR
: undefined),
})
if (signature !== undefined) hasSignedToolCall = true
continue continue
} }
} }
@@ -388,6 +417,9 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
temperature: generation?.temperature, temperature: generation?.temperature,
topP: generation?.topP, topP: generation?.topP,
topK: generation?.topK, topK: generation?.topK,
frequencyPenalty: generation?.frequencyPenalty,
presencePenalty: generation?.presencePenalty,
seed: generation?.seed,
stopSequences: generation?.stop, stopSequences: generation?.stop,
thinkingConfig: options.thinkingConfig, thinkingConfig: options.thinkingConfig,
} }
+1 -1
View File
@@ -539,7 +539,7 @@ const lowerOptions = (request: LLMRequest) => {
return { return {
...(options.instructions ? { instructions: options.instructions } : {}), ...(options.instructions ? { instructions: options.instructions } : {}),
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}), ...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
...(options.include ? { include: options.include } : {}), ...(options.include ? { include: options.include } : {}),
...(options.reasoningEffort || options.reasoningSummary ...(options.reasoningEffort || options.reasoningSummary
? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } } ? { reasoning: { effort: options.reasoningEffort, summary: options.reasoningSummary } }
+20 -17
View File
@@ -132,6 +132,7 @@ export const bodyFields = {
stream: Schema.Literal(true), stream: Schema.Literal(true),
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })), stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
store: Schema.optional(Schema.Boolean), store: Schema.optional(Schema.Boolean),
prompt_cache_key: Schema.optional(Schema.String),
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
max_completion_tokens: Schema.optional(Schema.Number), max_completion_tokens: Schema.optional(Schema.Number),
max_tokens: Schema.optional(Schema.Number), max_tokens: Schema.optional(Schema.Number),
@@ -370,12 +371,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
return text return text
})() })()
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined) const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined)
const result = { const result = {
role: "assistant" as const, role: "assistant" as const,
content: content.length === 0 ? null : ProviderShared.joinText(content), content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
tool_calls: toolCalls.length === 0 ? undefined : toolCalls, ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
reasoning_details: details, ...(details !== undefined ? { reasoning_details: details } : {}),
cache_control: options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined), ...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
} }
if (field === undefined || reasoningText === undefined) return result if (field === undefined || reasoningText === undefined) return result
return { ...result, [field]: reasoningText } return { ...result, [field]: reasoningText }
@@ -509,6 +511,7 @@ const lowerOptions = (request: LLMRequest) => {
const options = OpenAIOptions.resolve(request) const options = OpenAIOptions.resolve(request)
return { return {
...(options.store !== undefined ? { store: options.store } : {}), ...(options.store !== undefined ? { store: options.store } : {}),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}), ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
} }
} }
@@ -707,23 +710,20 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
Boolean(delta?.content) || Boolean(delta?.content) ||
reasoning !== undefined || reasoning !== undefined ||
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) || (Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
toolDeltas.some( toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
(tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments),
)
if (state.finishReason !== undefined) { if (state.finishReason !== undefined) {
if (hasLateContent) if (hasLateContent)
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason") return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
return [{ ...state, usage }, events] as const return [{ ...state, usage }, events] as const
} }
const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined) const reasoningField = state.reasoningField ?? reasoning?.field
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta) if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
const deltaMetadata = reasoningMetadata(reasoningField) const deltaMetadata = reasoningMetadata(reasoningField)
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
if (!state.lifecycle.text.has("text-0") && text !== undefined) if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
else if ( else if (
reasoningDetailsObserved && reasoningDetailsObserved &&
!lifecycle.reasoning.has("reasoning-0") && !lifecycle.reasoning.has("reasoning-0") &&
@@ -749,8 +749,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position) const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
const fallbackTool = tools[fallback] ?? pendingTools[fallback] const fallbackTool = tools[fallback] ?? pendingTools[fallback]
const index = const index =
tool.index ?? matched ?? tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
(tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
const current = tools[index] const current = tools[index]
const pending = pendingTools[index] const pending = pendingTools[index]
const id = current?.id ?? pending?.id ?? (tool.id || undefined) const id = current?.id ?? pending?.id ?? (tool.id || undefined)
@@ -813,14 +812,18 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => { const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
const events: LLMEvent[] = [] const events: LLMEvent[] = []
const hasToolCalls = state.toolCallEvents.length > 0 const toolCallEvents =
state.finishReason === undefined && Object.keys(state.tools).length > 0
? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
: state.toolCallEvents
const hasToolCalls = toolCallEvents.length > 0
const reason = state.finishReason const reason = state.finishReason
? { ? {
...state.finishReason, ...state.finishReason,
normalized: normalized:
state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized, state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
} }
: undefined : { normalized: hasToolCalls ? ("tool-calls" as const) : ("unknown" as const) }
const metadata = reasoningMetadata( const metadata = reasoningMetadata(
state.reasoningField, state.reasoningField,
state.reasoningDetailsObserved ? state.reasoningDetails : undefined, state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
@@ -830,9 +833,9 @@ const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField)) ? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
: state.lifecycle : state.lifecycle
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata) const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
events.push(...state.toolCallEvents) events.push(...toolCallEvents)
if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage }) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
return events return events
} }
@@ -61,37 +61,57 @@ const emptyObjectSchema = (schema: Record<string, unknown>) =>
(!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) && (!isRecord(schema.properties) || Object.keys(schema.properties).length === 0) &&
!schema.additionalProperties !schema.additionalProperties
const projectNode = (schema: unknown): Record<string, unknown> | undefined => { const projectNode = (schema: unknown, nested = false): Record<string, unknown> | undefined => {
if (!isRecord(schema)) return undefined if (!isRecord(schema)) return undefined
if (emptyObjectSchema(schema)) return undefined if (!nested && emptyObjectSchema(schema)) return undefined
return Object.fromEntries( const types = Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null") : undefined
const anyOf = Array.isArray(schema.anyOf) ? schema.anyOf : undefined
const hasNullAnyOf = anyOf?.some((item) => isRecord(item) && item.type === "null") ?? false
const anyOfTypes = hasNullAnyOf ? anyOf?.filter((item) => !isRecord(item) || item.type !== "null") : anyOf
const flattenedAnyOf = hasNullAnyOf && anyOfTypes?.length === 1 ? projectNode(anyOfTypes[0], true) : undefined
const result = Object.fromEntries(
[ [
["description", schema.description], ["description", schema.description],
["required", schema.required], ["required", schema.required],
["format", schema.format], ["format", schema.format],
["type", Array.isArray(schema.type) ? schema.type.filter((type) => type !== "null")[0] : schema.type], ["type", types ? (types.length === 0 ? "null" : undefined) : schema.type],
["nullable", Array.isArray(schema.type) && schema.type.includes("null") ? true : undefined], [
"nullable",
(Array.isArray(schema.type) && schema.type.includes("null") && types && types.length > 0) || hasNullAnyOf
? true
: undefined,
],
["enum", schema.const !== undefined ? [schema.const] : schema.enum], ["enum", schema.const !== undefined ? [schema.const] : schema.enum],
[ [
"properties", "properties",
isRecord(schema.properties) isRecord(schema.properties)
? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value)])) ? Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [key, projectNode(value, true)]))
: undefined, : undefined,
], ],
[ [
"items", "items",
Array.isArray(schema.items) Array.isArray(schema.items)
? schema.items.map(projectNode) ? schema.items.map((item) => projectNode(item, true))
: schema.items === undefined : schema.items === undefined
? undefined ? undefined
: projectNode(schema.items), : projectNode(schema.items, true),
], ],
["allOf", Array.isArray(schema.allOf) ? schema.allOf.map(projectNode) : undefined], ["allOf", Array.isArray(schema.allOf) ? schema.allOf.map((item) => projectNode(item, true)) : undefined],
["anyOf", Array.isArray(schema.anyOf) ? schema.anyOf.map(projectNode) : undefined], [
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map(projectNode) : undefined], "anyOf",
anyOfTypes
? hasNullAnyOf && anyOfTypes.length === 1
? undefined
: anyOfTypes.map((item) => projectNode(item, true))
: types && types.length > 0
? types.map((type) => ({ type }))
: undefined,
],
["oneOf", Array.isArray(schema.oneOf) ? schema.oneOf.map((item) => projectNode(item, true)) : undefined],
["minLength", schema.minLength], ["minLength", schema.minLength],
].filter((entry) => entry[1] !== undefined), ].filter((entry) => entry[1] !== undefined),
) )
return flattenedAnyOf ? { ...result, ...flattenedAnyOf } : result
} }
export const convert = (schema: unknown) => projectNode(sanitizeNode(schema)) export const convert = (schema: unknown) => projectNode(sanitizeNode(schema))
@@ -33,7 +33,6 @@ export const ServiceTierSchema = Schema.Literals(ServiceTiers)
export interface Resolved { export interface Resolved {
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: string readonly reasoningEffort?: string
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -50,7 +49,6 @@ export const resolve = (request: LLMRequest): Resolved => {
return { return {
instructions: typeof input?.instructions === "string" ? input.instructions : undefined, instructions: typeof input?.instructions === "string" ? input.instructions : undefined,
store: typeof input?.store === "boolean" ? input.store : undefined, store: typeof input?.store === "boolean" ? input.store : undefined,
promptCacheKey: typeof input?.promptCacheKey === "string" ? input.promptCacheKey : undefined,
reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined, reasoningEffort: typeof input?.reasoningEffort === "string" ? input.reasoningEffort : undefined,
reasoningSummary: reasoningSummary:
reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed" reasoningSummary === "auto" || reasoningSummary === "concise" || reasoningSummary === "detailed"
@@ -5,7 +5,6 @@ export interface OpenResponsesOptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
readonly instructions?: string readonly instructions?: string
readonly store?: boolean readonly store?: boolean
readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" | "concise" | "detailed" readonly reasoningSummary?: "auto" | "concise" | "detailed"
readonly include?: ReadonlyArray<ResponseIncludable> readonly include?: ReadonlyArray<ResponseIncludable>
@@ -17,7 +17,6 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
const openai = Object.fromEntries( const openai = Object.fromEntries(
definedEntries({ definedEntries({
store: options?.store, store: options?.store,
promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort, reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary, reasoningSummary: options?.reasoningSummary,
include: options?.include, include: options?.include,
+1 -2
View File
@@ -55,7 +55,6 @@ export interface OpenRouterOptions {
readonly debug?: Readonly<{ echo_upstream_body?: boolean }> readonly debug?: Readonly<{ echo_upstream_body?: boolean }>
readonly models?: ReadonlyArray<string> readonly models?: ReadonlyArray<string>
readonly plugins?: ReadonlyArray<OpenRouterPlugin> readonly plugins?: ReadonlyArray<OpenRouterPlugin>
readonly promptCacheKey?: string
readonly provider?: OpenRouterProviderRouting readonly provider?: OpenRouterProviderRouting
readonly reasoning?: Readonly<{ readonly reasoning?: Readonly<{
enabled?: boolean enabled?: boolean
@@ -122,6 +121,7 @@ export const protocol = Protocol.make({
...body, ...body,
messages, messages,
...bodyOptions(request.providerOptions?.openrouter), ...bodyOptions(request.providerOptions?.openrouter),
...(request.promptCacheKey ? { prompt_cache_key: request.promptCacheKey } : {}),
} as OpenRouterBody } as OpenRouterBody
}), }),
), ),
@@ -161,7 +161,6 @@ const bodyOptions = (input: unknown) => {
...(isRecord(debug) ? { debug } : {}), ...(isRecord(debug) ? { debug } : {}),
...(typeof user === "string" ? { user } : {}), ...(typeof user === "string" ? { user } : {}),
...(isRecord(reasoning) ? { reasoning } : {}), ...(isRecord(reasoning) ? { reasoning } : {}),
...(typeof promptCacheKey === "string" ? { prompt_cache_key: promptCacheKey } : {}),
} }
} }
+2
View File
@@ -47,6 +47,8 @@ const chatRoute = Route.make({
protocol: OpenAIChat.protocol, protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }), endpoint: Endpoint.path("/chat/completions", { baseURL: OpenAICompatibleProfiles.profiles.xai.baseURL }),
transport: OpenAICompatibleChat.route.transport, transport: OpenAICompatibleChat.route.transport,
headers: ({ request }): Record<string, string> =>
request.promptCacheKey ? { "x-grok-conv-id": request.promptCacheKey } : {},
}) })
export const routes = [responsesRoute, chatRoute] export const routes = [responsesRoute, chatRoute]
+1 -7
View File
@@ -255,13 +255,7 @@ const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent,
if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true
return Effect.succeed(event) return Effect.succeed(event)
}), }),
Stream.onEnd( Stream.onEnd(Effect.suspend(() => (terminal ? Effect.void : Effect.fail(incompleteStreamError(route))))),
Effect.suspend(() =>
terminal
? Effect.void
: Effect.fail(incompleteStreamError(route)),
),
),
) )
}) })
+14 -2
View File
@@ -1,4 +1,4 @@
import { Cause, Context, Effect, Layer } from "effect" import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
import { import {
FetchHttpClient, FetchHttpClient,
Headers, Headers,
@@ -198,8 +198,20 @@ const responseBody = (body: string | void, request: HttpClientRequest.HttpClient
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true } return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
} }
const decodeProviderBody = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({
message: Schema.optionalKey(Schema.String),
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
}),
),
)
const providerMessage = (status: number, body: { readonly body?: string }) => { const providerMessage = (status: number, body: { readonly body?: string }) => {
if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}` if (body.body && body.body.length <= 500) {
const decoded = Option.getOrUndefined(decodeProviderBody(body.body))
return `Provider request failed with HTTP ${status}: ${decoded?.error?.message ?? decoded?.message ?? body.body}`
}
return `Provider request failed with HTTP ${status}` return `Provider request failed with HTTP ${status}`
} }
+3
View File
@@ -272,6 +272,8 @@ export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
providerOptions: Schema.optional(ProviderOptions), providerOptions: Schema.optional(ProviderOptions),
http: Schema.optional(HttpOptions), http: Schema.optional(HttpOptions),
cache: Schema.optional(CachePolicy), cache: Schema.optional(CachePolicy),
// Stable cache affinity for protocols that support provider-managed prompt caching.
promptCacheKey: Schema.optional(Schema.String),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}) {} }) {}
@@ -289,6 +291,7 @@ export namespace LLMRequest {
providerOptions: request.providerOptions, providerOptions: request.providerOptions,
http: request.http, http: request.http,
cache: request.cache, cache: request.cache,
promptCacheKey: request.promptCacheKey,
metadata: request.metadata, metadata: request.metadata,
}) })
+2 -7
View File
@@ -1,10 +1,6 @@
import { Effect, JsonSchema, Schema } from "effect" import { Effect, JsonSchema, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool" import { Tool } from "@opencode-ai/schema/tool"
import type { import type { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
ToolCallPart,
ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType,
} from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema" import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
/** /**
@@ -244,8 +240,7 @@ const project = (
): ToolOutputType => ): ToolOutputType =>
ToolOutput.make( ToolOutput.make(
toStructuredOutput?.(output) ?? output, toStructuredOutput?.(output) ?? output,
toModelOutput?.({ id, parameters, output }) ?? toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []),
(typeof output === "string" ? [{ type: "text", text: output }] : []),
) )
export { ToolFailure } export { ToolFailure }
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:anthropic-messages-cache", "provider:anthropic", "protocol:anthropic-messages", "cache", "tool"],
"prefix:anthropic-messages-cache",
"provider:anthropic",
"protocol:anthropic-messages",
"cache",
"tool"
],
"name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback", "name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback",
"recordedAt": "2026-07-24T16:22:29.494Z" "recordedAt": "2026-07-24T16:22:29.494Z"
}, },
@@ -1,12 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:bedrock-converse-cache", "provider:amazon-bedrock", "protocol:bedrock-converse", "cache"],
"prefix:bedrock-converse-cache",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"cache"
],
"name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call", "name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call",
"recordedAt": "2026-07-23T02:29:10.955Z" "recordedAt": "2026-07-23T02:29:10.955Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:google-images", "provider:google", "protocol:google-images"],
"prefix:google-images",
"provider:google",
"protocol:google-images"
],
"name": "google-images/generates-an-image", "name": "google-images/generates-an-image",
"recordedAt": "2026-07-19T16:05:51.868Z" "recordedAt": "2026-07-19T16:05:51.868Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:openai-images", "provider:openai", "protocol:openai-images"],
"prefix:openai-images",
"provider:openai",
"protocol:openai-images"
],
"name": "openai-images/generates-an-image", "name": "openai-images/generates-an-image",
"recordedAt": "2026-07-19T14:41:43.188Z" "recordedAt": "2026-07-19T14:41:43.188Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:openai-responses-images", "provider:openai", "protocol:openai-responses"],
"prefix:openai-responses-images",
"provider:openai",
"protocol:openai-responses"
],
"name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool", "name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool",
"recordedAt": "2026-07-19T14:57:16.284Z" "recordedAt": "2026-07-19T14:57:16.284Z"
}, },
@@ -2,12 +2,7 @@
"version": 1, "version": 1,
"metadata": { "metadata": {
"model": "anthropic/claude-sonnet-4.6", "model": "anthropic/claude-sonnet-4.6",
"tags": [ "tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
"prefix:openai-compatible-chat",
"provider:openrouter",
"protocol:openai-chat",
"reasoning"
],
"name": "openrouter-reasoning", "name": "openrouter-reasoning",
"recordedAt": "2026-07-18T11:28:39.267Z" "recordedAt": "2026-07-18T11:28:39.267Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"tool",
"tool-result"
],
"name": "pdf/anthropic-tool-result", "name": "pdf/anthropic-tool-result",
"recordedAt": "2026-07-22T18:15:39.002Z" "recordedAt": "2026-07-22T18:15:39.002Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "user-input"],
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"user-input"
],
"name": "pdf/anthropic-user-input", "name": "pdf/anthropic-user-input",
"recordedAt": "2026-07-22T18:15:37.979Z" "recordedAt": "2026-07-22T18:15:37.979Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"tool",
"tool-result"
],
"name": "pdf/bedrock-tool-result", "name": "pdf/bedrock-tool-result",
"recordedAt": "2026-07-22T18:15:52.400Z" "recordedAt": "2026-07-22T18:15:52.400Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "user-input"],
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"user-input"
],
"name": "pdf/bedrock-user-input", "name": "pdf/bedrock-user-input",
"recordedAt": "2026-07-22T18:15:48.408Z" "recordedAt": "2026-07-22T18:15:48.408Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"tool",
"tool-result"
],
"name": "pdf/gemini-tool-result", "name": "pdf/gemini-tool-result",
"recordedAt": "2026-07-22T18:21:59.606Z" "recordedAt": "2026-07-22T18:21:59.606Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "user-input"],
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"user-input"
],
"name": "pdf/gemini-user-input", "name": "pdf/gemini-user-input",
"recordedAt": "2026-07-22T18:20:55.140Z" "recordedAt": "2026-07-22T18:20:55.140Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/openai-tool-result", "name": "pdf/openai-tool-result",
"recordedAt": "2026-07-22T18:15:36.438Z" "recordedAt": "2026-07-22T18:15:36.438Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "user-input"],
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/openai-user-input", "name": "pdf/openai-user-input",
"recordedAt": "2026-07-22T18:15:34.867Z" "recordedAt": "2026-07-22T18:15:34.867Z"
}, },
@@ -1,14 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "tool", "tool-result"],
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/xai-tool-result", "name": "pdf/xai-tool-result",
"recordedAt": "2026-07-22T18:15:43.608Z" "recordedAt": "2026-07-22T18:15:43.608Z"
}, },
@@ -1,13 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "user-input"],
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/xai-user-input", "name": "pdf/xai-user-input",
"recordedAt": "2026-07-22T18:15:42.429Z" "recordedAt": "2026-07-22T18:15:42.429Z"
}, },
@@ -2,12 +2,7 @@
"version": 1, "version": 1,
"metadata": { "metadata": {
"model": "anthropic/claude-sonnet-4.6", "model": "anthropic/claude-sonnet-4.6",
"tags": [ "tags": ["prefix:openai-compatible-chat", "provider:vercel-ai-gateway", "protocol:openai-chat", "reasoning"],
"prefix:openai-compatible-chat",
"provider:vercel-ai-gateway",
"protocol:openai-chat",
"reasoning"
],
"name": "vercel-ai-gateway-reasoning", "name": "vercel-ai-gateway-reasoning",
"recordedAt": "2026-07-18T11:28:42.077Z" "recordedAt": "2026-07-18T11:28:42.077Z"
}, },
@@ -1,11 +1,7 @@
{ {
"version": 1, "version": 1,
"metadata": { "metadata": {
"tags": [ "tags": ["prefix:xai-images", "provider:xai", "protocol:xai-images"],
"prefix:xai-images",
"provider:xai",
"protocol:xai-images"
],
"name": "xai-images/generates-an-image", "name": "xai-images/generates-an-image",
"recordedAt": "2026-07-19T15:56:20.098Z" "recordedAt": "2026-07-19T15:56:20.098Z"
}, },
@@ -3,11 +3,11 @@ import { CloudflareWorkersAI } from "../../src/providers"
const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model") const model = CloudflareWorkersAI.configure({ accountId: "account", apiKey: "test" }).model("model")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { promptCacheKey: "cache" } } }) LLM.request({ model, prompt: "Hello", promptCacheKey: "cache" })
LLM.request({ LLM.request({
model, model,
prompt: "Hello", prompt: "Hello",
// @ts-expect-error Cloudflare's OpenAI-compatible prompt cache key must be a string. // @ts-expect-error Prompt cache keys must be strings.
providerOptions: { openai: { promptCacheKey: 1 } }, promptCacheKey: 1,
}) })
@@ -519,14 +519,8 @@ describe("Bedrock Converse route", () => {
fixedBytes( fixedBytes(
eventStreamBody( eventStreamBody(
["messageStart", { role: "assistant" }], ["messageStart", { role: "assistant" }],
[ ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }],
"contentBlockDelta", ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
{ contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } },
],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["messageStop", { stopReason: "end_turn" }], ["messageStop", { stopReason: "end_turn" }],
), ),
), ),
@@ -561,10 +555,7 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const body = eventStreamBody( const body = eventStreamBody(
["messageStart", { role: "assistant" }], ["messageStart", { role: "assistant" }],
[ ["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["contentBlockStop", { contentBlockIndex: 0 }], ["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "end_turn" }], ["messageStop", { stopReason: "end_turn" }],
) )
+172
View File
@@ -16,6 +16,13 @@ const model = Gemini.route
}) })
.model({ id: "gemini-2.5-flash" }) .model({ id: "gemini-2.5-flash" })
const gemini3 = Gemini.route
.with({
endpoint: { baseURL: "https://generativelanguage.test/v1beta/" },
auth: Auth.header("x-goog-api-key", "test"),
})
.model({ id: "gemini-3-flash-preview" })
const request = LLM.request({ const request = LLM.request({
id: "req_1", id: "req_1",
model, model,
@@ -86,6 +93,39 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("forwards standard Gemini generation options", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Say hello.",
generation: {
maxTokens: 40,
temperature: 0.2,
topP: 0.8,
topK: 12,
frequencyPenalty: 0.3,
presencePenalty: 0.4,
seed: 42,
stop: ["done"],
},
}),
)
expect(prepared.body.generationConfig).toEqual({
maxOutputTokens: 40,
temperature: 0.2,
topP: 0.8,
topK: 12,
frequencyPenalty: 0.3,
presencePenalty: 0.4,
seed: 42,
stopSequences: ["done"],
thinkingConfig: undefined,
})
}),
)
it.effect("lowers chronological system updates to wrapped user text in order", () => it.effect("lowers chronological system updates to wrapped user text in order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -350,6 +390,100 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("preserves nested empty object tool schemas", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Use the tool.",
tools: [
{
name: "configure",
description: "Configure the operation",
inputSchema: {
type: "object",
required: ["options"],
properties: {
options: { type: "object", description: "Optional provider settings", properties: {} },
},
},
},
],
}),
)
expect(prepared.body.tools).toEqual([
{
functionDeclarations: [
{
name: "configure",
description: "Configure the operation",
parameters: {
type: "object",
required: ["options"],
properties: {
options: { type: "object", description: "Optional provider settings", properties: {} },
},
},
},
],
},
])
}),
)
it.effect("projects Gemini type arrays without narrowing their allowed values", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
prompt: "Use the tool.",
tools: [
{
name: "filter",
description: "Filter values",
inputSchema: {
type: "object",
properties: {
status: { type: ["number", "string"], description: "Status filter" },
maybe: { type: ["string", "null"] },
nothing: { type: ["null"] },
explicit: { anyOf: [{ type: "string" }, { type: "null" }] },
choice: { anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
},
},
},
],
}),
)
expect(prepared.body.tools?.[0]?.functionDeclarations[0]?.parameters).toEqual({
type: "object",
properties: {
status: {
description: "Status filter",
anyOf: [{ type: "number" }, { type: "string" }],
},
maybe: {
nullable: true,
anyOf: [{ type: "string" }],
},
nothing: {
type: "null",
},
explicit: {
type: "string",
nullable: true,
},
choice: {
anyOf: [{ type: "string" }, { type: "number" }],
nullable: true,
},
},
})
}),
)
it.effect("parses text, reasoning, and usage stream fixtures", () => it.effect("parses text, reasoning, and usage stream fixtures", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents( const body = sseEvents(
@@ -536,6 +670,44 @@ describe("Gemini route", () => {
}), }),
) )
it.effect("replays unsigned Gemini 3 tool calls with the validator bypass sentinel", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: gemini3,
messages: [
Message.assistant([ToolCallPart.make({ id: "tool_0", name: "lookup", input: { query: "weather" } })]),
Message.tool({ id: "tool_0", name: "lookup", result: "done", resultType: "text" }),
],
}),
)
expect(prepared.body.contents).toEqual([
{
role: "model",
parts: [
{
functionCall: { id: undefined, name: "lookup", args: { query: "weather" } },
thoughtSignature: "skip_thought_signature_validator",
},
],
},
{
role: "user",
parts: [
{
functionResponse: {
id: undefined,
name: "lookup",
response: { name: "lookup", content: "done" },
},
},
],
},
])
}),
)
it.effect("emits streamed tool calls and maps finish reason", () => it.effect("emits streamed tool calls and maps finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents({ const body = sseEvents({
+91 -26
View File
@@ -15,6 +15,8 @@ import {
} from "../../src" } from "../../src"
import * as Azure from "../../src/providers/azure" import * as Azure from "../../src/providers/azure"
import * as OpenAI from "../../src/providers/openai" import * as OpenAI from "../../src/providers/openai"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as XAI from "../../src/providers/xai"
import * as OpenAIChat from "../../src/protocols/openai-chat" import * as OpenAIChat from "../../src/protocols/openai-chat"
import { ProviderShared } from "../../src/protocols/shared" import { ProviderShared } from "../../src/protocols/shared"
import { Auth, LLMClient } from "../../src/route" import { Auth, LLMClient } from "../../src/route"
@@ -100,6 +102,24 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("concatenates assistant text parts without adding separators", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "Hello" },
{ type: "text", text: " world" },
]),
],
}),
)
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "Hello world" }])
}),
)
it.effect("writes reasoning to a configured custom field on every assistant message", () => it.effect("writes reasoning to a configured custom field on every assistant message", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -154,6 +174,47 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("maps the request prompt cache key", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAICompatible.configure({
baseURL: "https://api.compatible.test/v1",
apiKey: "test",
}).model("compatible-model"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
)
expect(prepared.body.prompt_cache_key).toBe("session_123")
}),
)
it.effect("maps the xAI Chat prompt cache key to conversation affinity", () =>
LLMClient.generate(
LLM.request({
model: XAI.configure({ apiKey: "test", baseURL: "https://api.x.ai/v1" }).chat("grok-4.5"),
prompt: "Hello",
promptCacheKey: "session_123",
}),
).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const web = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
expect(web.headers.get("x-grok-conv-id")).toBe("session_123")
const body = decodeJson(yield* Effect.promise(() => web.text()))
expect(ProviderShared.isRecord(body) ? body.prompt_cache_key : undefined).toBe("session_123")
return input.respond(sseEvents(deltaChunk({}, "stop")), {
headers: { "content-type": "text/event-stream" },
})
}),
),
),
),
)
it.effect("passes through custom OpenAI-compatible reasoning effort strings", () => it.effect("passes through custom OpenAI-compatible reasoning effort strings", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
@@ -535,7 +596,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }]) expect(prepared.body.messages).toEqual([{ role: "assistant", content: "", reasoning_content: "hidden" }])
}), }),
) )
@@ -784,7 +845,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("ignores scalar reasoning after content starts", () => it.effect("preserves scalar reasoning after content starts", () =>
Effect.gen(function* () { Effect.gen(function* () {
const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }] const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }]
const response = yield* LLMClient.generate(request).pipe( const response = yield* LLMClient.generate(request).pipe(
@@ -800,11 +861,11 @@ describe("OpenAI Chat route", () => {
), ),
) )
expect(response.reasoning).toBe("detail") expect(response.reasoning).toBe("detailscalar")
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(2)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(2)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({ expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningDetails: details }, openai: { reasoningField: "reasoning", reasoningDetails: details },
}) })
}), }),
) )
@@ -904,7 +965,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1) expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] })) const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }]) expect(replay.body.messages).toEqual([{ role: "assistant", content: "", reasoning_details: details }])
}), }),
) )
@@ -954,7 +1015,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning: "firstsecond", reasoning_details: [first, second] }, { role: "assistant", content: "", reasoning: "firstsecond", reasoning_details: [first, second] },
]) ])
}), }),
) )
@@ -979,7 +1040,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning_content: "AB", reasoning_details: [detail] }, { role: "assistant", content: "", reasoning_content: "AB", reasoning_details: [detail] },
]) ])
}), }),
) )
@@ -1001,7 +1062,7 @@ describe("OpenAI Chat route", () => {
) )
expect(replay.body.messages).toEqual([ expect(replay.body.messages).toEqual([
{ role: "assistant", content: null, reasoning_content: "thinking", reasoning_details: details }, { role: "assistant", content: "", reasoning_content: "thinking", reasoning_details: details },
]) ])
}), }),
) )
@@ -1109,7 +1170,7 @@ describe("OpenAI Chat route", () => {
}), }),
) )
it.effect("fails a streamed tool call when the provider ends without a finish reason", () => it.effect("finalizes a streamed tool call when the provider ends without a finish reason", () =>
Effect.gen(function* () { Effect.gen(function* () {
const body = sseEvents( const body = sseEvents(
deltaChunk({ deltaChunk({
@@ -1121,27 +1182,31 @@ describe("OpenAI Chat route", () => {
const input = LLMRequest.update(request, { const input = LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })], tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
}) })
const events: LLMEvent[] = [] const response = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)))
const streamError = yield* LLMClient.stream(input).pipe(
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
Effect.flip,
Effect.provide(fixedResponse(body)),
)
const error = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
expect(events).toEqual([ expect(response.events).toEqual([
{ type: "step-start", index: 0 }, { type: "step-start", index: 0 },
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined }, { type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' }, { type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' }, { type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' },
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata: undefined },
{
type: "tool-call",
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: undefined,
},
{
type: "step-finish",
index: 0,
reason: { normalized: "tool-calls" },
usage: undefined,
providerMetadata: undefined,
},
{ type: "finish", reason: { normalized: "tool-calls" }, usage: undefined },
]) ])
expect(events.filter(LLMEvent.is.toolCall)).toEqual([])
expect(streamError.reason).toMatchObject({
_tag: "InvalidProviderOutput",
classification: "incomplete-stream",
})
expect(streamError.message).toContain("The provider response ended unexpectedly.")
expect(error.message).toContain("The provider response ended unexpectedly.")
}), }),
) )
@@ -300,7 +300,9 @@ describe("OpenAI-compatible Chat route", () => {
Effect.gen(function* () { Effect.gen(function* () {
const response = yield* LLMClient.generate( const response = yield* LLMClient.generate(
LLMRequest.update(request, { LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } })], tools: [
ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } }),
],
}), }),
).pipe( ).pipe(
Effect.provide( Effect.provide(
@@ -20,7 +20,7 @@ const cacheRequest = LLM.request({
system: LARGE_CACHEABLE_SYSTEM, system: LARGE_CACHEABLE_SYSTEM,
prompt: "Say hi.", prompt: "Say hi.",
generation: { maxTokens: 16, temperature: 0 }, generation: { maxTokens: 16, temperature: 0 },
providerOptions: { openai: { promptCacheKey: "recorded-cache-test" } }, promptCacheKey: "recorded-cache-test",
}) })
const recorded = recordedTests({ const recorded = recordedTests({
@@ -682,9 +682,9 @@ describe("OpenAI Responses route", () => {
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "think", prompt: "think",
promptCacheKey: "session_123",
providerOptions: { providerOptions: {
openai: { openai: {
promptCacheKey: "session_123",
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: ["reasoning.encrypted_content"],
@@ -803,17 +803,16 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("request OpenAI provider options override route defaults", () => it.effect("maps the request prompt cache key", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* compileRequest( const prepared = yield* compileRequest(
LLM.request({ LLM.request({
model: OpenAI.configure({ model: OpenAI.configure({
baseURL: "https://api.openai.test/v1/", baseURL: "https://api.openai.test/v1/",
apiKey: "test", apiKey: "test",
providerOptions: { openai: { promptCacheKey: "model_cache" } },
}).model("gpt-4.1-mini"), }).model("gpt-4.1-mini"),
prompt: "no cache", prompt: "no cache",
providerOptions: { openai: { promptCacheKey: "request_cache" } }, promptCacheKey: "request_cache",
}), }),
) )
+1 -1
View File
@@ -162,7 +162,6 @@ describe("OpenRouter", () => {
openrouter: { openrouter: {
usage: true, usage: true,
reasoning: { effort: "high" }, reasoning: { effort: "high" },
promptCacheKey: "session_123",
models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"], models: ["anthropic/claude-sonnet-4.6", "google/gemini-3.1-pro"],
provider: { order: ["anthropic", "google"], require_parameters: true }, provider: { order: ["anthropic", "google"], require_parameters: true },
plugins: [{ id: "response-healing" }], plugins: [{ id: "response-healing" }],
@@ -174,6 +173,7 @@ describe("OpenRouter", () => {
}, },
}).model("anthropic/claude-3.7-sonnet:thinking"), }).model("anthropic/claude-3.7-sonnet:thinking"),
prompt: "Think briefly.", prompt: "Think briefly.",
promptCacheKey: "session_123",
}), }),
) )
@@ -85,8 +85,7 @@ async function mockServers(page: Page, requests: string[]) {
const current = url.origin === serverA ? sessionA : sessionB const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/health") return json(route, { pid: 1 }) if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
@@ -181,12 +181,17 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
return json(route, true) return json(route, true)
} }
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/provider") if (url.pathname === "/api/provider")
return json(route, { return json(route, {
location: { directory }, location: { directory },
data: [{ id: remote ? "server-b" : "server-a", name: remote ? "Server B Provider" : "Server A Provider", package: "test" }], data: [
{
id: remote ? "server-b" : "server-a",
name: remote ? "Server B Provider" : "Server A Provider",
package: "test",
},
],
}) })
if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] }) if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] })
if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) }) if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) })
@@ -58,8 +58,7 @@ async function mockServers(page: Page) {
const current = url.origin === serverA ? sessionA : sessionB const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory") const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route, url.pathname === "/api/event")
return sse(route, url.pathname === "/api/event")
if (url.pathname === "/api/health") return json(route, { pid: 1 }) if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session/active") if (url.pathname === "/api/session/active")
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
@@ -86,15 +86,10 @@ test.describe("session timeline projection", () => {
], ],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } }, { summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
) )
const aborted = assistantMessage( const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
[
{ id: "prt_before_abort", type: "text", text: "Before interruption" },
],
{
id: "msg_1001_assistant_aborted", id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } }, error: { name: "MessageAbortedError", data: { message: "Stopped" } },
}, })
)
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], { const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
id: "msg_1002_assistant_failed", id: "msg_1002_assistant_failed",
error: { error: {
@@ -89,8 +89,7 @@ async function mockServer(page: Page) {
if (url.origin !== server) return route.fallback() if (url.origin !== server) return route.fallback()
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
return new Promise(() => {}) return new Promise(() => {})
if (url.pathname === "/api/event") if (url.pathname === "/api/event") return sse(route)
return sse(route)
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} }) if (url.pathname === "/api/session/active") return json(route, { data: {} })
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
@@ -300,9 +300,8 @@ export const fixture = {
.filter((message) => message.info.role === "user") .filter((message) => message.info.role === "user")
.map((message) => message.info.id), .map((message) => message.info.id),
targetPartIDs: targetMessages.flatMap(currentPartIDs), targetPartIDs: targetMessages.flatMap(currentPartIDs),
expandedShellPartID: targetMessages expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!
.flatMap((message) => message.parts) .callID,
.find((part) => part.tool === "bash")!.callID,
}, },
} }
+21 -19
View File
@@ -144,7 +144,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
location: location(config), location: location(config),
data: currentProviders(providerConfig(config)), data: currentProviders(providerConfig(config)),
}) })
if (path === "/api/model") return json(route, { location: location(config), data: currentModels(providerConfig(config)) }) if (path === "/api/model")
return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
if (path === "/api/model/default") if (path === "/api/model/default")
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) }) return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
if (path === "/api/integration") return json(route, { location: location(config), data: [] }) if (path === "/api/integration") return json(route, { location: location(config), data: [] })
@@ -206,7 +207,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1] const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1]
if (fileRead && config.fileContent) { if (fileRead && config.fileContent) {
const value = await config.fileContent(decodeURIComponent(fileRead)) const value = await config.fileContent(decodeURIComponent(fileRead))
const content = value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "") const content =
value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } }) return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } })
} }
if (path === "/api/fs/find" && config.findFiles) { if (path === "/api/fs/find" && config.findFiles) {
@@ -281,7 +283,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
} }
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") return json(route, true) if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST")
return json(route, true)
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
return json(route, true) return json(route, true)
if ( if (
@@ -386,7 +389,9 @@ function providerConfig(config: MockServerConfig) {
function currentProviders(value: unknown) { function currentProviders(value: unknown) {
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : [] if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
return value.all.filter(record).flatMap((provider) => return value.all
.filter(record)
.flatMap((provider) =>
typeof provider.id === "string" && typeof provider.name === "string" typeof provider.id === "string" && typeof provider.name === "string"
? [{ id: provider.id, name: provider.name, package: provider.id }] ? [{ id: provider.id, name: provider.name, package: provider.id }]
: [], : [],
@@ -440,9 +445,7 @@ function currentDefaultModel(value: unknown) {
if (!record(value) || !record(value.default)) return null if (!record(value) || !record(value.default)) return null
const selected = value.default const selected = value.default
const models = currentModels(value) const models = currentModels(value)
return models.find( return models.find((model) => model.providerID === selected.providerID && model.id === selected.modelID) ?? null
(model) => model.providerID === selected.providerID && model.id === selected.modelID,
) ?? null
} }
function currentPermission(value: unknown) { function currentPermission(value: unknown) {
@@ -562,22 +565,16 @@ function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
} }
function mentionFrom(value: Record<string, unknown> | undefined) { function mentionFrom(value: Record<string, unknown> | undefined) {
if ( if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
!value ||
typeof value.value !== "string" ||
typeof value.start !== "number" ||
typeof value.end !== "number"
)
return return
return { text: value.value, start: value.start, end: value.end } return { text: value.value, start: value.start, end: value.end }
} }
function legacyAssistantContent( function legacyAssistantContent(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
part: Record<string, unknown>,
created: number,
): SessionMessageAssistant["content"] {
if (part.type === "text" && typeof part.text === "string") if (part.type === "text" && typeof part.text === "string")
return [{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) }] return [
{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) },
]
if (part.type === "reasoning" && typeof part.text === "string") { if (part.type === "reasoning" && typeof part.text === "string") {
const time = record(part.time) ? part.time : undefined const time = record(part.time) ? part.time : undefined
return [ return [
@@ -618,7 +615,12 @@ function legacyAssistantContent(
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}), ...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
} }
if (state.status === "pending") if (state.status === "pending")
return [{ ...base, state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) } }] return [
{
...base,
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
},
]
if (state.status === "completed") if (state.status === "completed")
return [ return [
{ {
+1 -2
View File
@@ -174,8 +174,7 @@ export async function installSseTransport<T>(
const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init) const request = new Request(input, init)
const url = new URL(request.url) const url = new URL(request.url)
if (url.origin !== server || url.pathname !== "/api/event") if (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
return originalFetch(request)
const id = ++nextConnectionID const id = ++nextConnectionID
const record = { const record = {
+4 -1
View File
@@ -20,7 +20,10 @@
<meta property="twitter:image" content="/social-share.png" /> <meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script> <script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head> </head>
<body data-new-layout class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"> <body
data-new-layout
class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"
>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div> <div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
<script src="/src/entry.tsx" type="module"></script> <script src="/src/entry.tsx" type="module"></script>
+1 -8
View File
@@ -9,14 +9,7 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo" import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
type BaseRouterProps,
Navigate,
Route,
Router,
useParams,
useSearchParams,
} from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect" import { Effect } from "effect"
import { import {
@@ -123,9 +123,7 @@ export const SettingsProvidersV2: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? [] const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`) if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all( await Promise.all(
credentials.map((credential) => credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })),
serverSdk().api.credential.remove({ credentialID: credential.id, location }),
),
) )
showToast({ showToast({
variant: "success", variant: "success",
@@ -5,7 +5,16 @@ import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs" import { Tabs } from "@opencode-ai/ui/tabs"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js" import {
type Accessor,
createEffect,
createMemo,
createResource,
For,
type JSXElement,
onCleanup,
Show,
} from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -291,7 +300,10 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const lspCount = createMemo(() => lspItems().length) const lspCount = createMemo(() => lspItems().length)
const [pluginList] = createResource( const [pluginList] = createResource(
() => (props.shown() ? sdk().directory : undefined), () => (props.shown() ? sdk().directory : undefined),
(directory) => sdk().api.plugin.list({ location: { directory } }).then((result) => result.data), (directory) =>
sdk()
.api.plugin.list({ location: { directory } })
.then((result) => result.data),
) )
const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id)) const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id))
const pluginCount = createMemo(() => plugins().length) const pluginCount = createMemo(() => plugins().length)
+1 -4
View File
@@ -190,10 +190,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
.find((item) => ServerConnection.key(item) === (route.server ?? server.key)) .find((item) => ServerConnection.key(item) === (route.server ?? server.key))
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
}, },
({ route, sdk }) => ({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
sdk.api.session
.get({ sessionID: route.sessionId })
.catch(() => {}),
) )
const matchRoute = (route: LayoutRoute) => { const matchRoute = (route: LayoutRoute) => {
+1 -3
View File
@@ -124,9 +124,7 @@ export const createDirSyncContext = (
const [store, setStore] = current() const [store, setStore] = current()
setStore("limit", (value) => value + count) setStore("limit", (value) => value + count)
const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" }) const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" })
const sessions = response.data const sessions = response.data.sort((a, b) => cmp(a.id, b.id)).slice(0, store.limit)
.sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit)
sessions.forEach(serverSync.session.remember) sessions.forEach(serverSync.session.remember)
setStore("session", reconcile(sessions, { key: "id" })) setStore("session", reconcile(sessions, { key: "id" }))
}, },
@@ -1,9 +1,4 @@
import type { import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import type { import type {
AgentListInput, AgentListInput,
AgentListOutput, AgentListOutput,
@@ -31,12 +26,7 @@ import { batch } from "solid-js"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import type { ServerSession } from "../server-session" import type { ServerSession } from "../server-session"
import { import { cmp, normalizeAgentList, normalizeProjectInfo, normalizeProviderList } from "./utils"
cmp,
normalizeAgentList,
normalizeProjectInfo,
normalizeProviderList,
} from "./utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query" import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync" import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
@@ -148,10 +138,7 @@ export async function bootstrapGlobal(input: {
}) { }) {
const slow = [ const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)), () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)),
() => () => input.queryClient.fetchQuery(loadProvidersQuery(input.scope, null, input.serverAPI)),
input.queryClient.fetchQuery(
loadProvidersQuery(input.scope, null, input.serverAPI),
),
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)), () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
() => () =>
input.queryClient input.queryClient
@@ -211,11 +198,7 @@ function warmSessions(input: {
).then(() => undefined) ).then(() => undefined)
} }
export const loadProvidersQuery = ( export const loadProvidersQuery = (scope: ServerScope, directory: string | null, sdk: CatalogApi) =>
scope: ServerScope,
directory: string | null,
sdk: CatalogApi,
) =>
queryOptions({ queryOptions({
queryKey: [scope, directory, "providers"], queryKey: [scope, directory, "providers"],
queryFn: () => queryFn: () =>
@@ -242,28 +225,16 @@ type ReferenceListApi = {
readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput> readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput>
} }
export const loadAgentsQuery = ( export const loadAgentsQuery = (scope: ServerScope, directory: string, sdk: AgentListApi) =>
scope: ServerScope,
directory: string,
sdk: AgentListApi,
) =>
queryOptions({ queryOptions({
queryKey: [scope, directory, "agents"], queryKey: [scope, directory, "agents"],
queryFn: () => queryFn: () => retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
}) })
export const loadCommands = ( export const loadCommands = (directory: string, api: CommandListApi): Promise<CommandInfo[]> =>
directory: string,
api: CommandListApi,
): Promise<CommandInfo[]> =>
retry(() => api.list({ location: { directory } }).then((result) => result.data)) retry(() => api.list({ location: { directory } }).then((result) => result.data))
export const loadPathQuery = ( export const loadPathQuery = (scope: ServerScope, directory: string | null, api: LocationApi) =>
scope: ServerScope,
directory: string | null,
api: LocationApi,
) =>
queryOptions<Path>({ queryOptions<Path>({
queryKey: [scope, directory, "path"], queryKey: [scope, directory, "path"],
queryFn: () => queryFn: () =>
@@ -276,15 +247,10 @@ export const loadPathQuery = (
})), })),
}) })
export const loadReferencesQuery = ( export const loadReferencesQuery = (scope: ServerScope, directory: string, api: ReferenceListApi) =>
scope: ServerScope,
directory: string,
api: ReferenceListApi,
) =>
queryOptions<ReferenceInfo[]>({ queryOptions<ReferenceInfo[]>({
queryKey: [scope, directory, "references"] as const, queryKey: [scope, directory, "references"] as const,
queryFn: () => queryFn: () => retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
placeholderData: [], placeholderData: [],
}) })
@@ -353,13 +319,8 @@ export async function bootstrapDirectory(input: {
})), })),
input.mcp && input.mcp &&
(() => (() =>
loadCommands(input.directory, input.api.command).then((commands) => loadCommands(input.directory, input.api.command).then((commands) => input.setStore("command", commands))),
input.setStore("command", commands), () => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.api.reference)),
)),
() =>
input.queryClient.fetchQuery(
loadReferencesQuery(input.scope, input.directory, input.api.reference),
),
() => () =>
retry(() => retry(() =>
input.api.permission.request input.api.permission.request
@@ -429,20 +390,11 @@ export async function bootstrapDirectory(input: {
}), }),
), ),
() => Promise.resolve(input.loadSessions(input.directory)), () => Promise.resolve(input.loadSessions(input.directory)),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))),
input.mcp && input.mcp &&
(() => (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))),
input.queryClient.fetchQuery(
loadMcpQuery(input.scope, input.directory, input.api.mcp),
)),
input.mcp &&
(() =>
input.queryClient.fetchQuery(
loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp),
)),
() => () =>
input.queryClient input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api)).catch((err) => {
.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api))
.catch((err) => {
const project = getFilename(input.directory) const project = getFilename(input.directory)
showToast({ showToast({
variant: "error", variant: "error",
@@ -1,12 +1,13 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { Message, Part, Project, Todo } from "@/types"
import type { import type {
Message, FileDiffInfo,
Part, PermissionRequest,
Project, QuestionRequest,
Todo, SessionInfo,
} from "@/types" SessionStatus,
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionInfo, SessionStatus } from "@opencode-ai/client/promise" } from "@opencode-ai/client/promise"
import type { State, VcsCache } from "./types" import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim" import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache" import { dropSessionCaches } from "./session-cache"
+1 -10
View File
@@ -1,13 +1,4 @@
import type { import type { Agent, Config, LspStatus, Message, Part, Path, Todo, VcsInfo } from "@/types"
Agent,
Config,
LspStatus,
Message,
Part,
Path,
Todo,
VcsInfo,
} from "@/types"
import type { import type {
FileDiffInfo, FileDiffInfo,
PermissionRequest, PermissionRequest,
@@ -153,4 +153,88 @@ describe("v2 session reducer", () => {
expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] }) expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] })
}) })
test("removes cancelled input from the pending promotion fold", () => {
const reducer = createV2SessionReducer()
reducer.reduce(
[],
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "cancel me" } },
},
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_cancelled",
type: "session.input.cancelled",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
const result = reducer.reduce(
[],
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
expect(result).toMatchObject({ missing: "msg_user" })
})
test("keeps steered input available to the promotion fold", () => {
const reducer = createV2SessionReducer()
reducer.reduce(
[],
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "steer me" } },
},
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_steered",
type: "session.input.steered",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
reducer.reduce(
[],
event({
...base,
id: "evt_queued",
type: "session.input.queued",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
const result = reducer.reduce(
[],
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
}),
)
expect(result?.messages).toMatchObject([{ id: "msg_user", type: "user", text: "steer me" }])
})
}) })
@@ -29,6 +29,9 @@ export function createV2SessionReducer() {
case "session.input.admitted": case "session.input.admitted":
pending.set(key(sessionID, event.data.inputID), event.data.input) pending.set(key(sessionID, event.data.inputID), event.data.input)
return result([...source]) return result([...source])
case "session.input.cancelled":
pending.delete(key(sessionID, event.data.inputID))
return
case "session.input.promoted": { case "session.input.promoted": {
const input = pending.get(key(sessionID, event.data.inputID)) const input = pending.get(key(sessionID, event.data.inputID))
pending.delete(key(sessionID, event.data.inputID)) pending.delete(key(sessionID, event.data.inputID))
+18 -19
View File
@@ -67,7 +67,8 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
}, },
] ]
} }
return [{ return [
{
id: item.info.id, id: item.info.id,
type: "assistant", type: "assistant",
agent: item.info.agent, agent: item.info.agent,
@@ -84,7 +85,8 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
] ]
if (part.type !== "tool") return [] if (part.type !== "tool") return []
const state: SessionMessageAssistantTool["state"] = (() => { const state: SessionMessageAssistantTool["state"] = (() => {
if (part.state.status === "pending") return { status: "streaming" as const, input: JSON.stringify(part.state.input) } if (part.state.status === "pending")
return { status: "streaming" as const, input: JSON.stringify(part.state.input) }
if (part.state.status === "running") if (part.state.status === "running")
return { return {
status: "running" as const, status: "running" as const,
@@ -123,8 +125,16 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
time: item.info.time, time: item.info.time,
cost: item.info.cost, cost: item.info.cost,
tokens: item.info.tokens, tokens: item.info.tokens,
finish: item.info.finish as "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" | undefined, finish: item.info.finish as
}] | "stop"
| "length"
| "tool-calls"
| "content-filter"
| "error"
| "unknown"
| undefined,
},
]
}) })
} }
@@ -432,9 +442,7 @@ describe("server session", () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)], [singleResponse(user)],
) )
const store = createServerSession(client) const store = createServerSession(client)
@@ -495,9 +503,7 @@ describe("server session", () => {
const part = textPart(user.id) const part = textPart(user.id)
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)], [singleResponse(user)],
) )
const store = createServerSession(client) const store = createServerSession(client)
@@ -516,10 +522,7 @@ describe("server session", () => {
const user = userMessage("message-1") const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response([{ info: unrelated, parts: [] }]), response(assistants.map((info) => ({ info, parts: [] })))],
response([{ info: unrelated, parts: [] }]),
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)], [singleResponse(user)],
) )
const store = createServerSession(client) const store = createServerSession(client)
@@ -612,11 +615,7 @@ describe("server session", () => {
const live = { ...user, agent: "stale" } const live = { ...user, agent: "stale" }
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient( const client = rootMessageClient(
[ [response(assistants.map((info) => ({ info, parts: [] })))],
response(
assistants.map((info) => ({ info, parts: [] })),
),
],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)], [failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
) )
const store = createServerSession(client, { retry: retryImmediately }) const store = createServerSession(client, { retry: retryImmediately })
+6 -12
View File
@@ -1,11 +1,7 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { retry } from "@opencode-ai/core/util/retry" import { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise" import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
import type { import type { Message, Part, Todo } from "@/types"
Message,
Part,
Todo,
} from "@/types"
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise" import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
import { batch } from "solid-js" import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
@@ -38,14 +34,16 @@ function projectMessageSource(message: Message): SessionMessageInfo[] {
{ id: message.id, type: "user", text: "", time: message.time }, { id: message.id, type: "user", text: "", time: message.time },
] ]
} }
return [{ return [
{
id: message.id, id: message.id,
type: "assistant", type: "assistant",
agent: message.agent ?? message.mode, agent: message.agent ?? message.mode,
model: { id: message.modelID, providerID: message.providerID, variant: message.variant }, model: { id: message.modelID, providerID: message.providerID, variant: message.variant },
content: [], content: [],
time: message.time, time: message.time,
}] },
]
} }
function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) { function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) {
@@ -243,11 +241,7 @@ export function createServerSession(
const indexProjectedMessage = (message: Message) => { const indexProjectedMessage = (message: Message) => {
const current = data.session_message[message.sessionID] ?? [] const current = data.session_message[message.sessionID] ?? []
if (current.some((item) => item.id === message.id)) return if (current.some((item) => item.id === message.id)) return
setData( setData("session_message", message.sessionID, reconcile([...current, ...projectMessageSource(message)]))
"session_message",
message.sessionID,
reconcile([...current, ...projectMessageSource(message)]),
)
} }
const remember = (session: SessionInfo) => { const remember = (session: SessionInfo) => {
+3 -12
View File
@@ -1,9 +1,4 @@
import type { import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js" import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -164,15 +159,11 @@ export function seedActiveSessionStatuses(
} }
} }
function makeQueryOptionsApi( function makeQueryOptionsApi(scope: ServerScope, serverAPI: ServerApi) {
scope: ServerScope,
serverAPI: ServerApi,
) {
return { return {
globalConfig: () => loadGlobalConfigQuery(scope), globalConfig: () => loadGlobalConfigQuery(scope),
projects: () => loadProjectsQuery(scope, serverAPI.project), projects: () => loadProjectsQuery(scope, serverAPI.project),
providers: (directory: PathKey | null) => providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
loadProvidersQuery(scope, directory, serverAPI),
path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location), path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location),
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent), agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference), references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
+4 -1
View File
@@ -268,7 +268,10 @@ function createWorkspaceTerminalSession(
const index = store.all.findIndex((x) => x.id === id) const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index] const pty = store.all[index]
if (!pty) return if (!pty) return
const data = await sdk.api.pty.create({ location, title: pty.title }).then((result) => result.data).catch((error: unknown) => { const data = await sdk.api.pty
.create({ location, title: pty.title })
.then((result) => result.data)
.catch((error: unknown) => {
console.error("Failed to clone terminal", error) console.error("Failed to clone terminal", error)
return undefined return undefined
}) })
+12 -13
View File
@@ -688,6 +688,8 @@ export default function Page() {
return { return {
queryKey: [...vcsKey(), mode] as const, queryKey: [...vcsKey(), mode] as const,
enabled, enabled,
refetchOnMount: "always" as const,
refetchOnWindowFocus: true,
queryFn: mode queryFn: mode
? () => ? () =>
sdk() sdk()
@@ -701,6 +703,16 @@ export default function Page() {
} }
}) })
const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100) const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100)
createEffect(
on(
() => desktopReviewOpen() || mobileChanges(),
(open, previous) => {
if (!open || previous || !desktopFileTreeOpen() || vcsQuery.isFetching) return
refreshVcs()
},
{ defer: true },
),
)
const reviewDiffs = () => { const reviewDiffs = () => {
if (reviewMode() === "git" || reviewMode() === "branch") if (reviewMode() === "git" || reviewMode() === "branch")
// avoids suspense // avoids suspense
@@ -947,19 +959,6 @@ export default function Page() {
), ),
) )
const stopVcs = sdk().event.listen((evt) => {
const details = evt.details as { type: string; properties?: unknown }
if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return
const props =
typeof details.properties === "object" && details.properties
? (details.properties as Record<string, unknown>)
: undefined
const file = typeof props?.file === "string" ? props.file : undefined
if (!file || file.startsWith(".git/")) return
refreshVcs()
})
onCleanup(stopVcs)
createEffect( createEffect(
on( on(
() => sdk().directory, () => sdk().directory,
@@ -45,13 +45,7 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { TextReveal } from "@opencode-ai/ui/text-reveal" import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer" import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import type { import type { AssistantMessage, Message as MessageType, Part as PartType, ToolPart, UserMessage } from "@/types"
AssistantMessage,
Message as MessageType,
Part as PartType,
ToolPart,
UserMessage,
} from "@/types"
import { showToast } from "@/utils/toast" import { showToast } from "@/utils/toast"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover" import { Popover as KobaltePopover } from "@kobalte/core/popover"
@@ -54,9 +54,9 @@ describe("reviewDiffNeedsLoad", () => {
patch: "@@ -0,0 +1 @@\n+value", patch: "@@ -0,0 +1 @@\n+value",
}), }),
).toBe(false) ).toBe(false)
expect( expect(reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" })).toBe(
reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" }), false,
).toBe(false) )
}) })
}) })
+1 -5
View File
@@ -1,8 +1,4 @@
import type { import type { EventSubscribeOutput, FileDiffInfo, ProjectListOutput } from "@opencode-ai/client/promise"
EventSubscribeOutput,
FileDiffInfo,
ProjectListOutput,
} from "@opencode-ai/client/promise"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string } export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string }
+2 -1
View File
@@ -22,5 +22,6 @@
} }
}, },
"include": ["src", "package.json"], "include": ["src", "package.json"],
"exclude": ["dist", "ts-dist"] "exclude": ["dist", "ts-dist"],
"references": [{ "path": "../core" }]
} }
+12 -2
View File
@@ -119,7 +119,15 @@ function installPackage(name) {
try { try {
const result = childProcess.spawnSync( const result = childProcess.spawnSync(
"npm", "npm",
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`], [
"install",
"--ignore-scripts",
"--no-save",
"--loglevel=error",
"--prefix",
temp,
`${name}@${dependencies[name]}`,
],
{ stdio: "inherit", windowsHide: true }, { stdio: "inherit", windowsHide: true },
) )
if (result.status !== 0) return false if (result.status !== 0) return false
@@ -150,7 +158,9 @@ function main() {
} }
} }
throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`) throw new Error(
`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`,
)
} }
try { try {
+11 -13
View File
@@ -40,15 +40,13 @@ try {
const token = encodeURIComponent(credential) const token = encodeURIComponent(credential)
const health = await waitForReady(info.url, headers) const health = await waitForReady(info.url, headers)
if (health.pid !== info.pid) throw new Error("Health process does not match registration") if (health.pid !== info.pid) throw new Error("Health process does not match registration")
const tokenHealth = await fetch( const tokenHealth = await fetch(new URL(`/api/health?auth_token=${token}`, info.url), {
new URL(`/api/health?auth_token=${token}`, info.url), signal: AbortSignal.timeout(5_000),
{ signal: AbortSignal.timeout(5_000) }, })
)
if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication") if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication")
const tokenOpenApi = await fetch( const tokenOpenApi = await fetch(new URL(`/openapi.json?auth_token=${token}`, info.url), {
new URL(`/openapi.json?auth_token=${token}`, info.url), signal: AbortSignal.timeout(5_000),
{ signal: AbortSignal.timeout(5_000) }, })
)
if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication") if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication")
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation") if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts") const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
@@ -63,7 +61,8 @@ try {
const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), { const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), {
signal: AbortSignal.timeout(5_000), signal: AbortSignal.timeout(5_000),
}) })
if (unauthorizedOpenApi.status !== 401) throw new Error("Compiled service exposed application routes without authentication") if (unauthorizedOpenApi.status !== 401)
throw new Error("Compiled service exposed application routes without authentication")
const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), { const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
@@ -94,7 +93,8 @@ try {
} finally { } finally {
processes.forEach((process) => process.kill()) processes.forEach((process) => process.kill())
await Promise.all(processes.map((process) => process.exited)) await Promise.all(processes.map((process) => process.exited))
if (failure) errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => "")) if (failure)
errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
} }
const output = await Promise.all(errors) const output = await Promise.all(errors)
@@ -154,9 +154,7 @@ async function pluginIDs(url: string, headers: HeadersInit) {
throw new Error("Compiled service returned an invalid plugin list") throw new Error("Compiled service returned an invalid plugin list")
} }
return body.data.flatMap((plugin) => return body.data.flatMap((plugin) =>
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" ? [plugin.id] : [],
? [plugin.id]
: [],
) )
} }
+5 -4
View File
@@ -20,7 +20,8 @@ export default Runtime.handler(
return response.text() return response.text()
}) })
: Bun.file(input.file).text(), : Bun.file(input.file).text(),
catch: (cause) => new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`), catch: (cause) =>
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
}) })
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text) const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
const encoded = Schema.encodeSync(SessionTransfer.Data)(data) const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
@@ -52,9 +53,9 @@ export default Runtime.handler(
return return
} }
if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`)) if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`))
const imported = yield* Schema.decodeUnknownEffect( const imported = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ data: Session.Info })))(
Schema.fromJsonString(Schema.Struct({ data: Session.Info })), yield* Effect.promise(() => response.text()),
)(yield* Effect.promise(() => response.text())) )
process.stdout.write(`Imported session: ${imported.data.id}${EOL}`) process.stdout.write(`Imported session: ${imported.data.id}${EOL}`)
}), }),
) )
+1 -3
View File
@@ -36,8 +36,6 @@ export default Runtime.handler(
const hostname = new URL(endpoint.url).hostname const hostname = new URL(endpoint.url).hostname
if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return
process.stderr.write( process.stderr.write(` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`)
` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`,
)
}), }),
) )
+7 -3
View File
@@ -48,12 +48,16 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record<strin
const attentionSoundPack = kv.attention_sound_pack const attentionSoundPack = kv.attention_sound_pack
const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined) const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined)
const thinking = const thinking =
kv.thinking_mode ?? kv.thinking_mode ?? (kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
(kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
return { return {
...(themeName !== undefined || themeMode !== undefined ...(themeName !== undefined || themeMode !== undefined
? { theme: { ...(themeName === undefined ? {} : { name: themeName }), ...(themeMode === undefined ? {} : { mode: themeMode }) } } ? {
theme: {
...(themeName === undefined ? {} : { name: themeName }),
...(themeMode === undefined ? {} : { mode: themeMode }),
},
}
: {}), : {}),
...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }), ...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }),
...(plugins.length ? { plugins } : {}), ...(plugins.length ? { plugins } : {}),
+1 -4
View File
@@ -468,10 +468,7 @@ export async function runNonInteractivePrompt(input: Input) {
continue continue
} }
if (event.type === "session.step.failed") { if (event.type === "session.step.failed") {
if ( if (input.compatibility === "v1" && event.data.error.message === "The provider response ended unexpectedly.") {
input.compatibility === "v1" &&
event.data.error.message === "The provider response ended unexpectedly."
) {
pendingStep = undefined pendingStep = undefined
v1InvalidOutput = true v1InvalidOutput = true
continue continue
@@ -62,10 +62,7 @@ function managedService(options: EnsureOptions) {
} }
} }
const resolveManaged = Effect.fnUntraced(function* ( const resolveManaged = Effect.fnUntraced(function* (options: EnsureOptions, mismatch: NonNullable<Args["mismatch"]>) {
options: EnsureOptions,
mismatch: NonNullable<Args["mismatch"]>,
) {
if (mismatch === "replace") return yield* Service.ensure(options) if (mismatch === "replace") return yield* Service.ensure(options)
if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined }) if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined })
@@ -413,9 +413,7 @@ function UpdateFooter(props: {
const completion = smoothstep(headerFade.progress()) const completion = smoothstep(headerFade.progress())
return Array.from({ length: width }, (_, index) => { return Array.from({ length: width }, (_, index) => {
const color = const color =
index >= filled index >= filled ? colors.muted : shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
? colors.muted
: shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
return { return {
char: success || index < filled ? "━" : "·", char: success || index < filled ? "━" : "·",
color: success ? blend(color, colors.accent, completion) : color, color: success ? blend(color, colors.accent, completion) : color,
+16 -15
View File
@@ -52,9 +52,10 @@ export const layer = Layer.effect(
const readPolicy = Effect.fnUntraced(function* () { const readPolicy = Effect.fnUntraced(function* () {
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) => const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
fs fs.readFileString(path.join(global.config, name)).pipe(
.readFileString(path.join(global.config, name)) Effect.map(decodePolicy),
.pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))), Effect.catch(() => Effect.succeed(undefined)),
),
) )
return values.findLast((value) => value !== undefined) ?? true return values.findLast((value) => value !== undefined) ?? true
}) })
@@ -94,10 +95,10 @@ export const layer = Layer.effect(
const latest = Effect.fnUntraced(function* () { const latest = Effect.fnUntraced(function* () {
const response = yield* Effect.tryPromise({ const response = yield* Effect.tryPromise({
try: () => try: () =>
fetch( fetch(`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, {
`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` },
{ headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` }, signal: AbortSignal.timeout(10_000) }, signal: AbortSignal.timeout(10_000),
), }),
catch: (cause) => new Error("Failed to check for updates", { cause }), catch: (cause) => new Error("Failed to check for updates", { cause }),
}) })
if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`)) if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`))
@@ -118,7 +119,7 @@ export const layer = Layer.effect(
pnpm: ["pnpm", "install", "--global", target], pnpm: ["pnpm", "install", "--global", target],
yarn: ["yarn", "global", "add", target], yarn: ["yarn", "global", "add", target],
} }
const result = yield* (method === "bun" const result = yield* method === "bun"
? Effect.scoped( ? Effect.scoped(
Effect.gen(function* () { Effect.gen(function* () {
// Bun does not prune old versions from its shared package cache. // Bun does not prune old versions from its shared package cache.
@@ -127,16 +128,14 @@ export const layer = Layer.effect(
return yield* run(["bun", "install", "--global", "--cache-dir", cache, target], "5 minutes") return yield* run(["bun", "install", "--global", "--cache-dir", cache, target], "5 minutes")
}), }),
) )
: run(commands[method], "5 minutes")) : run(commands[method], "5 minutes")
if (result.code === 0) return if (result.code === 0) return
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`)) return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
}) })
const check = Effect.fn("cli.updater.check")(function* () { const check = Effect.fn("cli.updater.check")(
if ( function* () {
OPENCODE_LOCAL || if (OPENCODE_LOCAL || ["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? ""))
["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? "")
)
return yield* Effect.logInfo("update check skipped", { return yield* Effect.logInfo("update check skipped", {
reason: OPENCODE_LOCAL ? "local-install" : "disabled", reason: OPENCODE_LOCAL ? "local-install" : "disabled",
version: OPENCODE_VERSION, version: OPENCODE_VERSION,
@@ -158,7 +157,9 @@ export const layer = Layer.effect(
yield* upgrade(detected, version) yield* upgrade(detected, version)
yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected }) yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected })
}) })
}, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause }))) },
Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })),
)
return Service.of({ check }) return Service.of({ check })
}), }),
+2 -4
View File
@@ -102,9 +102,7 @@ test("migrates before the first update and does not remigrate afterward", async
draft.animations = false draft.animations = false
draft.mouse = false draft.mouse = false
}) })
yield* Effect.promise(() => yield* Effect.promise(() => Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })))
Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })),
)
return yield* service.get() return yield* service.get()
}), }),
) )
@@ -122,7 +120,7 @@ test("migrates before the first update and does not remigrate afterward", async
test("updates a config draft while preserving JSONC comments", async () => { test("updates a config draft while preserving JSONC comments", async () => {
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim()) const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
await Bun.write(path.join(directory, "cli.json"), "{\n // Keep this comment\n \"animations\": true\n}\n") await Bun.write(path.join(directory, "cli.json"), '{\n // Keep this comment\n "animations": true\n}\n')
try { try {
const config = await run( const config = await run(
@@ -17,7 +17,9 @@ export default defineScript({
const session = `mini-stage2-${process.pid}` const session = `mini-stage2-${process.pid}`
const snapshots = path.join(artifacts, "mini-stage2") const snapshots = path.join(artifacts, "mini-stage2")
const explicitDirectory = path.join(artifacts, "explicit-model") const explicitDirectory = path.join(artifacts, "explicit-model")
yield* Effect.promise(() => Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true })))) yield* Effect.promise(() =>
Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true }))),
)
/** @param {string} directory @param {string | undefined} model */ /** @param {string} directory @param {string | undefined} model */
const mini = (directory, model) => [ const mini = (directory, model) => [
"env", "env",
@@ -169,14 +171,7 @@ export default defineScript({
yield* Effect.promise(() => tmux(["clear-history", "-t", session])) yield* Effect.promise(() => tmux(["clear-history", "-t", session]))
yield* Effect.promise(() => yield* Effect.promise(() =>
tmux([ tmux(["respawn-pane", "-k", "-t", session, "--", ...mini(explicitDirectory, "simulation/gpt-sim-model")]),
"respawn-pane",
"-k",
"-t",
session,
"--",
...mini(explicitDirectory, "simulation/gpt-sim-model"),
]),
) )
const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000)) const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000))
yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel)) yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel))
+1 -8
View File
@@ -159,14 +159,7 @@ test("import validates a file and sends it to the resolved location", async () =
}) })
try { try {
const [stdout, , exitCode] = await run([ const [stdout, , exitCode] = await run(["import", file, "--directory", root, "--server", server.url.toString()])
"import",
file,
"--directory",
root,
"--server",
server.url.toString(),
])
expect(exitCode).toBe(0) expect(exitCode).toBe(0)
expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`) expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`)
+1 -7
View File
@@ -3,13 +3,7 @@ import { readFile } from "node:fs/promises"
import { createRequire } from "node:module" import { createRequire } from "node:module"
import { defineConfig, type Plugin, type UserConfig } from "vite" import { defineConfig, type Plugin, type UserConfig } from "vite"
import solid from "vite-plugin-solid" import solid from "vite-plugin-solid"
import { import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset, shellParserWasmAssets } from "./src/node/target"
nodeExecArgv,
nodeTarget,
type NodeTarget,
photonWasmAsset,
shellParserWasmAssets,
} from "./src/node/target"
const dir = import.meta.dirname const dir = import.meta.dirname
+1 -6
View File
@@ -1,11 +1,6 @@
import { NodeFileSystem } from "@effect/platform-node" import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen" import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Agent } from "@opencode-ai/schema/agent" import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command" import { Command } from "@opencode-ai/schema/command"
import { Config } from "@opencode-ai/schema/config" import { Config } from "@opencode-ai/schema/config"
+1 -6
View File
@@ -1,6 +1 @@
export { export { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
@@ -1,3 +1 @@
[ ["api.ts"]
"api.ts"
]
+73 -27
View File
@@ -263,38 +263,52 @@ export type Endpoint5_23Input = { readonly sessionID: Session.ID }
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info> export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E> export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
export type Endpoint5_24Input = { readonly sessionID: Session.ID } export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_24Output = ReadonlyArray<InstructionEntry.Info> export type Endpoint5_24Output = void
export type SessionInstructionsEntryListOperation<E = never> = ( export type SessionPendingCancelOperation<E = never> = (
input: Endpoint5_24Input, input: Endpoint5_24Input,
) => Effect.Effect<Endpoint5_24Output, E> ) => Effect.Effect<Endpoint5_24Output, E>
export type Endpoint5_25Input = { export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_25Output = void
export type SessionPendingSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_26Output = void
export type SessionPendingQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
export type Endpoint5_27Output = ReadonlyArray<InstructionEntry.Info>
export type SessionInstructionsEntryListOperation<E = never> = (
input: Endpoint5_27Input,
) => Effect.Effect<Endpoint5_27Output, E>
export type Endpoint5_28Input = {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly key: InstructionEntry.Key readonly key: InstructionEntry.Key
readonly value: Schema.Json readonly value: Schema.Json
} }
export type Endpoint5_25Output = void export type Endpoint5_28Output = void
export type SessionInstructionsEntryPutOperation<E = never> = ( export type SessionInstructionsEntryPutOperation<E = never> = (
input: Endpoint5_25Input, input: Endpoint5_28Input,
) => Effect.Effect<Endpoint5_25Output, E> ) => Effect.Effect<Endpoint5_28Output, E>
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } export type Endpoint5_29Input = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key }
export type Endpoint5_26Output = void export type Endpoint5_29Output = void
export type SessionInstructionsEntryRemoveOperation<E = never> = ( export type SessionInstructionsEntryRemoveOperation<E = never> = (
input: Endpoint5_26Input, input: Endpoint5_29Input,
) => Effect.Effect<Endpoint5_26Output, E> ) => Effect.Effect<Endpoint5_29Output, E>
export type Endpoint5_27Input = { readonly sessionID: Session.ID; readonly prompt: string } export type Endpoint5_30Input = { readonly sessionID: Session.ID; readonly prompt: string }
export type Endpoint5_27Output = { readonly text: string } export type Endpoint5_30Output = { readonly text: string }
export type SessionGenerateOperation<E = never> = (input: Endpoint5_27Input) => Effect.Effect<Endpoint5_27Output, E> export type SessionGenerateOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E>
export type Endpoint5_28Input = { export type Endpoint5_31Input = {
readonly sessionID: Session.ID readonly sessionID: Session.ID
readonly after?: Event.Seq | undefined readonly after?: Event.Seq | undefined
readonly follow?: boolean | undefined readonly follow?: boolean | undefined
} }
export type Endpoint5_28Output = export type Endpoint5_31Output =
| ( | (
| { | {
readonly id: Event.ID readonly id: Event.ID
@@ -404,6 +418,33 @@ export type Endpoint5_28Output =
readonly input: SessionPending.Message readonly input: SessionPending.Message
} }
} }
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.cancelled"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.steered"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.queued"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| { | {
readonly id: Event.ID readonly id: Event.ID
readonly created: DateTime.Utc readonly created: DateTime.Utc
@@ -862,19 +903,19 @@ export type Endpoint5_28Output =
} }
) )
| EventLog.Synced | EventLog.Synced
export type SessionLogOperation<E = never> = (input: Endpoint5_28Input) => Stream.Stream<Endpoint5_28Output, E> export type SessionLogOperation<E = never> = (input: Endpoint5_31Input) => Stream.Stream<Endpoint5_31Output, E>
export type Endpoint5_29Input = { readonly sessionID: Session.ID } export type Endpoint5_32Input = { readonly sessionID: Session.ID }
export type Endpoint5_29Output = void export type Endpoint5_32Output = void
export type SessionInterruptOperation<E = never> = (input: Endpoint5_29Input) => Effect.Effect<Endpoint5_29Output, E> export type SessionInterruptOperation<E = never> = (input: Endpoint5_32Input) => Effect.Effect<Endpoint5_32Output, E>
export type Endpoint5_30Input = { readonly sessionID: Session.ID } export type Endpoint5_33Input = { readonly sessionID: Session.ID }
export type Endpoint5_30Output = void export type Endpoint5_33Output = void
export type SessionBackgroundOperation<E = never> = (input: Endpoint5_30Input) => Effect.Effect<Endpoint5_30Output, E> export type SessionBackgroundOperation<E = never> = (input: Endpoint5_33Input) => Effect.Effect<Endpoint5_33Output, E>
export type Endpoint5_31Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } export type Endpoint5_34Input = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID }
export type Endpoint5_31Output = SessionMessage.Info export type Endpoint5_34Output = SessionMessage.Info
export type SessionMessageOperation<E = never> = (input: Endpoint5_31Input) => Effect.Effect<Endpoint5_31Output, E> export type SessionMessageOperation<E = never> = (input: Endpoint5_34Input) => Effect.Effect<Endpoint5_34Output, E>
export interface SessionApi<E = never> { export interface SessionApi<E = never> {
readonly list: SessionListOperation<E> readonly list: SessionListOperation<E>
@@ -902,7 +943,12 @@ export interface SessionApi<E = never> {
readonly commit: SessionRevertCommitOperation<E> readonly commit: SessionRevertCommitOperation<E>
} }
readonly context: SessionContextOperation<E> readonly context: SessionContextOperation<E>
readonly pending: { readonly list: SessionPendingListOperation<E> } readonly pending: {
readonly list: SessionPendingListOperation<E>
readonly cancel: SessionPendingCancelOperation<E>
readonly steer: SessionPendingSteerOperation<E>
readonly queue: SessionPendingQueueOperation<E>
}
readonly instructions: { readonly instructions: {
readonly entry: { readonly entry: {
readonly list: SessionInstructionsEntryListOperation<E> readonly list: SessionInstructionsEntryListOperation<E>
@@ -1,5 +1 @@
[ ["client-error.ts", "client.ts", "index.ts"]
"client-error.ts",
"client.ts",
"index.ts"
]
+49 -22
View File
@@ -80,6 +80,12 @@ import type {
Endpoint5_30Output, Endpoint5_30Output,
Endpoint5_31Input, Endpoint5_31Input,
Endpoint5_31Output, Endpoint5_31Output,
Endpoint5_32Input,
Endpoint5_32Output,
Endpoint5_33Input,
Endpoint5_33Output,
Endpoint5_34Input,
Endpoint5_34Output,
Endpoint6_0Input, Endpoint6_0Input,
Endpoint6_0Output, Endpoint6_0Output,
Endpoint7_0Input, Endpoint7_0Input,
@@ -523,37 +529,58 @@ const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23I
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) => const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
preserveEffect<Endpoint5_24Output>()( preserveEffect<Endpoint5_24Output>()(
raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( raw["session.pending.cancel"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data),
), ),
) )
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) => const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
preserveEffect<Endpoint5_25Output>()( preserveEffect<Endpoint5_25Output>()(
raw["session.instructions.entry.put"]({ raw["session.pending.steer"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
params: { sessionID: input["sessionID"], key: input["key"] }, Effect.mapError(mapClientError),
payload: { value: input["value"] }, ),
}).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) => const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
preserveEffect<Endpoint5_26Output>()( preserveEffect<Endpoint5_26Output>()(
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( raw["session.pending.queue"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
), ),
) )
const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) => const Endpoint5_27 = (raw: RawClient["server.session"]) => (input: Endpoint5_27Input) =>
preserveEffect<Endpoint5_27Output>()( preserveEffect<Endpoint5_27Output>()(
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
), ),
) )
const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) => const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28Input) =>
preserveStream<Endpoint5_28Output>()( preserveEffect<Endpoint5_28Output>()(
raw["session.instructions.entry.put"]({
params: { sessionID: input["sessionID"], key: input["key"] },
payload: { value: input["value"] },
}).pipe(Effect.mapError(mapClientError)),
)
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) =>
preserveEffect<Endpoint5_29Output>()(
raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) =>
preserveEffect<Endpoint5_30Output>()(
raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
)
const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31Input) =>
preserveStream<Endpoint5_31Output>()(
Stream.unwrap( Stream.unwrap(
raw["session.log"]({ raw["session.log"]({
params: { sessionID: input["sessionID"] }, params: { sessionID: input["sessionID"] },
@@ -565,18 +592,18 @@ const Endpoint5_28 = (raw: RawClient["server.session"]) => (input: Endpoint5_28I
), ),
) )
const Endpoint5_29 = (raw: RawClient["server.session"]) => (input: Endpoint5_29Input) => const Endpoint5_32 = (raw: RawClient["server.session"]) => (input: Endpoint5_32Input) =>
preserveEffect<Endpoint5_29Output>()( preserveEffect<Endpoint5_32Output>()(
raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_30 = (raw: RawClient["server.session"]) => (input: Endpoint5_30Input) => const Endpoint5_33 = (raw: RawClient["server.session"]) => (input: Endpoint5_33Input) =>
preserveEffect<Endpoint5_30Output>()( preserveEffect<Endpoint5_33Output>()(
raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), raw["session.background"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)),
) )
const Endpoint5_31 = (raw: RawClient["server.session"]) => (input: Endpoint5_31Input) => const Endpoint5_34 = (raw: RawClient["server.session"]) => (input: Endpoint5_34Input) =>
preserveEffect<Endpoint5_31Output>()( preserveEffect<Endpoint5_34Output>()(
raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe( raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
Effect.mapError(mapClientError), Effect.mapError(mapClientError),
Effect.map((value) => value.data), Effect.map((value) => value.data),
@@ -605,13 +632,13 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
wait: Endpoint5_18(raw), wait: Endpoint5_18(raw),
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) }, revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
context: Endpoint5_22(raw), context: Endpoint5_22(raw),
pending: { list: Endpoint5_23(raw) }, pending: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
instructions: { entry: { list: Endpoint5_24(raw), put: Endpoint5_25(raw), remove: Endpoint5_26(raw) } }, instructions: { entry: { list: Endpoint5_27(raw), put: Endpoint5_28(raw), remove: Endpoint5_29(raw) } },
generate: Endpoint5_27(raw), generate: Endpoint5_30(raw),
log: Endpoint5_28(raw), log: Endpoint5_31(raw),
interrupt: Endpoint5_29(raw), interrupt: Endpoint5_32(raw),
background: Endpoint5_30(raw), background: Endpoint5_33(raw),
message: Endpoint5_31(raw), message: Endpoint5_34(raw),
}) })
const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) => const Endpoint6_0 = (raw: RawClient["server.message"]) => (input: Endpoint6_0Input) =>
@@ -1,6 +1 @@
[ ["client-error.ts", "client.ts", "index.ts", "types.ts"]
"client-error.ts",
"client.ts",
"index.ts",
"types.ts"
]
@@ -58,6 +58,12 @@ import type {
SessionContextOutput, SessionContextOutput,
SessionPendingListInput, SessionPendingListInput,
SessionPendingListOutput, SessionPendingListOutput,
SessionPendingCancelInput,
SessionPendingCancelOutput,
SessionPendingSteerInput,
SessionPendingSteerOutput,
SessionPendingQueueInput,
SessionPendingQueueOutput,
SessionInstructionsEntryListInput, SessionInstructionsEntryListInput,
SessionInstructionsEntryListOutput, SessionInstructionsEntryListOutput,
SessionInstructionsEntryPutInput, SessionInstructionsEntryPutInput,
@@ -766,6 +772,39 @@ export function make(options: ClientOptions) {
}, },
requestOptions, requestOptions,
).then((value) => value.data), ).then((value) => value.data),
cancel: (input: SessionPendingCancelInput, requestOptions?: RequestOptions) =>
request<SessionPendingCancelOutput>(
{
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
steer: (input: SessionPendingSteerInput, requestOptions?: RequestOptions) =>
request<SessionPendingSteerOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/steer`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
queue: (input: SessionPendingQueueInput, requestOptions?: RequestOptions) =>
request<SessionPendingQueueOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/queue`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
}, },
instructions: { instructions: {
entry: { entry: {
@@ -502,6 +502,36 @@ export type SessionInputPromoted = {
data: { sessionID: string; inputID: string } data: { sessionID: string; inputID: string }
} }
export type SessionInputCancelled = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.cancelled"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputSteered = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.steered"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputQueued = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.queued"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionExecutionStarted = { export type SessionExecutionStarted = {
id: string id: string
created: number created: number
@@ -1970,6 +2000,9 @@ export type SessionEventDurable =
| SessionForked | SessionForked
| SessionInputPromoted | SessionInputPromoted
| SessionInputAdmitted | SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionExecutionStarted | SessionExecutionStarted
| SessionExecutionSucceeded | SessionExecutionSucceeded
| SessionExecutionFailed | SessionExecutionFailed
@@ -2024,6 +2057,9 @@ export type V2Event =
| SessionForked | SessionForked
| SessionInputPromoted | SessionInputPromoted
| SessionInputAdmitted | SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionExecutionStarted | SessionExecutionStarted
| SessionExecutionSucceeded | SessionExecutionSucceeded
| SessionExecutionFailed | SessionExecutionFailed
@@ -3689,6 +3725,27 @@ export type SessionPendingListInput = { readonly sessionID: { readonly sessionID
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"] export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
export type SessionPendingCancelInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingCancelOutput = void
export type SessionPendingSteerInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingSteerOutput = void
export type SessionPendingQueueInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
}
export type SessionPendingQueueOutput = void
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionInstructionsEntryListOutput = { data: Array<InstructionEntryInfo> }["data"] export type SessionInstructionsEntryListOutput = { data: Array<InstructionEntryInfo> }["data"]
+2 -4
View File
@@ -66,8 +66,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (registration.timedOut && registration.info !== undefined) { if (registration.timedOut && registration.info !== undefined) {
timeouts = { timeouts = {
info: registration.info, info: registration.info,
count: count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
} }
if (timeouts.count >= 3) { if (timeouts.count >= 3) {
announce("missing") announce("missing")
@@ -188,8 +187,7 @@ async function probeResult(info: Info, allowLegacy = false) {
const body = result.value.body const body = result.value.body
if (body !== undefined && "version" in body && "pid" in body) { if (body !== undefined && "version" in body && "pid" in body) {
if (body.pid !== info.pid) return { service: undefined, timedOut: false } if (body.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && body.version !== info.version) if (info.version !== undefined && body.version !== info.version) return { service: undefined, timedOut: false }
return { service: undefined, timedOut: false }
return { return {
service: { service: {
info, info,
@@ -19,7 +19,7 @@ test("effect entrypoint exposes canonical Schema contracts", () => {
test("generated Effect API names canonical and composed outputs", async () => { test("generated Effect API names canonical and composed outputs", async () => {
const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text() const source = await Bun.file(new URL("../src/effect/api/api.ts", import.meta.url)).text()
expect(source).toContain("export type Endpoint5_3Output = Session.Info") expect(source).toContain("export type Endpoint5_5Output = Session.Info")
expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent") expect(source).toContain("export type Endpoint19_0Output = OpenCodeEvent")
expect(source).not.toContain("HttpApiClient.ForApi") expect(source).not.toContain("HttpApiClient.ForApi")
}) })
+2 -8
View File
@@ -9,12 +9,7 @@ if (mode === "record-start") {
} }
if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL") if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
if ( if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" || mode === "coordinated-failed-loser") {
mode === "delayed" ||
mode === "delayed-failed" ||
mode === "coordinated" ||
mode === "coordinated-failed-loser"
) {
await appendFile(registration + ".starts", process.pid + "\n") await appendFile(registration + ".starts", process.pid + "\n")
const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" }) const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
.then(() => true) .then(() => true)
@@ -59,8 +54,7 @@ const server = Bun.serve({
if (mode === "legacy") return Response.json({ healthy: true }) if (mode === "legacy") return Response.json({ healthy: true })
if (mode === "starting" && !(await Bun.file(registration + ".release").exists())) if (mode === "starting" && !(await Bun.file(registration + ".release").exists()))
return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 }) return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 })
if (mode === "failed-owner") if (mode === "failed-owner") return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
if (mode === "starting" || mode === "graceful" || mode === "reject-stop") if (mode === "starting" || mode === "graceful" || mode === "reject-stop")
return Response.json({ healthy: true, version, pid: process.pid }) return Response.json({ healthy: true, version, pid: process.pid })
return Response.json({ healthy: true, version, pid: process.pid }) return Response.json({ healthy: true, version, pid: process.pid })
+23
View File
@@ -32,6 +32,7 @@ test("exposes every standard HTTP API group", () => {
"projectCopy", "projectCopy",
"vcs", "vcs",
"debug", "debug",
"migration",
"websearch", "websearch",
"config", "config",
]) ])
@@ -356,6 +357,28 @@ test("session.pending.list uses the public HTTP contract", async () => {
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }]) expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
}) })
test("session.pending mutations use the public HTTP contract", async () => {
const requests: Array<{ method: string; url: string }> = []
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
requests.push({ method: request.method, url: request.url })
return new Response(null, { status: 204 })
},
})
await client.session.pending.cancel({ sessionID: "ses_test", inputID: "msg_cancel" })
await client.session.pending.steer({ sessionID: "ses_test", inputID: "msg_steer" })
await client.session.pending.queue({ sessionID: "ses_test", inputID: "msg_queue" })
expect(requests).toEqual([
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/pending/msg_cancel" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_steer/steer" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_queue/queue" },
])
})
test("event.subscribe exposes the Promise event stream wire projection", async () => { test("event.subscribe exposes the Promise event stream wire projection", async () => {
const client = OpenCode.make({ const client = OpenCode.make({
baseUrl: "http://localhost:3000", baseUrl: "http://localhost:3000",

Some files were not shown because too many files have changed in this diff Show More