Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline 6aaaac27d0 fix(llm): preserve native provider options 2026-05-23 19:33:16 -05:00
81 changed files with 1645 additions and 1553 deletions
+14 -38
View File
@@ -199,15 +199,15 @@
},
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
@@ -409,15 +409,15 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
@@ -810,7 +810,7 @@
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
@@ -832,9 +832,9 @@
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.131", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/google": "3.0.75", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Oj1X8p0rVgvEoR5OOSxWi6XgzJ3QDlE/n30MZVtpKkCiToYYDyvlvVDGXz3IqhMyUev2JhlcuUk1brScKT01kA=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="],
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
@@ -5090,13 +5090,9 @@
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
@@ -5112,17 +5108,11 @@
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="],
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -6016,12 +6006,6 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6032,14 +6016,6 @@
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-szOnLhAI4d3O4NaQvCRBufklmtRU8AR8p5LyFxjlTXo=",
"aarch64-linux": "sha256-mfyL4Pswl6HeObPg+TMK1r7RjvLCMgDWYrFnfHkXGj4=",
"aarch64-darwin": "sha256-XMK5IFCIxlSwH21Nqya8ZkAQ4Skp6GghukIl7NMp+so=",
"x86_64-darwin": "sha256-W7nLMO6qhR32442yMebEJJgaZRNpqTbZ8BJhdqMK3S4="
"x86_64-linux": "sha256-qq8cKNcW10NRbPx2T/Gpzby4nx2qHzj0LWDwqyPl7rY=",
"aarch64-linux": "sha256-UguK4nObAL/ovGZQousveoXCmkcxEgWy7XSSzxVJNEY=",
"aarch64-darwin": "sha256-NibSqZiF0XMbFhOtl2CQvFe4N/BXB5UAdt2iKv7dIcI=",
"x86_64-darwin": "sha256-ZNeZSpYitdUjXx/sULAIoLXrM6+Vz+GXQxKpdfgXh60="
}
}
+3 -3
View File
@@ -27,15 +27,15 @@
},
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
+1 -4
View File
@@ -3,10 +3,9 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
import { realpathSync } from "fs"
import * as NFS from "fs/promises"
import { lookup } from "mime-types"
import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
import type { PlatformError } from "effect/PlatformError"
import { Glob } from "./util/glob"
import { serviceUse } from "./effect/service-use"
export namespace AppFileSystem {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
@@ -40,8 +39,6 @@ export namespace AppFileSystem {
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
export const use = serviceUse(Service)
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
+1 -1
View File
@@ -108,7 +108,7 @@ export const layer = Layer.effect(
if (!repo) return { id: ID.global, directory: input, vcs: undefined }
const previous = yield* cached(repo.store)
const id = previous ?? (yield* root(repo))
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
return {
previous,
+34 -6
View File
@@ -5,11 +5,16 @@ import path from "path"
import { Effect } from "effect"
import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Hash } from "@opencode-ai/core/util/hash"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
const it = testEffect(Project.defaultLayer)
function remoteID(remote: string) {
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
}
function abs(value: string) {
return AbsolutePath.make(value)
}
@@ -86,7 +91,7 @@ describe("ProjectV2.resolve", () => {
}),
)
it.live("uses root commit when origin exists", () =>
it.live("prefers normalized origin over root commit", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -97,13 +102,36 @@ describe("ProjectV2.resolve", () => {
const result = yield* project.resolve(abs(tmp.path))
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
expect(result.id).toBe(remoteID("github.com/Acme/App"))
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
expect(result.directory).toBe(yield* real(tmp.path))
expect(result.vcs?.type).toBe("git")
}),
)
it.live("uses root commit when local remote exists", () =>
it.live("normalizes ssh and https remotes to the same id", () =>
Effect.gen(function* () {
const ssh = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const https = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
const project = yield* Project.Service
const a = yield* project.resolve(abs(ssh.path))
const b = yield* project.resolve(abs(https.path))
expect(a.id).toBe(remoteID("github.com/owner/repo"))
expect(b.id).toBe(a.id)
}),
)
it.live("ignores file remotes and falls back to root commit", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -118,7 +146,7 @@ describe("ProjectV2.resolve", () => {
}),
)
it.live("prefers previous cached id over origin", () =>
it.live("returns previous cached id from common dir", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
@@ -131,7 +159,7 @@ describe("ProjectV2.resolve", () => {
const result = yield* project.resolve(abs(tmp.path))
expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo"))
}),
)
@@ -185,7 +213,7 @@ describe("ProjectV2.resolve", () => {
expect(result.directory).toBe(yield* real(worktree))
expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo"))
expect(result.vcs?.type).toBe("git")
}),
)
@@ -20,6 +20,7 @@ import {
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import * as Cache from "./utils/cache"
import { Lifecycle } from "./utils/lifecycle"
import { ProviderOptions } from "./utils/provider-options"
import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "anthropic-messages"
@@ -136,6 +137,7 @@ const AnthropicTool = Schema.Struct({
description: Schema.String,
input_schema: JsonObject,
cache_control: Schema.optional(AnthropicCacheControl),
eager_input_streaming: Schema.optional(Schema.Boolean),
})
type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
@@ -144,10 +146,10 @@ const AnthropicToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
])
const AnthropicThinking = Schema.Struct({
type: Schema.tag("enabled"),
budget_tokens: Schema.Number,
})
// Anthropic accepts several `thinking` shapes (enabled with `budget_tokens`,
// adaptive with optional `display`, and disabled). The body schema permits the
// full union so explicit lowering can pick the correct fields per model.
const AnthropicThinkingBody = Schema.Record(Schema.String, Schema.Unknown)
const AnthropicBodyFields = {
model: Schema.String,
@@ -161,9 +163,12 @@ const AnthropicBodyFields = {
top_p: Schema.optional(Schema.Number),
top_k: Schema.optional(Schema.Number),
stop_sequences: optionalArray(Schema.String),
thinking: Schema.optional(AnthropicThinking),
thinking: Schema.optional(AnthropicThinkingBody),
}
const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
// Unknown provider options pass through verbatim with top-level keys snake-cased.
const AnthropicMessagesBody = Schema.StructWithRest(Schema.Struct(AnthropicBodyFields), [
Schema.Record(Schema.String, Schema.Any),
])
export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
const AnthropicUsage = Schema.Struct({
@@ -254,11 +259,16 @@ const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string |
return typeof anthropic.signature === "string" ? anthropic.signature : undefined
}
const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition): AnthropicTool => ({
const lowerTool = (
breakpoints: Cache.Breakpoints,
tool: ToolDefinition,
eagerInputStreaming: boolean | undefined,
): AnthropicTool => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
cache_control: cacheControl(breakpoints, tool.cache),
eager_input_streaming: eagerInputStreaming ? true : undefined,
})
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
@@ -413,24 +423,46 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
return messages
})
const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
// Typed AI SDK Anthropic options. Mirrors the subset of
// `anthropicLanguageModelOptions` that opencode's provider transform actually
// emits today (see `packages/opencode/src/provider/transform.ts`). Unknown
// keys flow through the index signature and pass through to the wire body
// with their top-level key snake-cased.
type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max"
type AnthropicThinking =
| { readonly type: "enabled"; readonly budgetTokens?: number; readonly budget_tokens?: number }
| { readonly type: "adaptive"; readonly display?: "omitted" | "summarized" }
| { readonly type: "disabled" }
const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
const thinking = anthropicOptions(request)?.thinking
if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
const budget =
typeof thinking.budgetTokens === "number"
? thinking.budgetTokens
: typeof thinking.budget_tokens === "number"
? thinking.budget_tokens
: undefined
if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
interface AnthropicOptions {
readonly thinking?: AnthropicThinking
readonly effort?: AnthropicEffort
readonly toolStreaming?: boolean
readonly [extra: string]: unknown
}
const ANTHROPIC_KNOWN_KEYS: ReadonlySet<string> = new Set(["thinking", "effort", "toolStreaming"])
const lowerThinking = (thinking: AnthropicOptions["thinking"]) => {
if (thinking === undefined) return undefined
if (thinking.type === "disabled") return undefined
if (thinking.type === "adaptive") {
return { type: "adaptive" as const, ...(thinking.display ? { display: thinking.display } : {}) }
}
const budget = thinking.budgetTokens ?? thinking.budget_tokens
if (budget === undefined) return undefined
return { type: "enabled" as const, budget_tokens: budget }
})
}
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const generation = request.generation
const options = ProviderOptions.merge(request, ["anthropic"]) as AnthropicOptions
// AI SDK's `toolStreaming` controls per-tool `eager_input_streaming`. opencode
// sets `toolStreaming: false` for non-Claude models routed through
// `@ai-sdk/anthropic`; otherwise the field is left unset so the provider
// applies its own default.
const eagerInputStreaming = options.toolStreaming === true
// Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers
// over-mark we keep their tool hints and shed the message-tail ones first.
@@ -438,7 +470,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
const tools =
request.tools.length === 0 || request.toolChoice?.type === "none"
? undefined
: request.tools.map((tool) => lowerTool(breakpoints, tool))
: request.tools.map((tool) => lowerTool(breakpoints, tool, eagerInputStreaming))
const system =
request.system.length === 0
? undefined
@@ -454,6 +486,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
)
}
return {
...ProviderOptions.passthrough(options, ANTHROPIC_KNOWN_KEYS),
model: request.model.id,
system,
messages,
@@ -465,7 +498,8 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_p: generation?.topP,
top_k: generation?.topK,
stop_sequences: generation?.stop,
thinking: yield* lowerThinking(request),
thinking: lowerThinking(options.thinking),
...(options.effort !== undefined ? { effort: options.effort } : {}),
}
})
+42 -10
View File
@@ -15,6 +15,7 @@ import {
} from "../schema"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options"
import { ProviderOptions } from "./utils/provider-options"
import { Lifecycle } from "./utils/lifecycle"
import { ToolStream } from "./utils/tool-stream"
@@ -50,6 +51,10 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
})
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall>
// `reasoning_content` is a plain string per DeepSeek/OpenAI-compatible spec.
// `reasoning_details` is an OpenRouter-style array of typed reasoning objects
// (summary / encrypted / text). We accept the structured payload as-is so it
// round-trips verbatim to the provider on continuation requests.
const OpenAIChatMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
Schema.Struct({ role: Schema.Literal("user"), content: Schema.String }),
@@ -58,6 +63,7 @@ const OpenAIChatMessage = Schema.Union([
content: Schema.NullOr(Schema.String),
tool_calls: optionalArray(OpenAIChatAssistantToolCall),
reasoning_content: Schema.optional(Schema.String),
reasoning_details: Schema.optional(Schema.Array(Schema.Record(Schema.String, Schema.Unknown))),
}),
Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }),
]).pipe(Schema.toTaggedUnion("role"))
@@ -79,7 +85,7 @@ export const bodyFields = {
stream: Schema.Literal(true),
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
store: Schema.optional(Schema.Boolean),
reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
reasoning_effort: Schema.optional(Schema.String),
max_tokens: Schema.optional(Schema.Number),
temperature: Schema.optional(Schema.Number),
top_p: Schema.optional(Schema.Number),
@@ -88,7 +94,7 @@ export const bodyFields = {
seed: Schema.optional(Schema.Number),
stop: optionalArray(Schema.String),
}
const OpenAIChatBody = Schema.Struct(bodyFields)
const OpenAIChatBody = Schema.StructWithRest(Schema.Struct(bodyFields), [Schema.Record(Schema.String, Schema.Any)])
export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>
// =============================================================================
@@ -125,9 +131,16 @@ const OpenAIChatToolCallDelta = Schema.Struct({
})
type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta>
// Streaming reasoning fields. `reasoning_content` (DeepSeek) and `reasoning`
// (AI SDK fallback) are strings; `reasoning_details` (OpenRouter) is an array
// of typed reasoning detail objects. We surface their plaintext via reasoning
// deltas and preserve the structured array for downstream round-trip.
const OpenAIChatReasoningDetail = Schema.Record(Schema.String, Schema.Unknown)
const OpenAIChatDelta = Schema.Struct({
content: optionalNull(Schema.String),
reasoning_content: optionalNull(Schema.String),
reasoning: optionalNull(Schema.String),
reasoning_details: optionalNull(Schema.Array(OpenAIChatReasoningDetail)),
tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
})
@@ -188,6 +201,16 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
const openAICompatibleReasoningContent = (native: unknown) =>
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
// `reasoning_details` rounds-trips the OpenRouter structured array. Accept the
// array shape as canonical; tolerate a string for legacy callers that already
// flattened it.
const openAICompatibleReasoningDetails = (native: unknown) => {
if (!isRecord(native)) return undefined
const value = native.reasoning_details
if (Array.isArray(value)) return value as ReadonlyArray<Record<string, unknown>>
return undefined
}
const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) {
const content: TextPart[] = []
for (const part of message.content) {
@@ -220,6 +243,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
content: content.length === 0 ? null : ProviderShared.joinText(content),
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
reasoning_content: openAICompatibleReasoningContent(message.native?.openaiCompatible),
reasoning_details: openAICompatibleReasoningDetails(message.native?.openaiCompatible),
}
})
@@ -246,13 +270,14 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
})
const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) {
const store = OpenAIOptions.store(request)
const reasoningEffort = OpenAIOptions.reasoningEffort(request)
if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort))
return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`)
const options = OpenAIOptions.options(request)
const effort = options.reasoningEffort
if (effort !== undefined && !OpenAIOptions.isReasoningEffort(effort))
return yield* invalid(`OpenAI Chat does not support reasoning effort ${effort}`)
return {
...(store !== undefined ? { store } : {}),
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
...ProviderOptions.passthrough(options, OpenAIOptions.KNOWN_KEYS),
...(options.store !== undefined ? { store: options.store } : {}),
...(effort !== undefined ? { reasoning_effort: effort } : {}),
}
})
@@ -325,8 +350,15 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
let lifecycle = state.lifecycle
if (delta?.reasoning_content)
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content)
// OpenRouter-style `reasoning_details` ships an array of typed reasoning
// objects (summary / text / encrypted). Concatenate the plaintext fields
// into the reasoning delta stream; the structured array is preserved on
// the assistant message for round-trip via `providerMetadata`.
const detailText = (delta?.reasoning_details ?? [])
.map((detail) => (typeof detail.text === "string" ? detail.text : typeof detail.summary === "string" ? detail.summary : ""))
.join("")
const reasoning = delta?.reasoning_content ?? delta?.reasoning ?? (detailText.length > 0 ? detailText : undefined)
if (reasoning) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", reasoning)
if (delta?.content) lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content)
@@ -1,22 +1,84 @@
import { Effect, Schema } from "effect"
import { Route, type RouteRoutedModelInput } from "../route/client"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol"
import type { LLMRequest } from "../schema"
import { ProviderOptions } from "./utils/provider-options"
import * as OpenAIChat from "./openai-chat"
const ADAPTER = "openai-compatible-chat"
export type OpenAICompatibleChatModelInput = RouteRoutedModelInput
const OpenAICompatibleChatBody = Schema.StructWithRest(
Schema.Struct({ ...OpenAIChat.bodyFields, reasoning_effort: Schema.optional(Schema.String) }),
[Schema.Record(Schema.String, Schema.Any)],
)
export type OpenAICompatibleChatBody = Schema.Schema.Type<typeof OpenAICompatibleChatBody>
// Typed AI SDK `@ai-sdk/openai-compatible` options. Known keys are lowered
// explicitly; everything else passes through to the wire body with its
// top-level key snake-cased.
interface CompatibleOptions {
readonly user?: string
readonly reasoningEffort?: string
readonly textVerbosity?: string
readonly strictJsonSchema?: boolean
readonly [extra: string]: unknown
}
const COMPATIBLE_KNOWN_KEYS: ReadonlySet<string> = new Set([
"user",
"reasoningEffort",
"textVerbosity",
"strictJsonSchema",
])
// Match AI SDK `@ai-sdk/openai-compatible` option resolution: the deprecated
// `openai-compatible` alias, the canonical `openaiCompatible` key, the raw
// provider name (dot-split so e.g. `opencode.internal` matches `opencode`),
// and its camelCase variant. Later sources override earlier ones.
const bodyOptions = (request: LLMRequest) => {
const provider = String(request.model.provider).split(".")[0]
const camel = provider.replace(/[_-]([a-z])/g, (_, value: string) => value.toUpperCase())
const options = ProviderOptions.merge(request, [
"openai-compatible",
"openaiCompatible",
provider,
camel,
]) as CompatibleOptions
return {
...ProviderOptions.passthrough(options, COMPATIBLE_KNOWN_KEYS),
...(options.user !== undefined ? { user: options.user } : {}),
...(options.reasoningEffort !== undefined ? { reasoning_effort: options.reasoningEffort } : {}),
...(options.textVerbosity !== undefined ? { verbosity: options.textVerbosity } : {}),
}
}
export const protocol = Protocol.make({
id: ADAPTER,
body: {
schema: OpenAICompatibleChatBody,
// Drop providerOptions before delegating so OpenAI Chat's OpenAI-only
// option validation does not reject compatible-route requests whose
// provider id happens to be `openai` or use extended reasoning efforts.
from: (request) =>
OpenAIChat.protocol.body
.from({ ...request, providerOptions: undefined })
.pipe(Effect.map((body) => ({ ...body, ...bodyOptions(request) }))),
},
stream: OpenAIChat.protocol.stream,
})
/**
* Route for non-OpenAI providers that expose an OpenAI Chat-compatible
* `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
* overrides only the route id so providers can be resolved per-family without
* colliding with native OpenAI. Provider helpers configure the route endpoint
* before model selection.
* `/chat/completions` endpoint. Reuses OpenAI Chat streaming behavior while
* allowing compatible providers to pass through additional request-body
* options such as `enable_thinking` and extended reasoning efforts.
*/
export const route = Route.make({
id: ADAPTER,
protocol: OpenAIChat.protocol,
protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: Framing.sse,
})
+83 -238
View File
@@ -19,6 +19,7 @@ import {
} from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options"
import { ProviderOptions } from "./utils/provider-options"
import { Lifecycle } from "./utils/lifecycle"
import { ToolStream } from "./utils/tool-stream"
@@ -57,11 +58,6 @@ const OpenAIResponsesReasoningItem = Schema.Struct({
encrypted_content: optionalNull(Schema.String),
})
const OpenAIResponsesItemReference = Schema.Struct({
type: Schema.tag("item_reference"),
id: Schema.String,
})
// `function_call_output.output` accepts either a plain string or an ordered
// array of content items so tools can return images in addition to text.
// https://platform.openai.com/docs/api-reference/responses/object
@@ -77,7 +73,6 @@ const OpenAIResponsesInputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
OpenAIResponsesReasoningItem,
OpenAIResponsesItemReference,
Schema.Struct({
type: Schema.tag("function_call"),
call_id: Schema.String,
@@ -92,15 +87,6 @@ const OpenAIResponsesInputItem = Schema.Union([
])
type OpenAIResponsesInputItem = Schema.Schema.Type<typeof OpenAIResponsesInputItem>
// Mutable counterpart of the schema reasoning item so `lowerMessages` can fold
// multiple streamed summary parts into the same item before flushing.
type OpenAIResponsesReasoningInput = {
type: "reasoning"
id: string
summary: Array<{ type: "summary_text"; text: string }>
encrypted_content?: string | null
}
const OpenAIResponsesTool = Schema.Struct({
type: Schema.tag("function"),
name: Schema.String,
@@ -126,12 +112,23 @@ const OpenAIResponsesCoreFields = {
tools: optionalArray(OpenAIResponsesTool),
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
store: Schema.optional(Schema.Boolean),
conversation: Schema.optional(Schema.String),
max_tool_calls: Schema.optional(Schema.Number),
metadata: Schema.optional(JsonObject),
parallel_tool_calls: Schema.optional(Schema.Boolean),
previous_response_id: Schema.optional(Schema.String),
prompt_cache_key: Schema.optional(Schema.String),
include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
prompt_cache_retention: Schema.optional(Schema.String),
safety_identifier: Schema.optional(Schema.String),
service_tier: Schema.optional(Schema.String),
top_logprobs: Schema.optional(Schema.Number),
truncation: Schema.optional(Schema.String),
user: Schema.optional(Schema.String),
include: optionalArray(Schema.String),
reasoning: Schema.optional(
Schema.Struct({
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
summary: Schema.optional(Schema.Literal("auto")),
summary: Schema.optional(Schema.String),
}),
),
text: Schema.optional(
@@ -144,10 +141,15 @@ const OpenAIResponsesCoreFields = {
top_p: Schema.optional(Schema.Number),
}
const OpenAIResponsesBody = Schema.Struct({
...OpenAIResponsesCoreFields,
stream: Schema.Literal(true),
})
// Unknown provider options are passed through verbatim with their top-level
// key snake-cased; the rest record validates them against any JSON value.
const OpenAIResponsesBody = Schema.StructWithRest(
Schema.Struct({
...OpenAIResponsesCoreFields,
stream: Schema.Literal(true),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(
@@ -208,7 +210,6 @@ const OpenAIResponsesEvent = Schema.Struct({
type: Schema.String,
delta: Schema.optional(Schema.String),
item_id: Schema.optional(Schema.String),
summary_index: Schema.optional(Schema.Number),
item: Schema.optional(OpenAIResponsesStreamItem),
response: Schema.optional(
Schema.StructWithRest(
@@ -232,18 +233,6 @@ interface ParserState {
readonly tools: ToolStream.State<string>
readonly hasFunctionCall: boolean
readonly lifecycle: Lifecycle.State
readonly reasoningItems: Readonly<Record<string, ReasoningStreamItem>>
readonly store: boolean | undefined
}
type ReasoningSummaryStatus = "active" | "can-conclude" | "concluded"
interface ReasoningStreamItem {
readonly encryptedContent: string | null | undefined
// Keyed by OpenAI's numeric `summary_index`. JS object keys coerce to
// strings, but typing the map as `Record<number, ...>` documents intent
// and matches the wire field.
readonly summaryParts: Readonly<Record<number, ReasoningSummaryStatus>>
}
const invalid = ProviderShared.invalidRequest
@@ -273,21 +262,22 @@ const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
arguments: ProviderShared.encodeJson(part.input),
})
const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => {
const lowerReasoning = (part: ReasoningPart, store: boolean | undefined): OpenAIResponsesInputItem | undefined => {
const openai = part.providerMetadata?.openai
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
return undefined
const encryptedContent =
typeof openai.reasoningEncryptedContent === "string"
? openai.reasoningEncryptedContent
: openai.reasoningEncryptedContent === null
? null
: undefined
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string") return undefined
// With store:false, OpenAI only accepts previous reasoning items when the
// encrypted state is present. Bare rs_* ids point to non-persisted items.
if (store === false && typeof openai.reasoningEncryptedContent !== "string") return undefined
return {
type: "reasoning",
id: openai.itemId,
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
encrypted_content: encryptedContent,
encrypted_content:
typeof openai.reasoningEncryptedContent === "string"
? openai.reasoningEncryptedContent
: openai.reasoningEncryptedContent === null
? null
: undefined,
}
}
@@ -320,14 +310,15 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")
// Text/json/error results are encoded as a plain string for backward
// compatibility with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
return yield* Effect.forEach(part.result.value, lowerToolResultContentItem)
const items: ReadonlyArray<ToolResultContentPart> = part.result.value
return yield* Effect.forEach(items, lowerToolResultContentItem)
})
const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) {
const system: OpenAIResponsesInputItem[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
const input: OpenAIResponsesInputItem[] = [...system]
const store = OpenAIOptions.store(request)
const store = OpenAIOptions.options(request).store
for (const message of request.messages) {
if (message.role === "user") {
@@ -337,8 +328,6 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
if (message.role === "assistant") {
const content: TextPart[] = []
const reasoningItems: Record<string, OpenAIResponsesReasoningInput> = {}
const reasoningReferences = new Set<string>()
const flushText = () => {
if (content.length === 0) return
input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) })
@@ -351,22 +340,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
}
if (part.type === "reasoning") {
flushText()
const reasoning = lowerReasoning(part)
if (!reasoning) continue
if (store !== false && reasoning.id) {
if (!reasoningReferences.has(reasoning.id)) input.push({ type: "item_reference", id: reasoning.id })
reasoningReferences.add(reasoning.id)
continue
}
const existing = reasoningItems[reasoning.id]
if (existing) {
existing.summary.push(...reasoning.summary)
if (typeof reasoning.encrypted_content === "string")
existing.encrypted_content = reasoning.encrypted_content
continue
}
reasoningItems[reasoning.id] = reasoning
input.push(reasoning)
const reasoning = lowerReasoning(part, store)
if (reasoning) input.push(reasoning)
continue
}
if (part.type === "tool-call") {
@@ -395,35 +370,50 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
}
}
// With store:false, OpenAI only accepts previous reasoning items when the
// complete item has encrypted state. Summary blocks for one item may carry
// that state only on the last block, so filter after they have been joined.
return store === false
? input.filter(
(item) => !("type" in item) || item.type !== "reasoning" || typeof item.encrypted_content === "string",
)
: input
return input
})
const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
const store = OpenAIOptions.store(request)
const promptCacheKey = OpenAIOptions.promptCacheKey(request)
const effort = OpenAIOptions.reasoningEffort(request)
if (effort && !OpenAIOptions.isReasoningEffort(effort))
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
const summary = OpenAIOptions.reasoningSummary(request)
const include = OpenAIOptions.include(request)
const verbosity = OpenAIOptions.textVerbosity(request)
const instructions = OpenAIOptions.instructions(request)
const lowerOptions = (request: LLMRequest) => {
const options = OpenAIOptions.options(request)
// OpenAI Responses does not accept the `max` reasoning effort variant.
const effort = OpenAIOptions.isReasoningEffort(options.reasoningEffort) ? options.reasoningEffort : undefined
const summary = options.reasoningSummary
const verbosity = options.textVerbosity
// `logprobs` is enabled only by `true` or a numeric top-N. `false` and
// `undefined` leave the request without the logprobs include + top_logprobs.
const logprobsEnabled = options.logprobs === true || typeof options.logprobs === "number"
const include = (() => {
const base = options.include ? [...options.include] : []
if (options.includeEncryptedReasoning && !base.includes("reasoning.encrypted_content")) {
base.push("reasoning.encrypted_content")
}
if (logprobsEnabled && !base.includes("message.output_text.logprobs")) {
base.push("message.output_text.logprobs")
}
return base.length > 0 ? base : undefined
})()
const topLogprobs = typeof options.logprobs === "number" ? options.logprobs : options.logprobs === true ? 20 : undefined
return {
...(instructions ? { instructions } : {}),
...(store !== undefined ? { store } : {}),
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
...ProviderOptions.passthrough(options, OpenAIOptions.KNOWN_KEYS),
...(options.instructions !== undefined ? { instructions: options.instructions } : {}),
...(options.store !== undefined ? { store: options.store } : {}),
...(options.conversation !== undefined ? { conversation: options.conversation } : {}),
...(options.maxToolCalls !== undefined ? { max_tool_calls: options.maxToolCalls } : {}),
...(options.metadata !== undefined ? { metadata: options.metadata } : {}),
...(options.parallelToolCalls !== undefined ? { parallel_tool_calls: options.parallelToolCalls } : {}),
...(options.previousResponseId !== undefined ? { previous_response_id: options.previousResponseId } : {}),
...(options.promptCacheKey !== undefined ? { prompt_cache_key: options.promptCacheKey } : {}),
...(options.promptCacheRetention !== undefined ? { prompt_cache_retention: options.promptCacheRetention } : {}),
...(options.safetyIdentifier !== undefined ? { safety_identifier: options.safetyIdentifier } : {}),
...(options.serviceTier !== undefined ? { service_tier: options.serviceTier } : {}),
...(topLogprobs !== undefined ? { top_logprobs: topLogprobs } : {}),
...(options.truncation !== undefined ? { truncation: options.truncation } : {}),
...(options.user !== undefined ? { user: options.user } : {}),
...(include ? { include } : {}),
...(effort || summary ? { reasoning: { effort, summary } } : {}),
...(verbosity ? { text: { verbosity } } : {}),
...(effort !== undefined || summary !== undefined ? { reasoning: { effort, summary } } : {}),
...(verbosity !== undefined ? { text: { verbosity } } : {}),
}
})
}
const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) {
const generation = request.generation
@@ -436,7 +426,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
max_output_tokens: generation?.maxTokens,
temperature: generation?.temperature,
top_p: generation?.topP,
...(yield* lowerOptions(request)),
...lowerOptions(request),
}
})
@@ -567,51 +557,24 @@ const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): Ste
const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
if (!event.delta) return [state, NO_EVENTS]
const events: LLMEvent[] = []
const itemID = event.item_id ?? "reasoning-0"
const id =
event.summary_index !== undefined || state.reasoningItems[itemID] ? `${itemID}:${event.summary_index ?? 0}` : itemID
return [
{
...state,
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, event.item_id ?? "reasoning-0", event.delta),
},
events,
]
}
// The summary done event does not carry encrypted continuation state. Finish the
// common reasoning block when the full reasoning item arrives in output_item.done.
const onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS]
const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null })
// OpenAI Responses streams reasoning items in a stable order:
// `output_item.added` (reasoning) →
// `reasoning_summary_part.added` (index=0) →
// `reasoning_summary_text.delta` →
// `reasoning_summary_part.done` (index=0) →
// (repeat for index>0) →
// `output_item.done` (reasoning).
// The handlers below rely on this ordering: `onOutputItemAdded` seeds the
// per-item entry, `onReasoningSummaryPartAdded` for `summary_index === 0`
// short-circuits when the entry already exists, and higher-index handlers
// fold against the same entry. Behaviour for out-of-order events is
// best-effort, not guaranteed.
const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
const item = event.item
if (item && isReasoningItem(item)) {
const events: LLMEvent[] = []
return [
{
...state,
lifecycle: Lifecycle.reasoningStart(state.lifecycle, events, `${item.id}:0`, reasoningMetadata(item)),
reasoningItems: {
...state.reasoningItems,
[item.id]: { encryptedContent: item.encrypted_content, summaryParts: { 0: "active" } },
},
},
events,
]
}
if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS]
const providerMetadata = openaiMetadata({ itemId: item.id })
const events: LLMEvent[] = []
@@ -632,103 +595,6 @@ const onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): Ste
]
}
const onReasoningSummaryPartAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
const item = state.reasoningItems[event.item_id] ?? { encryptedContent: undefined, summaryParts: {} }
if (event.summary_index === 0) {
if (state.reasoningItems[event.item_id]) return [state, NO_EVENTS]
const events: LLMEvent[] = []
return [
{
...state,
lifecycle: Lifecycle.reasoningStart(
state.lifecycle,
events,
`${event.item_id}:0`,
openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: null }),
),
reasoningItems: {
...state.reasoningItems,
[event.item_id]: { ...item, summaryParts: { 0: "active" } },
},
},
events,
]
}
const events: LLMEvent[] = []
const closed = Object.entries(item.summaryParts)
.filter((entry) => entry[1] === "can-conclude")
.reduce(
(lifecycle, entry) =>
Lifecycle.reasoningEnd(
lifecycle,
events,
`${event.item_id}:${entry[0]}`,
openaiMetadata({ itemId: event.item_id }),
),
state.lifecycle,
)
return [
{
...state,
lifecycle: Lifecycle.reasoningStart(
closed,
events,
`${event.item_id}:${event.summary_index}`,
openaiMetadata({ itemId: event.item_id, reasoningEncryptedContent: item.encryptedContent ?? null }),
),
reasoningItems: {
...state.reasoningItems,
[event.item_id]: {
...item,
summaryParts: {
...Object.fromEntries(
Object.entries(item.summaryParts).map((entry) =>
entry[1] === "can-conclude" ? [entry[0], "concluded" as const] : entry,
),
),
[event.summary_index]: "active",
},
},
},
},
events,
]
}
const onReasoningSummaryPartDone = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
if (!event.item_id || event.summary_index === undefined) return [state, NO_EVENTS]
const item = state.reasoningItems[event.item_id]
if (!item) return [state, NO_EVENTS]
const events: LLMEvent[] = []
return [
{
...state,
lifecycle:
state.store !== false
? Lifecycle.reasoningEnd(
state.lifecycle,
events,
`${event.item_id}:${event.summary_index}`,
openaiMetadata({ itemId: event.item_id }),
)
: state.lifecycle,
reasoningItems: {
...state.reasoningItems,
[event.item_id]: {
...item,
summaryParts: {
...item.summaryParts,
[event.summary_index]: state.store !== false ? "concluded" : "can-conclude",
},
},
},
},
events,
]
}
const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (
state: ParserState,
event: OpenAIResponsesEvent,
@@ -789,17 +655,6 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
if (isReasoningItem(item)) {
const events: LLMEvent[] = []
const providerMetadata = reasoningMetadata(item)
const reasoningItem = state.reasoningItems[item.id]
if (reasoningItem) {
const lifecycle = Object.entries(reasoningItem.summaryParts)
.filter((entry) => entry[1] === "active" || entry[1] === "can-conclude")
.reduce(
(lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, providerMetadata),
state.lifecycle,
)
const { [item.id]: _removed, ...reasoningItems } = state.reasoningItems
return [{ ...state, lifecycle, reasoningItems }, events] satisfies StepResult
}
if (!state.lifecycle.reasoning.has(item.id)) {
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }))
@@ -868,10 +723,6 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => {
event.type === "response.reasoning_summary_text.done"
)
return Effect.succeed(onReasoningDone(state, event))
if (event.type === "response.reasoning_summary_part.added")
return Effect.succeed(onReasoningSummaryPartAdded(state, event))
if (event.type === "response.reasoning_summary_part.done")
return Effect.succeed(onReasoningSummaryPartDone(state, event))
if (event.type === "response.output_item.added") return Effect.succeed(onOutputItemAdded(state, event))
if (event.type === "response.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event)
if (event.type === "response.output_item.done") return onOutputItemDone(state, event)
@@ -898,13 +749,7 @@ export const protocol = Protocol.make({
},
stream: {
event: Protocol.jsonEvent(OpenAIResponsesEvent),
initial: (request) => ({
hasFunctionCall: false,
tools: ToolStream.empty<string>(),
lifecycle: Lifecycle.initial(),
reasoningItems: {},
store: OpenAIOptions.store(request),
}),
initial: () => ({ hasFunctionCall: false, tools: ToolStream.empty<string>(), lifecycle: Lifecycle.initial() }),
step,
terminal: (event) => TERMINAL_TYPES.has(event.type),
},
+7 -15
View File
@@ -24,22 +24,14 @@ export const textDelta = (state: State, events: LLMEvent[], id: string, text: st
return { ...stepped, text: new Set([...stepped.text, id]) }
}
export const reasoningStart = (
state: State,
events: LLMEvent[],
id: string,
providerMetadata?: ProviderMetadata,
): State => {
if (state.reasoning.has(id)) return state
const stepped = stepStart(state, events)
events.push(LLMEvent.reasoningStart({ id, providerMetadata }))
return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
}
export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => {
const started = reasoningStart(state, events, id)
events.push(LLMEvent.reasoningDelta({ id, text }))
return started
const stepped = stepStart(state, events)
if (stepped.reasoning.has(id)) {
events.push(LLMEvent.reasoningDelta({ id, text }))
return stepped
}
events.push(LLMEvent.reasoningStart({ id }), LLMEvent.reasoningDelta({ id, text }))
return { ...stepped, reasoning: new Set([...stepped.reasoning, id]) }
}
export const reasoningEnd = (
@@ -1,84 +1,73 @@
import { Schema } from "effect"
import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema"
import { ReasoningEfforts, TextVerbosity } from "../../schema"
import type { LLMRequest } from "../../schema"
import { ReasoningEfforts, TextVerbosity, type ReasoningEffort } from "../../schema"
import { ProviderOptions } from "./provider-options"
export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
(effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max",
)
export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number]
// Mirrors OpenAI's `ResponseIncludable` union from the official SDK. Keep this
// in lockstep with `openai-node/src/resources/responses/responses.ts`.
export const OpenAIResponseIncludables = [
"file_search_call.results",
"web_search_call.results",
"web_search_call.action.sources",
"message.input_image.image_url",
"computer_call_output.output.image_url",
"code_interpreter_call.outputs",
"reasoning.encrypted_content",
"message.output_text.logprobs",
] as const
export type OpenAIResponseIncludable = (typeof OpenAIResponseIncludables)[number]
const REASONING_EFFORTS = new Set<string>(ReasoningEfforts)
const OPENAI_REASONING_EFFORTS = new Set<string>(OpenAIReasoningEfforts)
const TEXT_VERBOSITY = new Set<string>(["low", "medium", "high"])
const INCLUDABLES = new Set<string>(OpenAIResponseIncludables)
export const OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts)
export const OpenAITextVerbosity = TextVerbosity
export const OpenAIResponseIncludable = Schema.Literals(OpenAIResponseIncludables)
const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort =>
typeof effort === "string" && REASONING_EFFORTS.has(effort)
export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort =>
typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort)
const isTextVerbosity = (value: unknown): value is TextVerbosityValue =>
typeof value === "string" && TEXT_VERBOSITY.has(value)
const options = (request: LLMRequest) => request.providerOptions?.openai
export const store = (request: LLMRequest): boolean | undefined => {
const value = options(request)?.store
return typeof value === "boolean" ? value : undefined
// Typed AI SDK OpenAI options. Mirrors the camelCase surface AI SDK accepts.
// Known keys are typed; everything else passes through to the wire body with
// its top-level key snake-cased.
export interface Options {
readonly store?: boolean
readonly promptCacheKey?: string
readonly promptCacheRetention?: string
readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: string
readonly textVerbosity?: "low" | "medium" | "high"
readonly include?: ReadonlyArray<string>
readonly includeEncryptedReasoning?: boolean
readonly instructions?: string
readonly conversation?: string
readonly maxToolCalls?: number
readonly metadata?: Record<string, unknown>
readonly parallelToolCalls?: boolean
readonly previousResponseId?: string
readonly safetyIdentifier?: string
readonly serviceTier?: string
readonly logprobs?: boolean | number
readonly truncation?: string
readonly user?: string
readonly [extra: string]: unknown
}
export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => {
const value = options(request)?.reasoningEffort
return isAnyReasoningEffort(value) ? value : undefined
}
export const KNOWN_KEYS: ReadonlySet<string> = new Set([
"store",
"promptCacheKey",
"promptCacheRetention",
"reasoningEffort",
"reasoningSummary",
"textVerbosity",
"include",
"includeEncryptedReasoning",
"instructions",
"conversation",
"maxToolCalls",
"metadata",
"parallelToolCalls",
"previousResponseId",
"safetyIdentifier",
"serviceTier",
"logprobs",
"truncation",
"user",
])
export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>
options(request)?.reasoningSummary === "auto" ? "auto" : undefined
// Resolve the OpenAI Responses `include` field. Filters out unknown
// includable values defensively so a typo in upstream config drops the
// invalid entry instead of poisoning the wire body. An empty array (either
// passed directly or produced by filtering) is treated as "no include" and
// returns undefined so the request body omits the field entirely.
export const include = (request: LLMRequest): ReadonlyArray<OpenAIResponseIncludable> | undefined => {
const value = options(request)?.include
if (!Array.isArray(value)) return undefined
const filtered = value.filter((entry): entry is OpenAIResponseIncludable => INCLUDABLES.has(entry))
return filtered.length > 0 ? filtered : undefined
}
export const promptCacheKey = (request: LLMRequest) => {
const value = options(request)?.promptCacheKey
return typeof value === "string" ? value : undefined
}
export const textVerbosity = (request: LLMRequest) => {
const value = options(request)?.textVerbosity
return isTextVerbosity(value) ? value : undefined
}
export const instructions = (request: LLMRequest) => {
const value = options(request)?.instructions
return typeof value === "string" ? value : undefined
}
// Read the merged `openai` provider option bag. Producers
// (`packages/opencode/src/provider/transform.ts`) emit typed values; we widen
// only the index signature so passthrough keys remain reachable. Invalid
// shapes surface in the lowerer where they're consumed, not at decode time.
export const options = (request: LLMRequest): Options => ProviderOptions.merge(request, ["openai"]) as Options
export * as OpenAIOptions from "./openai-options"
@@ -0,0 +1,34 @@
import type { LLMRequest } from "../../schema"
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value)
// Convert a single top-level option key from camelCase to snake_case. Values
// are left verbatim — recursive conversion would mangle structured payloads
// (IDs, nested provider-shaped objects) and provider APIs do not require it.
// PascalCase (`FooBar`) becomes `foo_bar` without a leading underscore.
export const snakeKey = (key: string) =>
key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()
// Merge provider option namespaces using AI SDK precedence semantics: later
// sources override earlier ones, missing namespaces are skipped. Used by every
// native protocol that reads request-level provider options.
export const merge = (request: LLMRequest, keys: ReadonlyArray<string>) => {
const sources = keys.map((key) => request.providerOptions?.[key]).filter(isRecord)
return Object.assign({}, ...sources) as Record<string, unknown>
}
// Spread the unknown remainder of a merged option bag onto a provider body.
// `consumed` lists keys already lowered explicitly so they aren't duplicated
// or echoed at the wrong shape.
export const passthrough = (options: Record<string, unknown>, consumed: ReadonlySet<string>) => {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(options)) {
if (consumed.has(key)) continue
if (value === undefined) continue
result[snakeKey(key)] = value
}
return result
}
export * as ProviderOptions from "./provider-options"
+2 -14
View File
@@ -1,8 +1,5 @@
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
import { mergeProviderOptions } from "../schema"
import type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
export type { OpenAIResponseIncludable } from "../protocols/utils/openai-options"
export interface OpenAIOptionsInput {
readonly [key: string]: unknown
@@ -10,10 +7,7 @@ export interface OpenAIOptionsInput {
readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto"
// OpenAI Responses `include` wire field. Mirrors the official SDK's
// `ResponseIncludable[]` union exactly so AI SDK callers and direct
// native-SDK callers share one shape and no translation is required.
readonly include?: ReadonlyArray<OpenAIResponseIncludable>
readonly includeEncryptedReasoning?: boolean
readonly textVerbosity?: TextVerbosity
}
@@ -31,7 +25,7 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary,
include: options?.include,
includeEncryptedReasoning: options?.includeEncryptedReasoning,
textVerbosity: options?.textVerbosity,
}),
)
@@ -48,12 +42,6 @@ export const gpt5DefaultOptions = (
return openAIProviderOptions({
reasoningEffort: "medium",
reasoningSummary: "auto",
// GPT-5 reasoning models are configured stateless (`store: false`) by
// `openAIDefaultOptions` below, so the only way a follow-up turn can
// carry reasoning state is via the encrypted reasoning include. Without
// this, callers using the default model facade get reasoning summaries
// they cannot replay statelessly.
include: ["reasoning.encrypted_content"],
textVerbosity:
options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat")
? "low"
+1 -1
View File
@@ -5,7 +5,7 @@ import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
export type { OpenAIOptionsInput } from "./openai-options"
export const id = ProviderID.make("openai")
+1 -1
View File
@@ -283,7 +283,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
)
return events.pipe(
Stream.mapAccumEffect(
() => protocol.stream.initial(request),
protocol.stream.initial,
protocol.stream.step,
protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined,
),
+2 -2
View File
@@ -52,8 +52,8 @@ export interface ProtocolBody<Body> {
export interface ProtocolStream<Frame, Event, State> {
/** Schema for one decoded streaming event, decoded from a transport frame. */
readonly event: Schema.Codec<Event, Frame>
/** Initial parser state. Called once per response with the resolved request. */
readonly initial: (request: LLMRequest) => State
/** Initial parser state. Called once per response. */
readonly initial: () => State
/** Translate one event into emitted `LLMEvent`s plus the next state. */
readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
/** Optional request-completion signal for transports that do not end naturally. */
+1 -1
View File
@@ -97,7 +97,7 @@ export function continuationRequest(input: {
tools: features.has("tool-call") ? [continuationTool] : [],
cache: "none",
providerOptions: features.has("encrypted-reasoning")
? { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } }
? { openai: { store: false, includeEncryptedReasoning: true, reasoningSummary: "auto" } }
: undefined,
generation: { maxTokens: 80, temperature: 0 },
})
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -209,6 +209,80 @@ describe("Anthropic Messages route", () => {
}),
)
it.effect("lowers Anthropic thinking provider option (enabled)", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "think",
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 12345 } } },
}),
)
expect(prepared.body).toMatchObject({ thinking: { type: "enabled", budget_tokens: 12345 } })
}),
)
it.effect("lowers Anthropic adaptive thinking with effort sibling", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "think",
providerOptions: { anthropic: { thinking: { type: "adaptive", display: "summarized" }, effort: "max" } },
}),
)
expect(prepared.body).toMatchObject({
thinking: { type: "adaptive", display: "summarized" },
effort: "max",
})
}),
)
it.effect("sets per-tool eager_input_streaming only when toolStreaming is true", () =>
Effect.gen(function* () {
const off = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({
model,
prompt: "use tool",
providerOptions: { anthropic: { toolStreaming: false } },
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object" } }],
}),
)
expect(off.body.tools?.[0]?.eager_input_streaming).toBeUndefined()
const on = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
LLM.request({
model,
prompt: "use tool",
providerOptions: { anthropic: { toolStreaming: true } },
tools: [{ name: "lookup", description: "lookup", inputSchema: { type: "object" } }],
}),
)
expect(on.body.tools?.[0]?.eager_input_streaming).toBe(true)
}),
)
it.effect("passes unknown Anthropic provider options through with snake-cased keys", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
prompt: "go",
providerOptions: {
anthropic: {
anthropicBeta: ["claude-2024-07-15"],
customField: { keepCamelCase: true },
},
},
}),
)
expect(prepared.body).toMatchObject({
anthropic_beta: ["claude-2024-07-15"],
custom_field: { keepCamelCase: true },
})
}),
)
it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
@@ -6,7 +6,7 @@ import { Auth, LLMClient } from "../../src/route"
import * as OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { dynamicResponse, fixedResponse } from "../lib/http"
import { sseEvents } from "../lib/sse"
const Json = Schema.fromJsonString(Schema.Unknown)
@@ -199,6 +199,134 @@ describe("OpenAI-compatible Chat route", () => {
}),
)
it.effect("passes through compatible options and prior reasoning for tool continuations", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
providerOptions: {
deepseek: {
reasoningEffort: "max",
textVerbosity: "low",
promptCacheKey: "session_123",
strictJsonSchema: false,
enable_thinking: true,
},
},
messages: [
Message.user("Audit the site"),
Message.make({
role: "assistant",
native: { openaiCompatible: { reasoning_content: "I should inspect the page." } },
content: [ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "page" } })],
}),
],
}),
)
expect(prepared.body).toMatchObject({
reasoning_effort: "max",
verbosity: "low",
prompt_cache_key: "session_123",
enable_thinking: true,
messages: [
{ role: "user", content: "Audit the site" },
{
role: "assistant",
reasoning_content: "I should inspect the page.",
tool_calls: [
{
id: "call_1",
function: { name: "lookup", arguments: '{"query":"page"}' },
},
],
},
],
})
expect(prepared.body).not.toHaveProperty("strictJsonSchema")
}),
)
it.effect("preserves structured reasoning_details on compatible continuations", () =>
Effect.gen(function* () {
const details = [
{ type: "reasoning.text", text: "Let me work through this.", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.encrypted", data: "sha256:abc123", format: "anthropic-claude-v1", index: 1 },
]
const prepared = yield* LLMClient.prepare(
LLM.request({
model,
messages: [
Message.make({
role: "assistant",
native: { openaiCompatible: { reasoning_details: details } },
content: [ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })],
}),
],
}),
)
expect(prepared.body).toMatchObject({
messages: [{ role: "assistant", reasoning_details: details }],
})
}),
)
it.effect("resolves dot-scoped compatible provider options", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: OpenAICompatibleChat.route
.with({ provider: "opencode.internal", endpoint: { baseURL: "https://api.example.test/v1" } })
.model({ id: "reasoning-model" }),
prompt: "Think.",
providerOptions: { opencode: { reasoningEffort: "max", enable_thinking: true } },
}),
)
expect(prepared.body).toMatchObject({ reasoning_effort: "max", enable_thinking: true })
}),
)
it.effect("does not apply OpenAI effort limits to compatible providers", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
LLM.request({
model: OpenAICompatibleChat.route
.with({ provider: "openai", endpoint: { baseURL: "https://compatible.example.test/v1" } })
.model({ id: "reasoning-model" }),
prompt: "Think.",
providerOptions: { openai: { reasoningEffort: "max" } },
}),
)
expect(prepared.body).toMatchObject({ reasoning_effort: "max" })
}),
)
it.effect("parses compatible reasoning field variants", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
deltaChunk({ reasoning: "fallback" }),
deltaChunk({
reasoning_details: [
{ type: "reasoning.text", text: " text-detail", format: "anthropic-claude-v1", index: 0 },
{ type: "reasoning.summary", summary: " summary-detail", format: "anthropic-claude-v1", index: 1 },
],
}),
deltaChunk({}, "stop"),
),
),
),
)
expect(response.reasoning).toBe("fallback text-detail summary-detail")
}),
)
it.effect("posts to the configured compatible endpoint and parses text usage", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
@@ -393,7 +393,7 @@ describe("OpenAI Responses route", () => {
promptCacheKey: "session_123",
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
includeEncryptedReasoning: true,
},
},
}),
@@ -407,105 +407,27 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("accepts the full ResponseIncludable union", () =>
it.effect("passes unknown OpenAI provider options through with snake-cased keys", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
prompt: "hi",
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"),
prompt: "passthrough",
providerOptions: {
openai: {
include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"],
customCamelCaseField: "value",
already_snake_case: 42,
nested: { keepCamelCase: true },
},
},
}),
)
expect(prepared.body.include).toEqual([
"reasoning.encrypted_content",
"code_interpreter_call.outputs",
"web_search_call.results",
])
}),
)
it.effect("filters unknown includable values out of the include array", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
prompt: "hi",
// The user passed one invalid entry alongside a valid one. Keep the
// valid one so the request still succeeds rather than failing on a
// typo from upstream config.
providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } },
}),
)
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
}),
)
it.effect("treats an explicit empty include as no include at all", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
)
expect(prepared.body.include).toBeUndefined()
}),
)
it.effect("treats an all-invalid include as no include at all", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
)
expect(prepared.body.include).toBeUndefined()
}),
)
it.effect("omits include when no include is set", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
)
expect(prepared.body.include).toBeUndefined()
}),
)
it.effect("requests encrypted reasoning by default for GPT-5 reasoning models", () =>
Effect.gen(function* () {
// The native OpenAI facade configures GPT-5 stateless (store: false) with
// reasoningSummary: "auto" by default. Without `include`, a follow-up
// turn cannot replay reasoning state, so the facade also opts into
// `reasoning.encrypted_content` automatically.
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi",
}),
)
expect(prepared.body.store).toBe(false)
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
expect(prepared.body.reasoning).toEqual({ effort: "medium", summary: "auto" })
}),
)
it.effect("lets callers opt out of the GPT-5 default include", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
prompt: "hi",
providerOptions: { openai: { include: [] } },
}),
)
expect(prepared.body.include).toBeUndefined()
expect(prepared.body).toMatchObject({
custom_camel_case_field: "value",
already_snake_case: 42,
nested: { keepCamelCase: true },
})
}),
)
@@ -649,94 +571,6 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("streams each reasoning summary part as a separate block", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.updateRequest(request, { providerOptions: { openai: { store: false } } }),
).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{
type: "response.output_item.added",
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
},
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" },
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 },
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" },
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 },
{
type: "response.output_item.done",
item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" },
},
{ type: "response.completed", response: { id: "resp_1" } },
),
),
),
)
expect(response.reasoning).toBe("FirstSecond")
expect(response.events).toMatchObject([
{ type: "step-start", index: 0 },
{
type: "reasoning-start",
id: "rs_1:0",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{
type: "reasoning-start",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
{
type: "reasoning-end",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
{ type: "step-finish", index: 0, reason: "stop" },
{ type: "finish", reason: "stop" },
])
}),
)
it.effect("closes reasoning summary parts when storage is not disabled", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{
type: "response.output_item.added",
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
},
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 0, delta: "First" },
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 1 },
{ type: "response.reasoning_summary_text.delta", item_id: "rs_1", summary_index: 1, delta: "Second" },
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 1 },
{
type: "response.output_item.done",
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
},
{ type: "response.completed", response: { id: "resp_1" } },
),
),
),
)
expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{ type: "reasoning-end", id: "rs_1:1", providerMetadata: { openai: { itemId: "rs_1" } } },
])
}),
)
it.effect("continues a stateless reasoning conversation", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
@@ -760,7 +594,6 @@ describe("OpenAI Responses route", () => {
]),
Message.user("Summarize it."),
],
providerOptions: { openai: { store: false } },
}),
).pipe(
Effect.provide(
@@ -818,7 +651,6 @@ describe("OpenAI Responses route", () => {
{ type: "text", text: "After." },
]),
],
providerOptions: { openai: { store: false } },
}),
)
@@ -835,66 +667,6 @@ describe("OpenAI Responses route", () => {
}),
)
it.effect("references stored reasoning items by id", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
model,
messages: [
Message.assistant([
{
type: "reasoning",
text: "Checked the previous diff.",
providerMetadata: { openai: { itemId: "rs_1" } },
},
]),
],
providerOptions: { openai: { store: true } },
}),
)
expect(prepared.body.input).toEqual([{ type: "item_reference", id: "rs_1" }])
}),
)
it.effect("joins streamed summary blocks into one continuation reasoning item", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({
id: "req_multi_summary_continuation",
model,
messages: [
Message.assistant([
{
type: "reasoning",
text: "First",
providerMetadata: { openai: { itemId: "rs_1" } },
},
{
type: "reasoning",
text: "Second",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
]),
],
providerOptions: { openai: { store: false } },
}),
)
expect(prepared.body.input).toEqual([
{
type: "reasoning",
id: "rs_1",
encrypted_content: "encrypted-state",
summary: [
{ type: "summary_text", text: "First" },
{ type: "summary_text", text: "Second" },
],
},
])
}),
)
it.effect("skips non-persisted reasoning ids without encrypted state", () =>
Effect.gen(function* () {
const prepared = yield* LLMClient.prepare(
+1 -1
View File
@@ -158,7 +158,7 @@ const normalizeImageText = (value: string) =>
const encryptedReasoningOptions = {
openai: {
store: false,
include: ["reasoning.encrypted_content"],
includeEncryptedReasoning: true,
reasoningEffort: "low",
reasoningSummary: "auto",
},
-75
View File
@@ -4,7 +4,6 @@ import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice }
import { Auth, LLMClient } from "../src/route"
import * as AnthropicMessages from "../src/protocols/anthropic-messages"
import * as OpenAIChat from "../src/protocols/openai-chat"
import * as OpenAIResponses from "../src/protocols/openai-responses"
import { tool, ToolFailure, type ToolExecuteContext } from "../src/tool"
import { ToolRuntime } from "../src/tool-runtime"
import { it } from "./lib/effect"
@@ -310,80 +309,6 @@ describe("LLMClient tools", () => {
}),
)
it.effect("replays encrypted OpenAI reasoning items with tool outputs", () =>
Effect.gen(function* () {
const bodies: unknown[] = []
const layer = dynamicResponse((input) =>
Effect.sync(() => {
bodies.push(decodeJson(input.text))
return input.respond(
bodies.length === 1
? sseEvents(
{
type: "response.output_item.added",
item: { type: "reasoning", id: "rs_1", encrypted_content: null },
},
{ type: "response.reasoning_summary_part.added", item_id: "rs_1", summary_index: 0 },
{ type: "response.reasoning_summary_part.done", item_id: "rs_1", summary_index: 0 },
{
type: "response.output_item.done",
item: { type: "reasoning", id: "rs_1", encrypted_content: "encrypted-state" },
},
{
type: "response.output_item.added",
item: {
type: "function_call",
id: "item_1",
call_id: "call_1",
name: "get_weather",
arguments: "",
},
},
{ type: "response.function_call_arguments.delta", item_id: "item_1", delta: '{"city":"Paris"}' },
{
type: "response.output_item.done",
item: {
type: "function_call",
id: "item_1",
call_id: "call_1",
name: "get_weather",
arguments: '{"city":"Paris"}',
},
},
{ type: "response.completed", response: {} },
)
: sseEvents(
{ type: "response.output_text.delta", item_id: "msg_1", delta: "Done." },
{ type: "response.completed", response: {} },
),
{ headers: { "content-type": "text/event-stream" } },
)
}),
)
yield* TestToolRuntime.runTools({
request: LLM.request({
model: OpenAIResponses.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "gpt-5.5" }),
prompt: "Use the tool.",
providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } },
}),
tools: { get_weather },
}).pipe(Stream.runCollect, Effect.provide(layer))
expect(bodies[1]).toMatchObject({
include: ["reasoning.encrypted_content"],
input: [
{ role: "user" },
{ type: "reasoning", id: "rs_1", summary: [], encrypted_content: "encrypted-state" },
{ type: "function_call", call_id: "call_1", name: "get_weather" },
{ type: "function_call_output", call_id: "call_1" },
],
})
}),
)
it.effect("emits tool-error for unknown tools so the model can self-correct", () =>
Effect.gen(function* () {
const layer = scriptedResponses([
+3 -3
View File
@@ -74,15 +74,15 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.96",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/google": "3.0.63",
"@ai-sdk/google-vertex": "4.0.112",
"@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53",
+1 -1
View File
@@ -1,5 +1,5 @@
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import {
FetchHttpClient,
HttpClient,
+1 -1
View File
@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Config } from "@/config/config"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
+1 -1
View File
@@ -5,7 +5,7 @@ import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Identifier } from "@/id/id"
import type { InstanceContext } from "@/project/instance-context"
import { InstanceRef } from "@/effect/instance-ref"
+1 -1
View File
@@ -1,5 +1,5 @@
import * as Log from "@opencode-ai/core/util/log"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import path from "path"
import { pathToFileURL } from "url"
import os from "os"
@@ -1,5 +1,5 @@
import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http"
import { Database } from "@/storage/db"
import { asc } from "drizzle-orm"
@@ -50,7 +50,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
experimentalNativeLlm: enabledByExperimental("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")),
}) {}
@@ -15,7 +15,6 @@ type ServiceUse<Identifier, Shape> = {
}
export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, Shape>) => {
const cache = new Map<string, (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>>()
// This is the only dynamic boundary: TypeScript knows the accessor shape,
// but Proxy property names are runtime values.
const access = new Proxy(
@@ -23,9 +22,7 @@ export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, S
{
get: (_, key) => {
if (typeof key !== "string") return undefined
const cached = cache.get(key)
if (cached) return cached
const accessor = (...args: unknown[]) =>
return (...args: unknown[]) =>
tag.use((service) => {
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
const method = service[key as keyof Shape]
@@ -33,8 +30,6 @@ export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, S
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods.
return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args)
})
cache.set(key, accessor)
return accessor
},
},
)
+1 -1
View File
@@ -1,5 +1,5 @@
import { Context, Effect, Layer } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
type State = Record<string, string | undefined>
+1 -1
View File
@@ -1,5 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
+1 -1
View File
@@ -1,5 +1,5 @@
import path from "path"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect, Layer, Context, Schema } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error"
+1 -1
View File
@@ -1,5 +1,5 @@
import path from "path"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Context, Option, Schema } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
+1 -1
View File
@@ -1,5 +1,5 @@
import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
@@ -1,5 +1,5 @@
import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceRef } from "@/effect/instance-ref"
import { disposeInstance as runDisposers } from "@/effect/instance-registry"
+4 -6
View File
@@ -1,5 +1,6 @@
import { and, eq, sql } from "drizzle-orm"
import { and } from "drizzle-orm"
import { Database } from "@/storage/db"
import { eq } from "drizzle-orm"
import { ProjectTable } from "./project.sql"
import { PermissionTable, SessionTable } from "../session/session.sql"
import { WorkspaceTable } from "../control-plane/workspace.sql"
@@ -19,7 +20,7 @@ import { AppProcess } from "@opencode-ai/core/process"
import { Project as ProjectV2 } from "@opencode-ai/core/project"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "project" })
@@ -219,10 +220,7 @@ export const layer = Layer.effect(
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
}
d.update(SessionTable)
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
.where(eq(SessionTable.project_id, oldID))
.run()
d.update(SessionTable).set({ project_id: newID }).where(eq(SessionTable.project_id, oldID)).run()
d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
+1 -1
View File
@@ -1,5 +1,5 @@
import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
+1 -1
View File
@@ -7,7 +7,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { Npm } from "@opencode-ai/core/npm"
import { Hash } from "@opencode-ai/core/util/hash"
import { Plugin } from "../plugin"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { type LanguageModelV3 } from "@ai-sdk/provider"
import * as ModelsDev from "@opencode-ai/core/models-dev"
import { Auth } from "../auth"
+4 -12
View File
@@ -17,11 +17,6 @@ function mimeToModality(mime: string): Modality | undefined {
export const OUTPUT_TOKEN_MAX = 32_000
// OpenAI Responses `include` value that returns the encrypted reasoning state
// needed for stateless multi-turn reasoning (store: false). Hoisted so every
// branch that requests it stays in lockstep.
const INCLUDE_ENCRYPTED_REASONING = ["reasoning.encrypted_content"] as const
export function sanitizeSurrogates(content: string) {
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
}
@@ -761,7 +756,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{
reasoningEffort: effort,
reasoningSummary: "auto",
include: INCLUDE_ENCRYPTED_REASONING,
include: ["reasoning.encrypted_content"],
},
]),
)
@@ -795,7 +790,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{
reasoningEffort: effort,
reasoningSummary: "auto",
include: INCLUDE_ENCRYPTED_REASONING,
include: ["reasoning.encrypted_content"],
},
]),
)
@@ -808,7 +803,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{
reasoningEffort: effort,
reasoningSummary: "auto",
include: INCLUDE_ENCRYPTED_REASONING,
include: ["reasoning.encrypted_content"],
},
]),
)
@@ -1139,9 +1134,6 @@ export function options(input: {
if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
result["reasoningSummary"] = "auto"
if (input.model.api.npm === "@ai-sdk/openai") {
result["include"] = INCLUDE_ENCRYPTED_REASONING
}
}
// Only set textVerbosity for non-chat gpt-5.x models
@@ -1157,7 +1149,7 @@ export function options(input: {
if (input.model.providerID.startsWith("opencode")) {
result["promptCacheKey"] = input.sessionID
result["include"] = INCLUDE_ENCRYPTED_REASONING
result["include"] = ["reasoning.encrypted_content"]
result["reasoningSummary"] = "auto"
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ import { Effect, Layer, Context, Schema } from "effect"
import * as DateTime from "effect/DateTime"
import { InstanceState } from "@/effect/instance-state"
import { isOverflow as overflow, usable } from "./overflow"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionEvent } from "@opencode-ai/core/session-event"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Provider } from "@/provider/provider"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import * as Log from "@opencode-ai/core/util/log"
import { Context, Effect, Layer } from "effect"
import * as Stream from "effect/Stream"
@@ -110,7 +110,9 @@ const messages = (input: readonly ModelMessage[]) => {
Message.make({
role: message.role,
content: content(message.content),
native: isRecord(message.providerOptions) ? { providerOptions: message.providerOptions } : undefined,
// Message provider options are already provider-native wire metadata
// (for example DeepSeek's reasoning_content continuation field).
native: isRecord(message.providerOptions) ? message.providerOptions : undefined,
}),
]
})
@@ -70,14 +70,6 @@ export function stream(input: StreamInput): StreamResult {
// Integration point with @opencode-ai/llm: native-request lowers session data
// into an LLMRequest, then LLMClient handles route selection and transport.
//
// ProviderTransform.providerOptions builds AI-SDK-shaped options for the
// selected SDK key (e.g. "openai") and the native LLM SDK reads the same
// keys via OpenAIOptions.* (store, reasoningEffort, reasoningSummary,
// include, textVerbosity, promptCacheKey). Both sides intentionally use
// OpenAI's official wire field names, so this is identity, not translation
// — if a field ever needs to differ between the two surfaces, the
// translation belongs here, not split across both packages.
const stream = input.llmClient.stream({
request: LLMNative.request({
model: input.model,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Slug } from "@opencode-ai/core/util/slug"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import path from "path"
import { BackgroundJob } from "@/background/job"
import { BusEvent } from "@/bus/bus-event"
+1 -1
View File
@@ -1,5 +1,5 @@
import type * as SDK from "@opencode-ai/sdk/v2"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Account } from "@/account/account"
+1 -1
View File
@@ -12,7 +12,7 @@ import { EventID } from "./schema"
import { Context, Effect, Layer, Schema as EffectSchema } from "effect"
import type { DeepMutable } from "@opencode-ai/core/schema"
import { EventV2 } from "@opencode-ai/core/event"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { serviceUse } from "@/effect/service-use"
import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { EffectBridge } from "@/effect/bridge"
File diff suppressed because it is too large Load Diff
@@ -62,7 +62,7 @@ describe("RuntimeFlags", () => {
expect(flags.experimentalEventSystem).toBe(true)
expect(flags.experimentalWorkspaces).toBe(true)
expect(flags.experimentalIconDiscovery).toBe(true)
expect(flags.experimentalNativeLlm).toBe(false)
expect(flags.experimentalNativeLlm).toBe(true)
expect(flags.client).toBe("desktop")
}),
)
@@ -81,13 +81,13 @@ describe("RuntimeFlags", () => {
}),
)
it.effect("enables native LLM via dedicated flag only", () =>
it.effect("enables native LLM via dedicated or umbrella flag", () =>
Effect.gen(function* () {
const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" })))
const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
expect(explicit.experimentalNativeLlm).toBe(true)
expect(umbrella.experimentalNativeLlm).toBe(false)
expect(umbrella.experimentalNativeLlm).toBe(true)
}),
)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+81 -5
View File
@@ -7,6 +7,15 @@ import path from "path"
import { tmpdirScoped } from "../fixture/fixture"
import { GlobalBus } from "../../src/bus/global"
import { ProjectID } from "../../src/project/schema"
import { Database } from "@/storage/db"
import { ProjectTable } from "@/project/project.sql"
import { SessionTable } from "@/session/session.sql"
import { PermissionTable } from "@/session/session.sql"
import { WorkspaceTable } from "@/control-plane/workspace.sql"
import { eq } from "drizzle-orm"
import { Hash } from "@opencode-ai/core/util/hash"
import { SessionID } from "@/session/schema"
import { WorkspaceID } from "@/control-plane/schema"
import { Cause, Effect, Exit, Layer, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node"
@@ -31,6 +40,10 @@ function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
})
}
function remoteProjectID(remote: string) {
return ProjectID.make(Hash.fast(`git-remote:${remote}`))
}
/**
* Creates a mock ChildProcessSpawner layer that intercepts git subcommands
* matching `failArg` and returns exit code 128, while delegating everything
@@ -154,28 +167,91 @@ describe("Project.fromDirectory", () => {
}),
)
it.live("keeps root commit identity when origin exists", () =>
it.live("prefers normalized origin remote over root commit", () =>
Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ git: true })
yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet())
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
const root = (yield* Effect.promise(() => $`git rev-list --max-parents=0 HEAD`.cwd(tmp).text())).trim()
expect(project.id).toBe(ProjectID.make(root))
expect(project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo"))
}),
)
it.live("keeps cached project identity when origin becomes available", () =>
it.live("normalizes equivalent origin URL forms to the same project ID", () =>
Effect.gen(function* () {
const ssh = yield* tmpdirScoped({ git: true })
const https = yield* tmpdirScoped({ git: true })
yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(ssh).quiet())
yield* Effect.promise(() => $`git remote add origin https://github.com/owner/repo.git`.cwd(https).quiet())
const { project: a } = yield* run((svc) => svc.fromDirectory(ssh))
const { project: b } = yield* run((svc) => svc.fromDirectory(https))
expect(a.id).toBe(remoteProjectID("github.com/owner/repo"))
expect(b.id).toBe(a.id)
}),
)
it.live("migrates cached root project data when origin becomes available", () =>
Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ git: true })
const projects = yield* Project.Service
const { project: rootProject } = yield* projects.fromDirectory(tmp)
const remoteID = remoteProjectID("github.com/acme/app")
const sessionID = crypto.randomUUID() as SessionID
const workspaceID = WorkspaceID.ascending()
yield* Effect.sync(() => {
Database.use((db) => {
db.insert(SessionTable)
.values({
id: sessionID,
project_id: rootProject.id,
slug: sessionID,
directory: tmp,
title: "test",
version: "0.0.0-test",
time_created: Date.now(),
time_updated: Date.now(),
})
.run()
db.insert(PermissionTable)
.values({
project_id: rootProject.id,
data: [{ permission: "edit", pattern: "*", action: "allow" }],
time_created: Date.now(),
time_updated: Date.now(),
})
.run()
db.insert(WorkspaceTable)
.values({
id: workspaceID,
type: "local",
name: "test",
project_id: rootProject.id,
})
.run()
})
})
yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
const { project } = yield* projects.fromDirectory(tmp)
expect(project.id).toBe(rootProject.id)
expect(project.id).toBe(remoteID)
expect(
Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get()),
).toBeUndefined()
expect(
Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())?.project_id,
).toBe(remoteID)
expect(
Database.use((db) => db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get()),
).toBeDefined()
expect(
Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get())
?.project_id,
).toBe(remoteID)
}),
)
})
@@ -271,7 +271,6 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
const model = createGpt5Model("gpt-5.2")
const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
expect(result.textVerbosity).toBe("low")
expect(result.include).toEqual(["reasoning.encrypted_content"])
})
test("gpt-5.1 should have textVerbosity set to low", () => {
@@ -336,25 +336,10 @@ const weatherTool = tool({
})
const toolRoundtrip = (
events: ReadonlyArray<LLMEvent>,
call: { readonly id: string; readonly name: string; readonly input: unknown },
result: JSONValue,
): ModelMessage[] => [
{
role: "assistant",
content: [
...events.filter(LLMEvent.is.reasoningEnd).map((part) => ({
type: "reasoning" as const,
text: events
.filter(LLMEvent.is.reasoningDelta)
.filter((event) => event.id === part.id)
.map((event) => event.text)
.join(""),
providerMetadata: part.providerMetadata,
})),
{ type: "tool-call", toolCallId: call.id, toolName: call.name, input: call.input },
],
},
{ role: "assistant", content: [{ type: "tool-call", toolCallId: call.id, toolName: call.name, input: call.input }] },
{
role: "tool",
content: [
@@ -410,7 +395,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
const turn2 = yield* collect({
...base,
messages: [userMessage, ...toolRoundtrip(turn1, toolCall!, WEATHER_RESULT)],
messages: [userMessage, ...toolRoundtrip(toolCall!, WEATHER_RESULT)],
})
expect(LLMResponse.text({ events: turn2 })).toMatch(/Paris is sunny/i)
@@ -6,6 +6,7 @@ import { Effect, Layer, Stream } from "effect"
import { LLMNative } from "@/session/llm/native-request"
import { LLMNativeRuntime } from "@/session/llm/native-runtime"
import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { ModelID, ProviderID } from "@/provider/schema"
import { OAUTH_DUMMY_KEY } from "@/auth"
import { testEffect } from "../lib/effect"
@@ -70,6 +71,21 @@ const providerInfo: Provider.Info = {
models: {},
}
const compatibleModel: Provider.Model = {
...baseModel,
id: ModelID.make("deepseek-v4-flash-free"),
providerID: ProviderID.make("opencode"),
api: {
id: "deepseek-v4-flash-free",
url: "https://ai.example.test/v1",
npm: "@ai-sdk/openai-compatible",
},
capabilities: {
...baseModel.capabilities,
interleaved: { field: "reasoning_content" },
},
}
const it = testEffect(
LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(RequestExecutor.defaultLayer, WebSocketExecutor.layer))),
)
@@ -326,6 +342,70 @@ describe("session.llm-native.request", () => {
])
})
it.effect("preserves OpenAI-compatible reasoning continuation and provider options", () =>
Effect.gen(function* () {
const messages = ProviderTransform.message(
[
{ role: "user", content: "Audit the site" },
{
role: "assistant",
content: [
{ type: "reasoning", text: "I should inspect the page." },
{
type: "tool-call",
toolCallId: "call-1",
toolName: "devtools_new_page",
input: { url: "https://example.test" },
},
],
},
] as ModelMessage[],
compatibleModel,
{},
)
const prepared = yield* LLMClient.prepare(
LLMNative.request({
model: compatibleModel,
apiKey: "test-key",
messages,
providerOptions: ProviderTransform.providerOptions(compatibleModel, {
reasoningEffort: "max",
textVerbosity: "low",
promptCacheKey: "session-1",
enable_thinking: true,
}),
}),
)
expect(prepared.body).toMatchObject({
model: "deepseek-v4-flash-free",
reasoning_effort: "max",
verbosity: "low",
prompt_cache_key: "session-1",
enable_thinking: true,
messages: [
{ role: "user", content: "Audit the site" },
{
role: "assistant",
content: null,
reasoning_content: "I should inspect the page.",
tool_calls: [
{
id: "call-1",
type: "function",
function: {
name: "devtools_new_page",
arguments: '{"url":"https://example.test"}',
},
},
],
},
],
})
}),
)
test("selects native request routes for provider packages", () => {
const openai = LLMNative.model({
model: { ...baseModel, api: { ...baseModel.api, url: "", npm: "@ai-sdk/openai" } },
@@ -591,7 +671,7 @@ describe("session.llm-native.request", () => {
]),
storedSession.user("Summarize it."),
],
providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } },
providerOptions: { openai: { store: false, includeEncryptedReasoning: true } },
expectedBody: {
input: [
openAIResponses.user("What changed?"),
@@ -608,45 +688,6 @@ describe("session.llm-native.request", () => {
}),
)
it.effect("preserves empty encrypted OpenAI reasoning items before tool output", () =>
expectOpenAIResponsesRequest({
history: [
storedSession.assistant([
storedSession.openaiReasoning("", {
storedAs: "providerMetadata",
itemId: "rs_1",
encryptedContent: "encrypted-state",
}),
]),
],
providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } },
expectedBody: {
input: [{ type: "reasoning", id: "rs_1", summary: [], encrypted_content: "encrypted-state" }],
include: ["reasoning.encrypted_content"],
store: false,
},
}),
)
it.effect("references stored OpenAI reasoning items by id", () =>
expectOpenAIResponsesRequest({
history: [
storedSession.assistant([
storedSession.openaiReasoning("Checked the previous diff.", {
storedAs: "providerMetadata",
itemId: "rs_1",
encryptedContent: null,
}),
]),
],
providerOptions: { openai: { store: true } },
expectedBody: {
input: [{ type: "item_reference", id: "rs_1" }],
store: true,
},
}),
)
it.effect("uses provider fetch override for native OpenAI OAuth requests", () =>
Effect.gen(function* () {
const captures: Array<{ url: string; body: unknown }> = []
@@ -1166,7 +1166,6 @@ describe("session.llm.stream", () => {
expect(capture.body.model).toBe(model.id)
expect(capture.body.stream).toBe(true)
expect((capture.body.reasoning as { effort?: string } | undefined)?.effort).toBe("high")
expect(capture.body.include).toEqual(["reasoning.encrypted_content"])
expect(JSON.stringify(capture.body.input)).toContain("You are a helpful assistant.")
expect(capture.body.input).toContainEqual({ role: "user", content: [{ type: "input_text", text: "Hello" }] })
}),
+14 -20
View File
@@ -594,23 +594,17 @@ opencode upgrade v0.1.48
تقوم متغيرات البيئة التالية بتفعيل ميزات تجريبية قد تتغير أو تتم إزالتها.
| المتغير | النوع | الوصف |
| ----------------------------------------------- | ------- | -------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل الميزات التجريبية الخاضعة للعلم الشامل |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | تفعيل مهام subagent في الخلفية |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | تفعيل نظام الأحداث التجريبي |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | تفعيل مسار طلبات LLM الأصلي |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | تفعيل تنفيذ بحث الويب بالتوازي |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | تفعيل Scout subagent |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | تفعيل دعم مساحات العمل |
| المتغير | النوع | الوصف |
| ----------------------------------------------- | ------- | ------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل جميع الميزات التجريبية |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة |
+14 -20
View File
@@ -592,23 +592,17 @@ OpenCode se može konfigurirati pomoću varijabli okruženja.
Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti.
| Varijabla | Tip | Opis |
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući eksperimentalne funkcije pod zbirnom zastavicom |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Omogući pozadinske zadatke subagenata |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Omogući eksperimentalni sistem događaja |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Omogući nativnu putanju LLM zahtjeva |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Omogući paralelno izvršavanje web pretrage |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Omogući Scout subagenta |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Omogući podršku za radne prostore |
| Varijabla | Tip | Opis |
| ----------------------------------------------- | ------- | ------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Onemogući kopiranje pri odabiru u TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Zadano vremensko ograničenje za bash naredbe u ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod |
+1 -7
View File
@@ -712,7 +712,7 @@ These environment variables enable experimental features that may change or be r
| Variable | Type | Description |
| ----------------------------------------------- | ------- | --------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Enable the experimental umbrella flag |
| `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms |
@@ -724,9 +724,3 @@ These environment variables enable experimental features that may change or be r
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable TY LSP for python files |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Enable background subagent tasks |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Enable experimental event system |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Enable native LLM request path |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Enable parallel web search execution |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Enable Scout subagent |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Enable workspace support |
+14 -20
View File
@@ -595,23 +595,17 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler.
Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes.
| Variabel | Skriv | Beskrivelse |
| ----------------------------------------------- | ------- | ---------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver eksperimentelle funktioner under samleflaget |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Aktiver baggrundsopgaver for subagenter |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Aktiver eksperimentelt hændelsessystem |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Aktiver native LLM-anmodningssti |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Aktiver parallel udførelse af websøgning |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Aktiver Scout-subagent |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Aktiver workspace-understøttelse |
| Variabel | Skriv | Beskrivelse |
| ----------------------------------------------- | ------- | ----------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver alle eksperimentelle funktioner |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | nummer | Standard timeout for bash-kommandoer i ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand |
+14 -20
View File
@@ -594,23 +594,17 @@ OpenCode kann mithilfe von Umgebungsvariablen konfiguriert werden.
Diese Umgebungsvariablen ermöglichen experimentelle Funktionen, die sich ändern oder entfernt werden können.
| Variable | Typ | Beschreibung |
| ----------------------------------------------- | --------------- | --------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Experimentelle Funktionen unter dem Sammelflag aktivieren |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren |
| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolescher Wert | Hintergrundaufgaben für Subagenten aktivieren |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolescher Wert | Experimentelles Ereignissystem aktivieren |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolescher Wert | Nativen LLM-Anfragepfad aktivieren |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolescher Wert | Parallele Websuche aktivieren |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolescher Wert | Scout-Subagent aktivieren |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolescher Wert | Workspace-Unterstützung aktivieren |
| Variable | Typ | Beschreibung |
| ----------------------------------------------- | --------------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Alle experimentellen Funktionen aktivieren |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung aktivieren |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolescher Wert | Kopieren bei Auswahl in TUI deaktivieren |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Zahl | Standard-Timeout für Bash-Befehle in ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | Zahl | Maximale Ausgabetokens für LLM-Antworten |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren |
| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus aktivieren |
+1 -7
View File
@@ -596,7 +596,7 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
| Variable | Type | Description |
| ----------------------------------------------- | -------- | ---------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar funciones experimentales de la bandera global |
| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar todas las funciones experimentales |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booleano | Habilitar descubrimiento de íconos |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booleano | Deshabilitar copia al seleccionar en TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | número | Tiempo de espera predeterminado para comandos bash en ms |
@@ -608,9 +608,3 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
| `OPENCODE_EXPERIMENTAL_EXA` | booleano | Habilitar funciones experimentales de Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | booleano | Habilitar Habilitar TY LSP para archivos python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booleano | Habilitar modo de plan |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booleano | Habilitar tareas de subagentes en segundo plano |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booleano | Habilitar sistema de eventos experimental |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booleano | Habilitar ruta nativa de solicitud LLM |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | booleano | Habilitar ejecución paralela de búsqueda web |
| `OPENCODE_EXPERIMENTAL_SCOUT` | booleano | Habilitar subagente Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booleano | Habilitar soporte de espacios de trabajo |
+1 -7
View File
@@ -597,7 +597,7 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui
| Variables | Type | Description |
| ----------------------------------------------- | ------- | --------------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | booléen | Activer les fonctionnalités expérimentales du drapeau global |
| `OPENCODE_EXPERIMENTAL` | booléen | Activer toutes les fonctionnalités expérimentales |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booléen | Activer la découverte d'icônes |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booléen | Désactiver la copie lors de la sélection dans TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | numéro | Délai d'expiration par défaut pour les commandes bash en ms |
@@ -609,9 +609,3 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui
| `OPENCODE_EXPERIMENTAL_EXA` | booléen | Activer les fonctionnalités Exa expérimentales |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | booléen | Activer TY LSP pour les fichiers python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booléen | Activer le mode plan |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | booléen | Activer les tâches de sous-agents en arrière-plan |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | booléen | Activer le système d'événements expérimental |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | booléen | Activer le chemin de requête LLM natif |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | booléen | Activer l'exécution parallèle de la recherche web |
| `OPENCODE_EXPERIMENTAL_SCOUT` | booléen | Activer le sous-agent Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | booléen | Activer la prise en charge des espaces de travail |
+14 -20
View File
@@ -595,23 +595,17 @@ OpenCode può essere configurato tramite variabili d'ambiente.
Queste variabili d'ambiente abilitano funzionalità sperimentali che potrebbero cambiare o essere rimosse.
| Variabile | Tipo | Descrizione |
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Abilita funzionalità sperimentali sotto il flag globale |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Abilita task subagent in background |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Abilita sistema eventi sperimentale |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Abilita percorso nativo di richiesta LLM |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Abilita esecuzione parallela della ricerca web |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Abilita subagent Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Abilita supporto workspace |
| Variabile | Tipo | Descrizione |
| ----------------------------------------------- | ------- | ------------------------------------------ |
| `OPENCODE_EXPERIMENTAL` | boolean | Abilita tutte le funzionalità sperimentali |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Timeout di default per comandi bash in ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode |
+1 -7
View File
@@ -596,7 +596,7 @@ OpenCode は環境変数を使用して構成できます。
| 変数 | タイプ | 説明 |
| ----------------------------------------------- | -------- | ------------------------------------------------ |
| `OPENCODE_EXPERIMENTAL` | ブール値 | アンブレラフラグ配下の実験的機能を有効にする |
| `OPENCODE_EXPERIMENTAL` | ブール値 | すべての実験的機能を有効にする |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | ブール値 | アイコン検出を有効にする |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | ブール値 | TUI で選択時のコピーを無効にする |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | 数値 | bash コマンドのデフォルトのタイムアウト (ミリ秒) |
@@ -608,9 +608,3 @@ OpenCode は環境変数を使用して構成できます。
| `OPENCODE_EXPERIMENTAL_EXA` | ブール値 | 実験的な Exa 機能を有効にする |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | ブール値 | python ファイルの TY LSP を有効にする |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | ブール値 | プランモードを有効にする |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | ブール値 | バックグラウンド subagent タスクを有効にする |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | ブール値 | 実験的なイベントシステムを有効にする |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | ブール値 | ネイティブ LLM リクエスト経路を有効にする |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | ブール値 | 並列 Web 検索実行を有効にする |
| `OPENCODE_EXPERIMENTAL_SCOUT` | ブール値 | Scout subagent を有効にする |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | ブール値 | ワークスペースサポートを有効にする |
+14 -20
View File
@@ -594,23 +594,17 @@ OpenCode는 환경 변수로도 구성할 수 있습니다.
아래 환경 변수는 변경되거나 제거될 수 있는 실험 기능을 활성화합니다.
| 변수 | 타입 | 설명 |
| ----------------------------------------------- | ------- | ------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 엄브렐라 플래그로 제어되는 실험 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 백그라운드 subagent 작업 활성화 |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 실험적 이벤트 시스템 활성화 |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 네이티브 LLM 요청 경로 활성화 |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 병렬 웹 검색 실행 활성화 |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout subagent 활성화 |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | workspace 지원 활성화 |
| 변수 | 타입 | 설명 |
| ----------------------------------------------- | ------- | -------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 모든 실험 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 |
+14 -20
View File
@@ -595,23 +595,17 @@ OpenCode kan konfigureres ved hjelp av miljøvariabler.
Disse miljøvariablene muliggjør eksperimentelle funksjoner som kan endres eller fjernes.
| Variabel | Type | Beskrivelse |
| ----------------------------------------------- | ------ | ----------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver eksperimentelle funksjoner under samleflagget |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking |
| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolsk | Aktiver bakgrunnsoppgaver for subagenter |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolsk | Aktiver eksperimentelt hendelsessystem |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolsk | Aktiver innebygd LLM-forespørselsvei |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolsk | Aktiver parallell kjøring av websøk |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolsk | Aktiver Scout-subagent |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolsk | Aktiver arbeidsområde-støtte |
| Variabel | Type | Beskrivelse |
| ----------------------------------------------- | ------ | --------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver alle eksperimentelle funksjoner |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | tall | Standard tidsavbrudd for bash-kommandoer i ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking |
| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus |
+14 -20
View File
@@ -595,23 +595,17 @@ OpenCode można skonfigurować za pomocą zmiennych środowiskowych.
Te zmienne włączają funkcje eksperymentalne, które mogą ulec zmianie lub zostać usunięte.
| Zmienna | Typ | Opis |
| ----------------------------------------------- | ------- | --------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Włącz funkcje eksperymentalne objęte flagą zbiorczą |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Włącz zadania subagentów w tle |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Włącz eksperymentalny system zdarzeń |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Włącz natywną ścieżkę żądań LLM |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Włącz równoległe wykonywanie wyszukiwania web |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Włącz subagenta Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Włącz obsługę obszarów roboczych |
| Zmienna | Typ | Opis |
| ----------------------------------------------- | ------- | -------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Włącz wszystkie funkcje eksperymentalne |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Wyłącz kopiowanie przy zaznaczaniu w TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Domyślny limit czasu dla narzędzia bash w ms |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | Maksymalne tokeny wyjściowe dla LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania |
+1 -7
View File
@@ -596,7 +596,7 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
| Variável | Tipo | Descrição |
| ----------------------------------------------- | ------- | --------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar recursos experimentais sob o sinalizador global |
| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar todos os recursos experimentais |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Habilitar descoberta de ícones |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Desabilitar cópia ao selecionar no TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Tempo limite padrão para comandos bash em ms |
@@ -608,9 +608,3 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Habilitar recursos experimentais do Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Habilitar TY LSP para arquivos python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Habilitar modo de plano |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Habilitar tarefas de subagentes em segundo plano |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Habilitar sistema de eventos experimental |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Habilitar caminho nativo de requisição LLM |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Habilitar execução paralela de busca web |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Habilitar subagente Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Habilitar suporte a espaços de trabalho |
+1 -7
View File
@@ -597,7 +597,7 @@ opencode можно настроить с помощью переменных с
| Переменная | Тип | Описание |
| ----------------------------------------------- | ------------------- | ------------------------------------------------------ |
| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить экспериментальные функции под общим флагом |
| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить все экспериментальные функции |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | логическое значение | Включить обнаружение значков |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | логическое значение | Отключить копирование при выборе в TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | число | Таймаут по умолчанию для команд bash в мс |
@@ -609,9 +609,3 @@ opencode можно настроить с помощью переменных с
| `OPENCODE_EXPERIMENTAL_EXA` | логическое значение | Включить экспериментальные функции Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | логическое значение | Включить TY LSP для файлов python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | логическое значение | Включить режим плана |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | логическое значение | Включить фоновые задачи субагентов |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | логическое значение | Включить экспериментальную систему событий |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | логическое значение | Включить нативный путь запросов LLM |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | логическое значение | Включить параллельное выполнение веб-поиска |
| `OPENCODE_EXPERIMENTAL_SCOUT` | логическое значение | Включить субагент Scout |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | логическое значение | Включить поддержку рабочих областей |
+1 -7
View File
@@ -598,7 +598,7 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
| ตัวแปร | ชนิด | คำอธิบาย |
| ----------------------------------------------- | ------- | ---------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองภายใต้แฟล็กรวม |
| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองทั้งหมด |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | Boolean | การค้นหาไอคอนทดลอง |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | Boolean | ปิดใช้งานการคัดลอกเมื่อเลือกใน TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Number | การหมดเวลาเริ่มต้นสำหรับคำสั่ง bash ในหน่วย ms |
@@ -610,9 +610,3 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
| `OPENCODE_EXPERIMENTAL_EXA` | Boolean | ฟีเจอร์ Exa ทดลอง |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | Boolean | เปิดใช้งาน TY LSP สำหรับไฟล์ python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | Boolean | เปิดใช้งาน Plan mode |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Boolean | เปิดใช้งานงาน subagent เบื้องหลัง |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | Boolean | เปิดใช้งานระบบเหตุการณ์ทดลอง |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | Boolean | เปิดใช้งานเส้นทางคำขอ LLM แบบ native |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | Boolean | เปิดใช้งานการค้นหาเว็บแบบขนาน |
| `OPENCODE_EXPERIMENTAL_SCOUT` | Boolean | เปิดใช้งาน Scout subagent |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | Boolean | เปิดใช้งานการรองรับ workspace |
+1 -7
View File
@@ -597,7 +597,7 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli
| Değişken | Tip | Tanım |
| ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Şemsiye bayrağa bağlı deneysel özellikleri etkinleştir |
| `OPENCODE_EXPERIMENTAL` | boolean | Tüm deneysel özellikleri etkinleştir |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Simge bulmayı etkinleştir |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI'da seçim yapıldığında kopyalamayı devre dışı bırak |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | MS cinsinden bash komutları için varsayılan zaman aşımı |
@@ -609,9 +609,3 @@ Bu ortam değişkenleri değişebilecek veya kaldırılabilecek deneysel özelli
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Deneysel Exa özelliklerini etkinleştirin |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python dosyaları için TY LSP'yi etkinleştir |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan modunu etkinleştir |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | Arka plan alt ajan görevlerini etkinleştir |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | Deneysel olay sistemini etkinleştir |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | Yerel LLM istek yolunu etkinleştir |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | Paralel web araması yürütmesini etkinleştir |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | Scout alt ajanını etkinleştir |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | Çalışma alanı desteğini etkinleştir |
+1 -7
View File
@@ -597,7 +597,7 @@ OpenCode 可以通过环境变量进行配置。
| 变量 | 类型 | 描述 |
| ----------------------------------------------- | ------- | ------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 启用受总开关控制的实验性功能 |
| `OPENCODE_EXPERIMENTAL` | boolean | 启用所有实验性功能 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 启用图标发现 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 禁用 TUI 中的选中即复制 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 命令的默认超时时间(毫秒) |
@@ -609,9 +609,3 @@ OpenCode 可以通过环境变量进行配置。
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 启用实验性 Exa 功能 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 为 python 文件启用 TY LSP |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 启用计划模式 |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 启用后台子代理任务 |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 启用实验性事件系统 |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 启用原生 LLM 请求路径 |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 启用并行 Web 搜索执行 |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 启用 Scout 子代理 |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 启用工作区支持 |
+1 -7
View File
@@ -597,7 +597,7 @@ OpenCode 可以透過環境變數進行設定。
| 變數 | 類型 | 說明 |
| ----------------------------------------------- | ------- | ------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 啟用受總開關控制的實驗性功能 |
| `OPENCODE_EXPERIMENTAL` | boolean | 啟用所有實驗性功能 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 啟用圖示探索 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 停用 TUI 中的選取即複製 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 指令的預設逾時時間(毫秒) |
@@ -609,9 +609,3 @@ OpenCode 可以透過環境變數進行設定。
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 啟用實驗性 Exa 功能 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 為 python 檔案啟用 TY LSP |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 啟用計畫模式 |
| `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | boolean | 啟用背景子代理任務 |
| `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM` | boolean | 啟用實驗性事件系統 |
| `OPENCODE_EXPERIMENTAL_NATIVE_LLM` | boolean | 啟用原生 LLM 請求路徑 |
| `OPENCODE_EXPERIMENTAL_PARALLEL` | boolean | 啟用平行 Web 搜尋執行 |
| `OPENCODE_EXPERIMENTAL_SCOUT` | boolean | 啟用 Scout 子代理 |
| `OPENCODE_EXPERIMENTAL_WORKSPACES` | boolean | 啟用工作區支援 |