Compare commits

..
81 changed files with 1549 additions and 1641 deletions
+38 -14
View File
@@ -199,15 +199,15 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -409,15 +409,15 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0", "@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@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/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.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/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/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=="], "@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/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.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": ["@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-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/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/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=="], "@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,9 +5090,13 @@
"@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/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.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/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/@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/@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/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=="], "@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=="],
@@ -5108,11 +5112,17 @@
"@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/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-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": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@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/@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/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/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=="], "@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=="],
@@ -6006,6 +6016,12 @@
"@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=="], "@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/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=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6016,6 +6032,14 @@
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@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/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=="], "@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": { "nodeModules": {
"x86_64-linux": "sha256-qq8cKNcW10NRbPx2T/Gpzby4nx2qHzj0LWDwqyPl7rY=", "x86_64-linux": "sha256-szOnLhAI4d3O4NaQvCRBufklmtRU8AR8p5LyFxjlTXo=",
"aarch64-linux": "sha256-UguK4nObAL/ovGZQousveoXCmkcxEgWy7XSSzxVJNEY=", "aarch64-linux": "sha256-mfyL4Pswl6HeObPg+TMK1r7RjvLCMgDWYrFnfHkXGj4=",
"aarch64-darwin": "sha256-NibSqZiF0XMbFhOtl2CQvFe4N/BXB5UAdt2iKv7dIcI=", "aarch64-darwin": "sha256-XMK5IFCIxlSwH21Nqya8ZkAQ4Skp6GghukIl7NMp+so=",
"x86_64-darwin": "sha256-ZNeZSpYitdUjXx/sULAIoLXrM6+Vz+GXQxKpdfgXh60=" "x86_64-darwin": "sha256-W7nLMO6qhR32442yMebEJJgaZRNpqTbZ8BJhdqMK3S4="
} }
} }
+3 -3
View File
@@ -27,15 +27,15 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -15,6 +15,7 @@ type ServiceUse<Identifier, Shape> = {
} }
export const serviceUse = <Identifier, Shape>(tag: Context.Service<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, // This is the only dynamic boundary: TypeScript knows the accessor shape,
// but Proxy property names are runtime values. // but Proxy property names are runtime values.
const access = new Proxy( const access = new Proxy(
@@ -22,7 +23,9 @@ export const serviceUse = <Identifier, Shape>(tag: Context.Service<Identifier, S
{ {
get: (_, key) => { get: (_, key) => {
if (typeof key !== "string") return undefined if (typeof key !== "string") return undefined
return (...args: unknown[]) => const cached = cache.get(key)
if (cached) return cached
const accessor = (...args: unknown[]) =>
tag.use((service) => { tag.use((service) => {
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime. // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime.
const method = service[key as keyof Shape] const method = service[key as keyof Shape]
@@ -30,6 +33,8 @@ 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. // 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) return (method as (...args: unknown[]) => Effect.Effect<unknown, unknown, unknown>)(...args)
}) })
cache.set(key, accessor)
return accessor
}, },
}, },
) )
+4 -1
View File
@@ -3,9 +3,10 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
import { realpathSync } from "fs" import { realpathSync } from "fs"
import * as NFS from "fs/promises" import * as NFS from "fs/promises"
import { lookup } from "mime-types" import { lookup } from "mime-types"
import { Effect, FileSystem, Layer, Schema, Context } from "effect" import { Context, Effect, FileSystem, Layer, Schema } from "effect"
import type { PlatformError } from "effect/PlatformError" import type { PlatformError } from "effect/PlatformError"
import { Glob } from "./util/glob" import { Glob } from "./util/glob"
import { serviceUse } from "./effect/service-use"
export namespace AppFileSystem { export namespace AppFileSystem {
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", { export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
@@ -39,6 +40,8 @@ export namespace AppFileSystem {
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {} export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
export const use = serviceUse(Service)
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { 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 } if (!repo) return { id: ID.global, directory: input, vcs: undefined }
const previous = yield* cached(repo.store) const previous = yield* cached(repo.store)
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo)) const id = previous ?? (yield* root(repo))
return { return {
previous, previous,
+6 -34
View File
@@ -5,16 +5,11 @@ import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Project } from "@opencode-ai/core/project" import { Project } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
import { Hash } from "@opencode-ai/core/util/hash"
import { tmpdir } from "./fixture/tmpdir" import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect" import { testEffect } from "./lib/effect"
const it = testEffect(Project.defaultLayer) const it = testEffect(Project.defaultLayer)
function remoteID(remote: string) {
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
}
function abs(value: string) { function abs(value: string) {
return AbsolutePath.make(value) return AbsolutePath.make(value)
} }
@@ -91,7 +86,7 @@ describe("ProjectV2.resolve", () => {
}), }),
) )
it.live("prefers normalized origin over root commit", () => it.live("uses root commit when origin exists", () =>
Effect.gen(function* () { Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease( const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -102,36 +97,13 @@ describe("ProjectV2.resolve", () => {
const result = yield* project.resolve(abs(tmp.path)) const result = yield* project.resolve(abs(tmp.path))
expect(result.id).toBe(remoteID("github.com/Acme/App")) expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
expect(result.directory).toBe(yield* real(tmp.path)) expect(result.directory).toBe(yield* real(tmp.path))
expect(result.vcs?.type).toBe("git") expect(result.vcs?.type).toBe("git")
}), }),
) )
it.live("normalizes ssh and https remotes to the same id", () => it.live("uses root commit when local remote exists", () =>
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* () { Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease( const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -146,7 +118,7 @@ describe("ProjectV2.resolve", () => {
}), }),
) )
it.live("returns previous cached id from common dir", () => it.live("prefers previous cached id over origin", () =>
Effect.gen(function* () { Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease( const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()), Effect.promise(() => tmpdir()),
@@ -159,7 +131,7 @@ describe("ProjectV2.resolve", () => {
const result = yield* project.resolve(abs(tmp.path)) const result = yield* project.resolve(abs(tmp.path))
expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo")) expect(result.id).toBe(Project.ID.make("old-id"))
}), }),
) )
@@ -213,7 +185,7 @@ describe("ProjectV2.resolve", () => {
expect(result.directory).toBe(yield* real(worktree)) expect(result.directory).toBe(yield* real(worktree))
expect(result.previous).toBe(Project.ID.make("old-id")) expect(result.previous).toBe(Project.ID.make("old-id"))
expect(result.id).toBe(remoteID("github.com/owner/repo")) expect(result.id).toBe(Project.ID.make("old-id"))
expect(result.vcs?.type).toBe("git") expect(result.vcs?.type).toBe("git")
}), }),
) )
@@ -20,7 +20,6 @@ import {
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import * as Cache from "./utils/cache" import * as Cache from "./utils/cache"
import { Lifecycle } from "./utils/lifecycle" import { Lifecycle } from "./utils/lifecycle"
import { ProviderOptions } from "./utils/provider-options"
import { ToolStream } from "./utils/tool-stream" import { ToolStream } from "./utils/tool-stream"
const ADAPTER = "anthropic-messages" const ADAPTER = "anthropic-messages"
@@ -137,7 +136,6 @@ const AnthropicTool = Schema.Struct({
description: Schema.String, description: Schema.String,
input_schema: JsonObject, input_schema: JsonObject,
cache_control: Schema.optional(AnthropicCacheControl), cache_control: Schema.optional(AnthropicCacheControl),
eager_input_streaming: Schema.optional(Schema.Boolean),
}) })
type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool> type AnthropicTool = Schema.Schema.Type<typeof AnthropicTool>
@@ -146,10 +144,10 @@ const AnthropicToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }), Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
]) ])
// Anthropic accepts several `thinking` shapes (enabled with `budget_tokens`, const AnthropicThinking = Schema.Struct({
// adaptive with optional `display`, and disabled). The body schema permits the type: Schema.tag("enabled"),
// full union so explicit lowering can pick the correct fields per model. budget_tokens: Schema.Number,
const AnthropicThinkingBody = Schema.Record(Schema.String, Schema.Unknown) })
const AnthropicBodyFields = { const AnthropicBodyFields = {
model: Schema.String, model: Schema.String,
@@ -163,12 +161,9 @@ const AnthropicBodyFields = {
top_p: Schema.optional(Schema.Number), top_p: Schema.optional(Schema.Number),
top_k: Schema.optional(Schema.Number), top_k: Schema.optional(Schema.Number),
stop_sequences: optionalArray(Schema.String), stop_sequences: optionalArray(Schema.String),
thinking: Schema.optional(AnthropicThinkingBody), thinking: Schema.optional(AnthropicThinking),
} }
// Unknown provider options pass through verbatim with top-level keys snake-cased. const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
const AnthropicMessagesBody = Schema.StructWithRest(Schema.Struct(AnthropicBodyFields), [
Schema.Record(Schema.String, Schema.Any),
])
export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody> export type AnthropicMessagesBody = Schema.Schema.Type<typeof AnthropicMessagesBody>
const AnthropicUsage = Schema.Struct({ const AnthropicUsage = Schema.Struct({
@@ -259,16 +254,11 @@ const signatureFromMetadata = (metadata: ProviderMetadata | undefined): string |
return typeof anthropic.signature === "string" ? anthropic.signature : undefined return typeof anthropic.signature === "string" ? anthropic.signature : undefined
} }
const lowerTool = ( const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition): AnthropicTool => ({
breakpoints: Cache.Breakpoints,
tool: ToolDefinition,
eagerInputStreaming: boolean | undefined,
): AnthropicTool => ({
name: tool.name, name: tool.name,
description: tool.description, description: tool.description,
input_schema: tool.inputSchema, input_schema: tool.inputSchema,
cache_control: cacheControl(breakpoints, tool.cache), cache_control: cacheControl(breakpoints, tool.cache),
eager_input_streaming: eagerInputStreaming ? true : undefined,
}) })
const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) => const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
@@ -423,46 +413,24 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
return messages return messages
}) })
// Typed AI SDK Anthropic options. Mirrors the subset of const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
// `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" }
interface AnthropicOptions { const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
readonly thinking?: AnthropicThinking const thinking = anthropicOptions(request)?.thinking
readonly effort?: AnthropicEffort if (!ProviderShared.isRecord(thinking) || thinking.type !== "enabled") return undefined
readonly toolStreaming?: boolean const budget =
readonly [extra: string]: unknown typeof thinking.budgetTokens === "number"
} ? thinking.budgetTokens
: typeof thinking.budget_tokens === "number"
const ANTHROPIC_KNOWN_KEYS: ReadonlySet<string> = new Set(["thinking", "effort", "toolStreaming"]) ? thinking.budget_tokens
: undefined
const lowerThinking = (thinking: AnthropicOptions["thinking"]) => { if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
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 } return { type: "enabled" as const, budget_tokens: budget }
} })
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) { const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined const toolChoice = request.toolChoice ? yield* lowerToolChoice(request.toolChoice) : undefined
const generation = request.generation 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 → // Allocate the 4-breakpoint budget in invalidation order: tools → system →
// messages. Tools live highest in the cache hierarchy, so when callers // 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. // over-mark we keep their tool hints and shed the message-tail ones first.
@@ -470,7 +438,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
const tools = const tools =
request.tools.length === 0 || request.toolChoice?.type === "none" request.tools.length === 0 || request.toolChoice?.type === "none"
? undefined ? undefined
: request.tools.map((tool) => lowerTool(breakpoints, tool, eagerInputStreaming)) : request.tools.map((tool) => lowerTool(breakpoints, tool))
const system = const system =
request.system.length === 0 request.system.length === 0
? undefined ? undefined
@@ -486,7 +454,6 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
) )
} }
return { return {
...ProviderOptions.passthrough(options, ANTHROPIC_KNOWN_KEYS),
model: request.model.id, model: request.model.id,
system, system,
messages, messages,
@@ -498,8 +465,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
top_p: generation?.topP, top_p: generation?.topP,
top_k: generation?.topK, top_k: generation?.topK,
stop_sequences: generation?.stop, stop_sequences: generation?.stop,
thinking: lowerThinking(options.thinking), thinking: yield* lowerThinking(request),
...(options.effort !== undefined ? { effort: options.effort } : {}),
} }
}) })
+10 -42
View File
@@ -15,7 +15,6 @@ import {
} from "../schema" } from "../schema"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options" import { OpenAIOptions } from "./utils/openai-options"
import { ProviderOptions } from "./utils/provider-options"
import { Lifecycle } from "./utils/lifecycle" import { Lifecycle } from "./utils/lifecycle"
import { ToolStream } from "./utils/tool-stream" import { ToolStream } from "./utils/tool-stream"
@@ -51,10 +50,6 @@ const OpenAIChatAssistantToolCall = Schema.Struct({
}) })
type OpenAIChatAssistantToolCall = Schema.Schema.Type<typeof OpenAIChatAssistantToolCall> 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([ const OpenAIChatMessage = Schema.Union([
Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }),
Schema.Struct({ role: Schema.Literal("user"), content: Schema.String }), Schema.Struct({ role: Schema.Literal("user"), content: Schema.String }),
@@ -63,7 +58,6 @@ const OpenAIChatMessage = Schema.Union([
content: Schema.NullOr(Schema.String), content: Schema.NullOr(Schema.String),
tool_calls: optionalArray(OpenAIChatAssistantToolCall), tool_calls: optionalArray(OpenAIChatAssistantToolCall),
reasoning_content: Schema.optional(Schema.String), 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 }), Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }),
]).pipe(Schema.toTaggedUnion("role")) ]).pipe(Schema.toTaggedUnion("role"))
@@ -85,7 +79,7 @@ export const bodyFields = {
stream: Schema.Literal(true), stream: Schema.Literal(true),
stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })), stream_options: Schema.optional(Schema.Struct({ include_usage: Schema.Boolean })),
store: Schema.optional(Schema.Boolean), store: Schema.optional(Schema.Boolean),
reasoning_effort: Schema.optional(Schema.String), reasoning_effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
max_tokens: Schema.optional(Schema.Number), max_tokens: Schema.optional(Schema.Number),
temperature: Schema.optional(Schema.Number), temperature: Schema.optional(Schema.Number),
top_p: Schema.optional(Schema.Number), top_p: Schema.optional(Schema.Number),
@@ -94,7 +88,7 @@ export const bodyFields = {
seed: Schema.optional(Schema.Number), seed: Schema.optional(Schema.Number),
stop: optionalArray(Schema.String), stop: optionalArray(Schema.String),
} }
const OpenAIChatBody = Schema.StructWithRest(Schema.Struct(bodyFields), [Schema.Record(Schema.String, Schema.Any)]) const OpenAIChatBody = Schema.Struct(bodyFields)
export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody> export type OpenAIChatBody = Schema.Schema.Type<typeof OpenAIChatBody>
// ============================================================================= // =============================================================================
@@ -131,16 +125,9 @@ const OpenAIChatToolCallDelta = Schema.Struct({
}) })
type OpenAIChatToolCallDelta = Schema.Schema.Type<typeof OpenAIChatToolCallDelta> 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({ const OpenAIChatDelta = Schema.Struct({
content: optionalNull(Schema.String), content: optionalNull(Schema.String),
reasoning_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)), tool_calls: optionalNull(Schema.Array(OpenAIChatToolCallDelta)),
}) })
@@ -201,16 +188,6 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
const openAICompatibleReasoningContent = (native: unknown) => const openAICompatibleReasoningContent = (native: unknown) =>
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined 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 lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) {
const content: TextPart[] = [] const content: TextPart[] = []
for (const part of message.content) { for (const part of message.content) {
@@ -243,7 +220,6 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
content: content.length === 0 ? null : ProviderShared.joinText(content), content: content.length === 0 ? null : ProviderShared.joinText(content),
tool_calls: toolCalls.length === 0 ? undefined : toolCalls, tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
reasoning_content: openAICompatibleReasoningContent(message.native?.openaiCompatible), reasoning_content: openAICompatibleReasoningContent(message.native?.openaiCompatible),
reasoning_details: openAICompatibleReasoningDetails(message.native?.openaiCompatible),
} }
}) })
@@ -270,14 +246,13 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
}) })
const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) { const lowerOptions = Effect.fn("OpenAIChat.lowerOptions")(function* (request: LLMRequest) {
const options = OpenAIOptions.options(request) const store = OpenAIOptions.store(request)
const effort = options.reasoningEffort const reasoningEffort = OpenAIOptions.reasoningEffort(request)
if (effort !== undefined && !OpenAIOptions.isReasoningEffort(effort)) if (reasoningEffort && !OpenAIOptions.isReasoningEffort(reasoningEffort))
return yield* invalid(`OpenAI Chat does not support reasoning effort ${effort}`) return yield* invalid(`OpenAI Chat does not support reasoning effort ${reasoningEffort}`)
return { return {
...ProviderOptions.passthrough(options, OpenAIOptions.KNOWN_KEYS), ...(store !== undefined ? { store } : {}),
...(options.store !== undefined ? { store: options.store } : {}), ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
...(effort !== undefined ? { reasoning_effort: effort } : {}),
} }
}) })
@@ -350,15 +325,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
let lifecycle = state.lifecycle let lifecycle = state.lifecycle
// OpenRouter-style `reasoning_details` ships an array of typed reasoning if (delta?.reasoning_content)
// objects (summary / text / encrypted). Concatenate the plaintext fields lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content)
// 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) if (delta?.content) lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content)
@@ -1,84 +1,22 @@
import { Effect, Schema } from "effect"
import { Route, type RouteRoutedModelInput } from "../route/client" import { Route, type RouteRoutedModelInput } from "../route/client"
import { Endpoint } from "../route/endpoint" import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing" 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" import * as OpenAIChat from "./openai-chat"
const ADAPTER = "openai-compatible-chat" const ADAPTER = "openai-compatible-chat"
export type OpenAICompatibleChatModelInput = RouteRoutedModelInput 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 * Route for non-OpenAI providers that expose an OpenAI Chat-compatible
* `/chat/completions` endpoint. Reuses OpenAI Chat streaming behavior while * `/chat/completions` endpoint. Reuses `OpenAIChat.protocol` end-to-end and
* allowing compatible providers to pass through additional request-body * overrides only the route id so providers can be resolved per-family without
* options such as `enable_thinking` and extended reasoning efforts. * colliding with native OpenAI. Provider helpers configure the route endpoint
* before model selection.
*/ */
export const route = Route.make({ export const route = Route.make({
id: ADAPTER, id: ADAPTER,
protocol, protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"), endpoint: Endpoint.path("/chat/completions"),
framing: Framing.sse, framing: Framing.sse,
}) })
+238 -83
View File
@@ -19,7 +19,6 @@ import {
} from "../schema" } from "../schema"
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared" import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { OpenAIOptions } from "./utils/openai-options" import { OpenAIOptions } from "./utils/openai-options"
import { ProviderOptions } from "./utils/provider-options"
import { Lifecycle } from "./utils/lifecycle" import { Lifecycle } from "./utils/lifecycle"
import { ToolStream } from "./utils/tool-stream" import { ToolStream } from "./utils/tool-stream"
@@ -58,6 +57,11 @@ const OpenAIResponsesReasoningItem = Schema.Struct({
encrypted_content: optionalNull(Schema.String), 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 // `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. // array of content items so tools can return images in addition to text.
// https://platform.openai.com/docs/api-reference/responses/object // https://platform.openai.com/docs/api-reference/responses/object
@@ -73,6 +77,7 @@ const OpenAIResponsesInputItem = Schema.Union([
Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }),
Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }),
OpenAIResponsesReasoningItem, OpenAIResponsesReasoningItem,
OpenAIResponsesItemReference,
Schema.Struct({ Schema.Struct({
type: Schema.tag("function_call"), type: Schema.tag("function_call"),
call_id: Schema.String, call_id: Schema.String,
@@ -87,6 +92,15 @@ const OpenAIResponsesInputItem = Schema.Union([
]) ])
type OpenAIResponsesInputItem = Schema.Schema.Type<typeof OpenAIResponsesInputItem> 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({ const OpenAIResponsesTool = Schema.Struct({
type: Schema.tag("function"), type: Schema.tag("function"),
name: Schema.String, name: Schema.String,
@@ -112,23 +126,12 @@ const OpenAIResponsesCoreFields = {
tools: optionalArray(OpenAIResponsesTool), tools: optionalArray(OpenAIResponsesTool),
tool_choice: Schema.optional(OpenAIResponsesToolChoice), tool_choice: Schema.optional(OpenAIResponsesToolChoice),
store: Schema.optional(Schema.Boolean), 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), prompt_cache_key: Schema.optional(Schema.String),
prompt_cache_retention: Schema.optional(Schema.String), include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
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( reasoning: Schema.optional(
Schema.Struct({ Schema.Struct({
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort), effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
summary: Schema.optional(Schema.String), summary: Schema.optional(Schema.Literal("auto")),
}), }),
), ),
text: Schema.optional( text: Schema.optional(
@@ -141,15 +144,10 @@ const OpenAIResponsesCoreFields = {
top_p: Schema.optional(Schema.Number), top_p: Schema.optional(Schema.Number),
} }
// Unknown provider options are passed through verbatim with their top-level const OpenAIResponsesBody = Schema.Struct({
// key snake-cased; the rest record validates them against any JSON value. ...OpenAIResponsesCoreFields,
const OpenAIResponsesBody = Schema.StructWithRest( stream: Schema.Literal(true),
Schema.Struct({ })
...OpenAIResponsesCoreFields,
stream: Schema.Literal(true),
}),
[Schema.Record(Schema.String, Schema.Any)],
)
export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody> export type OpenAIResponsesBody = Schema.Schema.Type<typeof OpenAIResponsesBody>
const OpenAIResponsesWebSocketMessage = Schema.StructWithRest( const OpenAIResponsesWebSocketMessage = Schema.StructWithRest(
@@ -210,6 +208,7 @@ const OpenAIResponsesEvent = Schema.Struct({
type: Schema.String, type: Schema.String,
delta: Schema.optional(Schema.String), delta: Schema.optional(Schema.String),
item_id: Schema.optional(Schema.String), item_id: Schema.optional(Schema.String),
summary_index: Schema.optional(Schema.Number),
item: Schema.optional(OpenAIResponsesStreamItem), item: Schema.optional(OpenAIResponsesStreamItem),
response: Schema.optional( response: Schema.optional(
Schema.StructWithRest( Schema.StructWithRest(
@@ -233,6 +232,18 @@ interface ParserState {
readonly tools: ToolStream.State<string> readonly tools: ToolStream.State<string>
readonly hasFunctionCall: boolean readonly hasFunctionCall: boolean
readonly lifecycle: Lifecycle.State 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 const invalid = ProviderShared.invalidRequest
@@ -262,22 +273,21 @@ const lowerToolCall = (part: ToolCallPart): OpenAIResponsesInputItem => ({
arguments: ProviderShared.encodeJson(part.input), arguments: ProviderShared.encodeJson(part.input),
}) })
const lowerReasoning = (part: ReasoningPart, store: boolean | undefined): OpenAIResponsesInputItem | undefined => { const lowerReasoning = (part: ReasoningPart): OpenAIResponsesReasoningInput | undefined => {
const openai = part.providerMetadata?.openai const openai = part.providerMetadata?.openai
if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string") return undefined if (!ProviderShared.isRecord(openai) || typeof openai.itemId !== "string" || openai.itemId.length === 0)
// With store:false, OpenAI only accepts previous reasoning items when the return undefined
// encrypted state is present. Bare rs_* ids point to non-persisted items. const encryptedContent =
if (store === false && typeof openai.reasoningEncryptedContent !== "string") return undefined typeof openai.reasoningEncryptedContent === "string"
? openai.reasoningEncryptedContent
: openai.reasoningEncryptedContent === null
? null
: undefined
return { return {
type: "reasoning", type: "reasoning",
id: openai.itemId, id: openai.itemId,
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [], summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
encrypted_content: encrypted_content: encryptedContent,
typeof openai.reasoningEncryptedContent === "string"
? openai.reasoningEncryptedContent
: openai.reasoningEncryptedContent === null
? null
: undefined,
} }
} }
@@ -310,15 +320,14 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput")
// Text/json/error results are encoded as a plain string for backward // Text/json/error results are encoded as a plain string for backward
// compatibility with existing cassettes and provider expectations. // compatibility with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part) if (part.result.type !== "content") return ProviderShared.toolResultText(part)
const items: ReadonlyArray<ToolResultContentPart> = part.result.value return yield* Effect.forEach(part.result.value, lowerToolResultContentItem)
return yield* Effect.forEach(items, lowerToolResultContentItem)
}) })
const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) {
const system: OpenAIResponsesInputItem[] = const system: OpenAIResponsesInputItem[] =
request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }]
const input: OpenAIResponsesInputItem[] = [...system] const input: OpenAIResponsesInputItem[] = [...system]
const store = OpenAIOptions.options(request).store const store = OpenAIOptions.store(request)
for (const message of request.messages) { for (const message of request.messages) {
if (message.role === "user") { if (message.role === "user") {
@@ -328,6 +337,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
if (message.role === "assistant") { if (message.role === "assistant") {
const content: TextPart[] = [] const content: TextPart[] = []
const reasoningItems: Record<string, OpenAIResponsesReasoningInput> = {}
const reasoningReferences = new Set<string>()
const flushText = () => { const flushText = () => {
if (content.length === 0) return if (content.length === 0) return
input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) }) input.push({ role: "assistant", content: content.map((part) => ({ type: "output_text", text: part.text })) })
@@ -340,8 +351,22 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
} }
if (part.type === "reasoning") { if (part.type === "reasoning") {
flushText() flushText()
const reasoning = lowerReasoning(part, store) const reasoning = lowerReasoning(part)
if (reasoning) input.push(reasoning) 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)
continue continue
} }
if (part.type === "tool-call") { if (part.type === "tool-call") {
@@ -370,50 +395,35 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
} }
} }
return input // 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
}) })
const lowerOptions = (request: LLMRequest) => { const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
const options = OpenAIOptions.options(request) const store = OpenAIOptions.store(request)
// OpenAI Responses does not accept the `max` reasoning effort variant. const promptCacheKey = OpenAIOptions.promptCacheKey(request)
const effort = OpenAIOptions.isReasoningEffort(options.reasoningEffort) ? options.reasoningEffort : undefined const effort = OpenAIOptions.reasoningEffort(request)
const summary = options.reasoningSummary if (effort && !OpenAIOptions.isReasoningEffort(effort))
const verbosity = options.textVerbosity return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
// `logprobs` is enabled only by `true` or a numeric top-N. `false` and const summary = OpenAIOptions.reasoningSummary(request)
// `undefined` leave the request without the logprobs include + top_logprobs. const include = OpenAIOptions.include(request)
const logprobsEnabled = options.logprobs === true || typeof options.logprobs === "number" const verbosity = OpenAIOptions.textVerbosity(request)
const include = (() => { const instructions = OpenAIOptions.instructions(request)
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 { return {
...ProviderOptions.passthrough(options, OpenAIOptions.KNOWN_KEYS), ...(instructions ? { instructions } : {}),
...(options.instructions !== undefined ? { instructions: options.instructions } : {}), ...(store !== undefined ? { store } : {}),
...(options.store !== undefined ? { store: options.store } : {}), ...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
...(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 } : {}), ...(include ? { include } : {}),
...(effort !== undefined || summary !== undefined ? { reasoning: { effort, summary } } : {}), ...(effort || summary ? { reasoning: { effort, summary } } : {}),
...(verbosity !== undefined ? { text: { verbosity } } : {}), ...(verbosity ? { text: { verbosity } } : {}),
} }
} })
const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) { const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: LLMRequest) {
const generation = request.generation const generation = request.generation
@@ -426,7 +436,7 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request:
max_output_tokens: generation?.maxTokens, max_output_tokens: generation?.maxTokens,
temperature: generation?.temperature, temperature: generation?.temperature,
top_p: generation?.topP, top_p: generation?.topP,
...lowerOptions(request), ...(yield* lowerOptions(request)),
} }
}) })
@@ -557,24 +567,51 @@ const onOutputTextDelta = (state: ParserState, event: OpenAIResponsesEvent): Ste
const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => { const onReasoningDelta = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
if (!event.delta) return [state, NO_EVENTS] if (!event.delta) return [state, NO_EVENTS]
const events: LLMEvent[] = [] 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 [ return [
{ {
...state, ...state,
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, event.item_id ?? "reasoning-0", event.delta), lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, id, event.delta),
}, },
events, 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 onReasoningDone = (state: ParserState, _event: OpenAIResponsesEvent): StepResult => [state, NO_EVENTS]
const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) => const reasoningMetadata = (item: OpenAIResponsesStreamItem & { id: string }) =>
openaiMetadata({ itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null }) 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 onOutputItemAdded = (state: ParserState, event: OpenAIResponsesEvent): StepResult => {
const item = event.item 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] if (item?.type !== "function_call" || !item.id) return [state, NO_EVENTS]
const providerMetadata = openaiMetadata({ itemId: item.id }) const providerMetadata = openaiMetadata({ itemId: item.id })
const events: LLMEvent[] = [] const events: LLMEvent[] = []
@@ -595,6 +632,103 @@ 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* ( const onFunctionCallArgumentsDelta = Effect.fn("OpenAIResponses.onFunctionCallArgumentsDelta")(function* (
state: ParserState, state: ParserState,
event: OpenAIResponsesEvent, event: OpenAIResponsesEvent,
@@ -655,6 +789,17 @@ const onOutputItemDone = Effect.fn("OpenAIResponses.onOutputItemDone")(function*
if (isReasoningItem(item)) { if (isReasoningItem(item)) {
const events: LLMEvent[] = [] const events: LLMEvent[] = []
const providerMetadata = reasoningMetadata(item) 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)) { if (!state.lifecycle.reasoning.has(item.id)) {
const lifecycle = Lifecycle.stepStart(state.lifecycle, events) const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata })) events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata }))
@@ -723,6 +868,10 @@ const step = (state: ParserState, event: OpenAIResponsesEvent) => {
event.type === "response.reasoning_summary_text.done" event.type === "response.reasoning_summary_text.done"
) )
return Effect.succeed(onReasoningDone(state, event)) 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.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.function_call_arguments.delta") return onFunctionCallArgumentsDelta(state, event)
if (event.type === "response.output_item.done") return onOutputItemDone(state, event) if (event.type === "response.output_item.done") return onOutputItemDone(state, event)
@@ -749,7 +898,13 @@ export const protocol = Protocol.make({
}, },
stream: { stream: {
event: Protocol.jsonEvent(OpenAIResponsesEvent), event: Protocol.jsonEvent(OpenAIResponsesEvent),
initial: () => ({ hasFunctionCall: false, tools: ToolStream.empty<string>(), lifecycle: Lifecycle.initial() }), initial: (request) => ({
hasFunctionCall: false,
tools: ToolStream.empty<string>(),
lifecycle: Lifecycle.initial(),
reasoningItems: {},
store: OpenAIOptions.store(request),
}),
step, step,
terminal: (event) => TERMINAL_TYPES.has(event.type), terminal: (event) => TERMINAL_TYPES.has(event.type),
}, },
+14 -6
View File
@@ -24,16 +24,24 @@ export const textDelta = (state: State, events: LLMEvent[], id: string, text: st
return { ...stepped, text: new Set([...stepped.text, id]) } return { ...stepped, text: new Set([...stepped.text, id]) }
} }
export const reasoningDelta = (state: State, events: LLMEvent[], id: string, text: string): State => { export const reasoningStart = (
state: State,
events: LLMEvent[],
id: string,
providerMetadata?: ProviderMetadata,
): State => {
if (state.reasoning.has(id)) return state
const stepped = stepStart(state, events) const stepped = stepStart(state, events)
if (stepped.reasoning.has(id)) { events.push(LLMEvent.reasoningStart({ id, providerMetadata }))
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]) } 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
}
export const reasoningEnd = ( export const reasoningEnd = (
state: State, state: State,
events: LLMEvent[], events: LLMEvent[],
@@ -1,73 +1,84 @@
import { Schema } from "effect" import { Schema } from "effect"
import type { LLMRequest } from "../../schema" import type { LLMRequest, ReasoningEffort, TextVerbosity as TextVerbosityValue } from "../../schema"
import { ReasoningEfforts, TextVerbosity, type ReasoningEffort } from "../../schema" import { ReasoningEfforts, TextVerbosity } from "../../schema"
import { ProviderOptions } from "./provider-options"
export const OpenAIReasoningEfforts = ReasoningEfforts.filter( export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
(effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max", (effort): effort is Exclude<ReasoningEffort, "max"> => effort !== "max",
) )
export type OpenAIReasoningEffort = (typeof OpenAIReasoningEfforts)[number] 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 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 OpenAIReasoningEffort = Schema.Literals(OpenAIReasoningEfforts)
export const OpenAITextVerbosity = TextVerbosity 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 => export const isReasoningEffort = (effort: unknown): effort is OpenAIReasoningEffort =>
typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort) typeof effort === "string" && OPENAI_REASONING_EFFORTS.has(effort)
// Typed AI SDK OpenAI options. Mirrors the camelCase surface AI SDK accepts. const isTextVerbosity = (value: unknown): value is TextVerbosityValue =>
// Known keys are typed; everything else passes through to the wire body with typeof value === "string" && TEXT_VERBOSITY.has(value)
// its top-level key snake-cased.
export interface Options { const options = (request: LLMRequest) => request.providerOptions?.openai
readonly store?: boolean
readonly promptCacheKey?: string export const store = (request: LLMRequest): boolean | undefined => {
readonly promptCacheRetention?: string const value = options(request)?.store
readonly reasoningEffort?: ReasoningEffort return typeof value === "boolean" ? value : undefined
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 KNOWN_KEYS: ReadonlySet<string> = new Set([ export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefined => {
"store", const value = options(request)?.reasoningEffort
"promptCacheKey", return isAnyReasoningEffort(value) ? value : undefined
"promptCacheRetention", }
"reasoningEffort",
"reasoningSummary",
"textVerbosity",
"include",
"includeEncryptedReasoning",
"instructions",
"conversation",
"maxToolCalls",
"metadata",
"parallelToolCalls",
"previousResponseId",
"safetyIdentifier",
"serviceTier",
"logprobs",
"truncation",
"user",
])
// Read the merged `openai` provider option bag. Producers export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>
// (`packages/opencode/src/provider/transform.ts`) emit typed values; we widen options(request)?.reasoningSummary === "auto" ? "auto" : undefined
// only the index signature so passthrough keys remain reachable. Invalid
// shapes surface in the lowerer where they're consumed, not at decode time. // Resolve the OpenAI Responses `include` field. Filters out unknown
export const options = (request: LLMRequest): Options => ProviderOptions.merge(request, ["openai"]) as Options // 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
}
export * as OpenAIOptions from "./openai-options" export * as OpenAIOptions from "./openai-options"
@@ -1,34 +0,0 @@
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"
+14 -2
View File
@@ -1,5 +1,8 @@
import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema" import type { ProviderOptions, ReasoningEffort, TextVerbosity } from "../schema"
import { mergeProviderOptions } 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 { export interface OpenAIOptionsInput {
readonly [key: string]: unknown readonly [key: string]: unknown
@@ -7,7 +10,10 @@ export interface OpenAIOptionsInput {
readonly promptCacheKey?: string readonly promptCacheKey?: string
readonly reasoningEffort?: ReasoningEffort readonly reasoningEffort?: ReasoningEffort
readonly reasoningSummary?: "auto" readonly reasoningSummary?: "auto"
readonly includeEncryptedReasoning?: boolean // 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 textVerbosity?: TextVerbosity readonly textVerbosity?: TextVerbosity
} }
@@ -25,7 +31,7 @@ const openAIProviderOptions = (options: OpenAIOptionsInput | undefined): Provide
promptCacheKey: options?.promptCacheKey, promptCacheKey: options?.promptCacheKey,
reasoningEffort: options?.reasoningEffort, reasoningEffort: options?.reasoningEffort,
reasoningSummary: options?.reasoningSummary, reasoningSummary: options?.reasoningSummary,
includeEncryptedReasoning: options?.includeEncryptedReasoning, include: options?.include,
textVerbosity: options?.textVerbosity, textVerbosity: options?.textVerbosity,
}), }),
) )
@@ -42,6 +48,12 @@ export const gpt5DefaultOptions = (
return openAIProviderOptions({ return openAIProviderOptions({
reasoningEffort: "medium", reasoningEffort: "medium",
reasoningSummary: "auto", 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: textVerbosity:
options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat") options.textVerbosity === true && id.includes("gpt-5.") && !id.includes("codex") && !id.includes("-chat")
? "low" ? "low"
+1 -1
View File
@@ -5,7 +5,7 @@ import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses" import * as OpenAIResponses from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options" import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export type { OpenAIOptionsInput } from "./openai-options" export type { OpenAIOptionsInput, OpenAIResponseIncludable } from "./openai-options"
export const id = ProviderID.make("openai") export const id = ProviderID.make("openai")
+1 -1
View File
@@ -283,7 +283,7 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
) )
return events.pipe( return events.pipe(
Stream.mapAccumEffect( Stream.mapAccumEffect(
protocol.stream.initial, () => protocol.stream.initial(request),
protocol.stream.step, protocol.stream.step,
protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined, 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> { export interface ProtocolStream<Frame, Event, State> {
/** Schema for one decoded streaming event, decoded from a transport frame. */ /** Schema for one decoded streaming event, decoded from a transport frame. */
readonly event: Schema.Codec<Event, Frame> readonly event: Schema.Codec<Event, Frame>
/** Initial parser state. Called once per response. */ /** Initial parser state. Called once per response with the resolved request. */
readonly initial: () => State readonly initial: (request: LLMRequest) => State
/** Translate one event into emitted `LLMEvent`s plus the next 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> readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
/** Optional request-completion signal for transports that do not end naturally. */ /** 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] : [], tools: features.has("tool-call") ? [continuationTool] : [],
cache: "none", cache: "none",
providerOptions: features.has("encrypted-reasoning") providerOptions: features.has("encrypted-reasoning")
? { openai: { store: false, includeEncryptedReasoning: true, reasoningSummary: "auto" } } ? { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningSummary: "auto" } }
: undefined, : undefined,
generation: { maxTokens: 80, temperature: 0 }, 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,80 +209,6 @@ 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", () => it.effect("lowers preserved Anthropic reasoning signature metadata", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* LLMClient.prepare( 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 OpenAICompatible from "../../src/providers/openai-compatible"
import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat" import * as OpenAICompatibleChat from "../../src/protocols/openai-compatible-chat"
import { it } from "../lib/effect" import { it } from "../lib/effect"
import { dynamicResponse, fixedResponse } from "../lib/http" import { dynamicResponse } from "../lib/http"
import { sseEvents } from "../lib/sse" import { sseEvents } from "../lib/sse"
const Json = Schema.fromJsonString(Schema.Unknown) const Json = Schema.fromJsonString(Schema.Unknown)
@@ -199,134 +199,6 @@ 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", () => it.effect("posts to the configured compatible endpoint and parses text usage", () =>
Effect.gen(function* () { Effect.gen(function* () {
const response = yield* LLMClient.generate(request).pipe( const response = yield* LLMClient.generate(request).pipe(
@@ -393,7 +393,7 @@ describe("OpenAI Responses route", () => {
promptCacheKey: "session_123", promptCacheKey: "session_123",
reasoningEffort: "high", reasoningEffort: "high",
reasoningSummary: "auto", reasoningSummary: "auto",
includeEncryptedReasoning: true, include: ["reasoning.encrypted_content"],
}, },
}, },
}), }),
@@ -407,27 +407,105 @@ describe("OpenAI Responses route", () => {
}), }),
) )
it.effect("passes unknown OpenAI provider options through with snake-cased keys", () => it.effect("accepts the full ResponseIncludable union", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>( const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
LLM.request({ LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).model("gpt-5.2"), model,
prompt: "passthrough", prompt: "hi",
providerOptions: { providerOptions: {
openai: { openai: {
customCamelCaseField: "value", include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"],
already_snake_case: 42,
nested: { keepCamelCase: true },
}, },
}, },
}), }),
) )
expect(prepared.body).toMatchObject({ expect(prepared.body.include).toEqual([
custom_camel_case_field: "value", "reasoning.encrypted_content",
already_snake_case: 42, "code_interpreter_call.outputs",
nested: { keepCamelCase: true }, "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()
}), }),
) )
@@ -571,6 +649,94 @@ 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", () => it.effect("continues a stateless reasoning conversation", () =>
Effect.gen(function* () { Effect.gen(function* () {
const response = yield* LLMClient.generate( const response = yield* LLMClient.generate(
@@ -594,6 +760,7 @@ describe("OpenAI Responses route", () => {
]), ]),
Message.user("Summarize it."), Message.user("Summarize it."),
], ],
providerOptions: { openai: { store: false } },
}), }),
).pipe( ).pipe(
Effect.provide( Effect.provide(
@@ -651,6 +818,7 @@ describe("OpenAI Responses route", () => {
{ type: "text", text: "After." }, { type: "text", text: "After." },
]), ]),
], ],
providerOptions: { openai: { store: false } },
}), }),
) )
@@ -667,6 +835,66 @@ 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", () => it.effect("skips non-persisted reasoning ids without encrypted state", () =>
Effect.gen(function* () { Effect.gen(function* () {
const prepared = yield* LLMClient.prepare( const prepared = yield* LLMClient.prepare(
+1 -1
View File
@@ -158,7 +158,7 @@ const normalizeImageText = (value: string) =>
const encryptedReasoningOptions = { const encryptedReasoningOptions = {
openai: { openai: {
store: false, store: false,
includeEncryptedReasoning: true, include: ["reasoning.encrypted_content"],
reasoningEffort: "low", reasoningEffort: "low",
reasoningSummary: "auto", reasoningSummary: "auto",
}, },
+75
View File
@@ -4,6 +4,7 @@ import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice }
import { Auth, LLMClient } from "../src/route" import { Auth, LLMClient } from "../src/route"
import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as AnthropicMessages from "../src/protocols/anthropic-messages"
import * as OpenAIChat from "../src/protocols/openai-chat" 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 { tool, ToolFailure, type ToolExecuteContext } from "../src/tool"
import { ToolRuntime } from "../src/tool-runtime" import { ToolRuntime } from "../src/tool-runtime"
import { it } from "./lib/effect" import { it } from "./lib/effect"
@@ -309,6 +310,80 @@ 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", () => it.effect("emits tool-error for unknown tools so the model can self-correct", () =>
Effect.gen(function* () { Effect.gen(function* () {
const layer = scriptedResponses([ const layer = scriptedResponses([
+3 -3
View File
@@ -74,15 +74,15 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0", "@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.75",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.131",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@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 { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { import {
FetchHttpClient, FetchHttpClient,
HttpClient, HttpClient,
+1 -1
View File
@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm" import { eq } from "drizzle-orm"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Effect, Layer, Option, Schema, Context } from "effect" import { Effect, Layer, Option, Schema, Context } from "effect"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { ModelID, ProviderID } from "../provider/schema" import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai" 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 { GlobalBus } from "./global"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import type { InstanceContext } from "@/project/instance-context" import type { InstanceContext } from "@/project/instance-context"
import { InstanceRef } from "@/effect/instance-ref" import { InstanceRef } from "@/effect/instance-ref"
+1 -1
View File
@@ -1,5 +1,5 @@
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import os from "os" import os from "os"
@@ -1,5 +1,5 @@
import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect" import { Context, Effect, FiberMap, Iterable, Layer, Schema, Stream } from "effect"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http" import { FetchHttpClient, HttpBody, HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { asc } from "drizzle-orm" import { asc } from "drizzle-orm"
@@ -50,7 +50,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"), experimentalIconDiscovery: enabledByExperimental("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY"),
outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"), outputTokenMax: positiveInteger("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"), bashDefaultTimeoutMs: positiveInteger("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
experimentalNativeLlm: enabledByExperimental("OPENCODE_EXPERIMENTAL_NATIVE_LLM"), experimentalNativeLlm: bool("OPENCODE_EXPERIMENTAL_NATIVE_LLM"),
client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")), client: Config.string("OPENCODE_CLIENT").pipe(Config.withDefault("cli")),
}) {} }) {}
+1 -1
View File
@@ -1,5 +1,5 @@
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
type State = Record<string, string | undefined> type State = Record<string, string | undefined>
+1 -1
View File
@@ -1,5 +1,5 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
+1 -1
View File
@@ -1,5 +1,5 @@
import path from "path" import path from "path"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect" import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect"
import type { PlatformError } from "effect/PlatformError" import type { PlatformError } from "effect/PlatformError"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect, Layer, Context, Schema } from "effect" import { Effect, Layer, Context, Schema } from "effect"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { ChildProcess } from "effect/unstable/process" import { ChildProcess } from "effect/unstable/process"
import { AppProcess } from "@opencode-ai/core/process" import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect, Layer, Schema, Context, Stream } from "effect" import { Effect, Layer, Schema, Context, Stream } from "effect"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { withTransientReadRetry } from "@/util/effect-http-client" import { withTransientReadRetry } from "@/util/effect-http-client"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
+1 -1
View File
@@ -1,5 +1,5 @@
import path from "path" import path from "path"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Effect, Layer, Context, Option, Schema } from "effect" import { Effect, Layer, Context, Option, Schema } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem" 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 { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
@@ -1,5 +1,5 @@
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import { InstanceRef } from "@/effect/instance-ref" import { InstanceRef } from "@/effect/instance-ref"
import { disposeInstance as runDisposers } from "@/effect/instance-registry" import { disposeInstance as runDisposers } from "@/effect/instance-registry"
+6 -4
View File
@@ -1,6 +1,5 @@
import { and } from "drizzle-orm" import { and, eq, sql } from "drizzle-orm"
import { Database } from "@/storage/db" import { Database } from "@/storage/db"
import { eq } from "drizzle-orm"
import { ProjectTable } from "./project.sql" import { ProjectTable } from "./project.sql"
import { PermissionTable, SessionTable } from "../session/session.sql" import { PermissionTable, SessionTable } from "../session/session.sql"
import { WorkspaceTable } from "../control-plane/workspace.sql" import { WorkspaceTable } from "../control-plane/workspace.sql"
@@ -20,7 +19,7 @@ import { AppProcess } from "@opencode-ai/core/process"
import { Project as ProjectV2 } from "@opencode-ai/core/project" import { Project as ProjectV2 } from "@opencode-ai/core/project"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema" import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
const log = Log.create({ service: "project" }) const log = Log.create({ service: "project" })
@@ -220,7 +219,10 @@ export const layer = Layer.effect(
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run() d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
} }
d.update(SessionTable).set({ project_id: newID }).where(eq(SessionTable.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(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.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() 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 type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Auth } from "@/auth" import { Auth } from "@/auth"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { optionalOmitUndefined } from "@opencode-ai/core/schema" 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 { Npm } from "@opencode-ai/core/npm"
import { Hash } from "@opencode-ai/core/util/hash" import { Hash } from "@opencode-ai/core/util/hash"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { type LanguageModelV3 } from "@ai-sdk/provider" import { type LanguageModelV3 } from "@ai-sdk/provider"
import * as ModelsDev from "@opencode-ai/core/models-dev" import * as ModelsDev from "@opencode-ai/core/models-dev"
import { Auth } from "../auth" import { Auth } from "../auth"
+12 -4
View File
@@ -17,6 +17,11 @@ function mimeToModality(mime: string): Modality | undefined {
export const OUTPUT_TOKEN_MAX = 32_000 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) { export function sanitizeSurrogates(content: string) {
return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD") return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD")
} }
@@ -756,7 +761,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{ {
reasoningEffort: effort, reasoningEffort: effort,
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: INCLUDE_ENCRYPTED_REASONING,
}, },
]), ]),
) )
@@ -790,7 +795,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{ {
reasoningEffort: effort, reasoningEffort: effort,
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: INCLUDE_ENCRYPTED_REASONING,
}, },
]), ]),
) )
@@ -803,7 +808,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
{ {
reasoningEffort: effort, reasoningEffort: effort,
reasoningSummary: "auto", reasoningSummary: "auto",
include: ["reasoning.encrypted_content"], include: INCLUDE_ENCRYPTED_REASONING,
}, },
]), ]),
) )
@@ -1134,6 +1139,9 @@ export function options(input: {
if (!input.model.api.id.includes("gpt-5-pro")) { if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium" result["reasoningEffort"] = "medium"
result["reasoningSummary"] = "auto" 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 // Only set textVerbosity for non-chat gpt-5.x models
@@ -1149,7 +1157,7 @@ export function options(input: {
if (input.model.providerID.startsWith("opencode")) { if (input.model.providerID.startsWith("opencode")) {
result["promptCacheKey"] = input.sessionID result["promptCacheKey"] = input.sessionID
result["include"] = ["reasoning.encrypted_content"] result["include"] = INCLUDE_ENCRYPTED_REASONING
result["reasoningSummary"] = "auto" 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 * as DateTime from "effect/DateTime"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { isOverflow as overflow, usable } from "./overflow" import { isOverflow as overflow, usable } from "./overflow"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2Bridge } from "@/event-v2-bridge"
import { SessionEvent } from "@opencode-ai/core/session-event" import { SessionEvent } from "@opencode-ai/core/session-event"
+1 -1
View File
@@ -1,5 +1,5 @@
import { Provider } from "@/provider/provider" import { Provider } from "@/provider/provider"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { Context, Effect, Layer } from "effect" import { Context, Effect, Layer } from "effect"
import * as Stream from "effect/Stream" import * as Stream from "effect/Stream"
@@ -110,9 +110,7 @@ const messages = (input: readonly ModelMessage[]) => {
Message.make({ Message.make({
role: message.role, role: message.role,
content: content(message.content), content: content(message.content),
// Message provider options are already provider-native wire metadata native: isRecord(message.providerOptions) ? { providerOptions: message.providerOptions } : undefined,
// (for example DeepSeek's reasoning_content continuation field).
native: isRecord(message.providerOptions) ? message.providerOptions : undefined,
}), }),
] ]
}) })
@@ -70,6 +70,14 @@ export function stream(input: StreamInput): StreamResult {
// Integration point with @opencode-ai/llm: native-request lowers session data // Integration point with @opencode-ai/llm: native-request lowers session data
// into an LLMRequest, then LLMClient handles route selection and transport. // 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({ const stream = input.llmClient.stream({
request: LLMNative.request({ request: LLMNative.request({
model: input.model, model: input.model,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Slug } from "@opencode-ai/core/util/slug" import { Slug } from "@opencode-ai/core/util/slug"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import path from "path" import path from "path"
import { BackgroundJob } from "@/background/job" import { BackgroundJob } from "@/background/job"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
+1 -1
View File
@@ -1,5 +1,5 @@
import type * as SDK from "@opencode-ai/sdk/v2" import type * as SDK from "@opencode-ai/sdk/v2"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect" import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { Account } from "@/account/account" 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 { Context, Effect, Layer, Schema as EffectSchema } from "effect"
import type { DeepMutable } from "@opencode-ai/core/schema" import type { DeepMutable } from "@opencode-ai/core/schema"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { serviceUse } from "@/effect/service-use" import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
import { EffectBridge } from "@/effect/bridge" 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.experimentalEventSystem).toBe(true)
expect(flags.experimentalWorkspaces).toBe(true) expect(flags.experimentalWorkspaces).toBe(true)
expect(flags.experimentalIconDiscovery).toBe(true) expect(flags.experimentalIconDiscovery).toBe(true)
expect(flags.experimentalNativeLlm).toBe(true) expect(flags.experimentalNativeLlm).toBe(false)
expect(flags.client).toBe("desktop") expect(flags.client).toBe("desktop")
}), }),
) )
@@ -81,13 +81,13 @@ describe("RuntimeFlags", () => {
}), }),
) )
it.effect("enables native LLM via dedicated or umbrella flag", () => it.effect("enables native LLM via dedicated flag only", () =>
Effect.gen(function* () { Effect.gen(function* () {
const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" }))) const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL_NATIVE_LLM: "true" })))
const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" }))) const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ OPENCODE_EXPERIMENTAL: "true" })))
expect(explicit.experimentalNativeLlm).toBe(true) expect(explicit.experimentalNativeLlm).toBe(true)
expect(umbrella.experimentalNativeLlm).toBe(true) expect(umbrella.experimentalNativeLlm).toBe(false)
}), }),
) )
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5 -81
View File
@@ -7,15 +7,6 @@ import path from "path"
import { tmpdirScoped } from "../fixture/fixture" import { tmpdirScoped } from "../fixture/fixture"
import { GlobalBus } from "../../src/bus/global" import { GlobalBus } from "../../src/bus/global"
import { ProjectID } from "../../src/project/schema" 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 { Cause, Effect, Exit, Layer, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { NodePath } from "@effect/platform-node" import { NodePath } from "@effect/platform-node"
@@ -40,10 +31,6 @@ 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 * Creates a mock ChildProcessSpawner layer that intercepts git subcommands
* matching `failArg` and returns exit code 128, while delegating everything * matching `failArg` and returns exit code 128, while delegating everything
@@ -167,91 +154,28 @@ describe("Project.fromDirectory", () => {
}), }),
) )
it.live("prefers normalized origin remote over root commit", () => it.live("keeps root commit identity when origin exists", () =>
Effect.gen(function* () { Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ git: true }) const tmp = yield* tmpdirScoped({ git: true })
yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet()) 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 { 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(remoteProjectID("github.com/Test-Org/Test-Repo")) expect(project.id).toBe(ProjectID.make(root))
}), }),
) )
it.live("normalizes equivalent origin URL forms to the same project ID", () => it.live("keeps cached project identity when origin becomes available", () =>
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* () { Effect.gen(function* () {
const tmp = yield* tmpdirScoped({ git: true }) const tmp = yield* tmpdirScoped({ git: true })
const projects = yield* Project.Service const projects = yield* Project.Service
const { project: rootProject } = yield* projects.fromDirectory(tmp) 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()) yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
const { project } = yield* projects.fromDirectory(tmp) const { project } = yield* projects.fromDirectory(tmp)
expect(project.id).toBe(remoteID) expect(project.id).toBe(rootProject.id)
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,6 +271,7 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
const model = createGpt5Model("gpt-5.2") const model = createGpt5Model("gpt-5.2")
const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) const result = ProviderTransform.options({ model, sessionID, providerOptions: {} })
expect(result.textVerbosity).toBe("low") expect(result.textVerbosity).toBe("low")
expect(result.include).toEqual(["reasoning.encrypted_content"])
}) })
test("gpt-5.1 should have textVerbosity set to low", () => { test("gpt-5.1 should have textVerbosity set to low", () => {
@@ -336,10 +336,25 @@ const weatherTool = tool({
}) })
const toolRoundtrip = ( const toolRoundtrip = (
events: ReadonlyArray<LLMEvent>,
call: { readonly id: string; readonly name: string; readonly input: unknown }, call: { readonly id: string; readonly name: string; readonly input: unknown },
result: JSONValue, result: JSONValue,
): ModelMessage[] => [ ): ModelMessage[] => [
{ role: "assistant", content: [{ type: "tool-call", toolCallId: call.id, toolName: call.name, input: call.input }] }, {
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: "tool", role: "tool",
content: [ content: [
@@ -395,7 +410,7 @@ const driveToolLoop = (scenario: RecordedScenario) =>
const turn2 = yield* collect({ const turn2 = yield* collect({
...base, ...base,
messages: [userMessage, ...toolRoundtrip(toolCall!, WEATHER_RESULT)], messages: [userMessage, ...toolRoundtrip(turn1, toolCall!, WEATHER_RESULT)],
}) })
expect(LLMResponse.text({ events: turn2 })).toMatch(/Paris is sunny/i) expect(LLMResponse.text({ events: turn2 })).toMatch(/Paris is sunny/i)
@@ -6,7 +6,6 @@ import { Effect, Layer, Stream } from "effect"
import { LLMNative } from "@/session/llm/native-request" import { LLMNative } from "@/session/llm/native-request"
import { LLMNativeRuntime } from "@/session/llm/native-runtime" import { LLMNativeRuntime } from "@/session/llm/native-runtime"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { ProviderTransform } from "@/provider/transform"
import { ModelID, ProviderID } from "@/provider/schema" import { ModelID, ProviderID } from "@/provider/schema"
import { OAUTH_DUMMY_KEY } from "@/auth" import { OAUTH_DUMMY_KEY } from "@/auth"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
@@ -71,21 +70,6 @@ const providerInfo: Provider.Info = {
models: {}, 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( const it = testEffect(
LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(RequestExecutor.defaultLayer, WebSocketExecutor.layer))), LLMClient.layer.pipe(Layer.provide(Layer.mergeAll(RequestExecutor.defaultLayer, WebSocketExecutor.layer))),
) )
@@ -342,70 +326,6 @@ 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", () => { test("selects native request routes for provider packages", () => {
const openai = LLMNative.model({ const openai = LLMNative.model({
model: { ...baseModel, api: { ...baseModel.api, url: "", npm: "@ai-sdk/openai" } }, model: { ...baseModel, api: { ...baseModel.api, url: "", npm: "@ai-sdk/openai" } },
@@ -671,7 +591,7 @@ describe("session.llm-native.request", () => {
]), ]),
storedSession.user("Summarize it."), storedSession.user("Summarize it."),
], ],
providerOptions: { openai: { store: false, includeEncryptedReasoning: true } }, providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"] } },
expectedBody: { expectedBody: {
input: [ input: [
openAIResponses.user("What changed?"), openAIResponses.user("What changed?"),
@@ -688,6 +608,45 @@ 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", () => it.effect("uses provider fetch override for native OpenAI OAuth requests", () =>
Effect.gen(function* () { Effect.gen(function* () {
const captures: Array<{ url: string; body: unknown }> = [] const captures: Array<{ url: string; body: unknown }> = []
@@ -1166,6 +1166,7 @@ describe("session.llm.stream", () => {
expect(capture.body.model).toBe(model.id) expect(capture.body.model).toBe(model.id)
expect(capture.body.stream).toBe(true) expect(capture.body.stream).toBe(true)
expect((capture.body.reasoning as { effort?: string } | undefined)?.effort).toBe("high") 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(JSON.stringify(capture.body.input)).toContain("You are a helpful assistant.")
expect(capture.body.input).toContainEqual({ role: "user", content: [{ type: "input_text", text: "Hello" }] }) expect(capture.body.input).toContainEqual({ role: "user", content: [{ type: "input_text", text: "Hello" }] })
}), }),
+20 -14
View File
@@ -594,17 +594,23 @@ opencode upgrade v0.1.48
تقوم متغيرات البيئة التالية بتفعيل ميزات تجريبية قد تتغير أو تتم إزالتها. تقوم متغيرات البيئة التالية بتفعيل ميزات تجريبية قد تتغير أو تتم إزالتها.
| المتغير | النوع | الوصف | | المتغير | النوع | الوصف |
| ----------------------------------------------- | ------- | ------------------------------------------- | | ----------------------------------------------- | ------- | -------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | تفعيل جميع الميزات التجريبية | | `OPENCODE_EXPERIMENTAL` | boolean | تفعيل الميزات التجريبية الخاضعة للعلم الشامل |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | تفعيل اكتشاف الأيقونات |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | تعطيل النسخ عند التحديد في TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | المهلة الافتراضية لأوامر bash بالميلي ثانية |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM | | `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | الحد الأقصى لرموز المخرجات لاستجابات LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | تفعيل مراقب الملفات لكامل الدليل |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | تفعيل مُنسّق oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | تفعيل أداة LSP تجريبية |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | تعطيل مراقب الملفات |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | تفعيل ميزات Exa التجريبية |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | تمكين TY LSP لملفات python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | تفعيل وضع الخطة | | `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 | تفعيل دعم مساحات العمل |
+20 -14
View File
@@ -592,17 +592,23 @@ OpenCode se može konfigurirati pomoću varijabli okruženja.
Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti. Ove varijable okruženja omogućavaju eksperimentalne karakteristike koje se mogu promijeniti ili ukloniti.
| Varijabla | Tip | Opis | | Varijabla | Tip | Opis |
| ----------------------------------------------- | ------- | ------------------------------------------------- | | ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Omogući sve eksperimentalne funkcije | | `OPENCODE_EXPERIMENTAL` | boolean | Omogući eksperimentalne funkcije pod zbirnom zastavicom |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Omogući otkrivanje ikona | | `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_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_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_OUTPUT_TOKEN_MAX` | number | Maksimalni izlazni tokeni za LLM odgovore |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Omogući praćenje datoteka za cijeli direktorij |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Omogući oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Omogući eksperimentalni LSP alat |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Onemogući praćenje datoteka |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Omogući eksperimentalne Exa funkcije |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Omogući TY LSP za python datoteke |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Omogući Plan mod | | `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 |
+7 -1
View File
@@ -712,7 +712,7 @@ These environment variables enable experimental features that may change or be r
| Variable | Type | Description | | Variable | Type | Description |
| ----------------------------------------------- | ------- | --------------------------------------- | | ----------------------------------------------- | ------- | --------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Enable all experimental features | | `OPENCODE_EXPERIMENTAL` | boolean | Enable the experimental umbrella flag |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Enable icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disable copy on select in TUI | | `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 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Default timeout for bash commands in ms |
@@ -724,3 +724,9 @@ These environment variables enable experimental features that may change or be r
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable TY LSP for python files | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable TY LSP for python files |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode | | `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 |
+20 -14
View File
@@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjælp af miljøvariabler.
Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes. Disse miljøvariabler muliggør eksperimentelle funktioner, der kan ændres eller fjernes.
| Variabel | Skriv | Beskrivelse | | Variabel | Skriv | Beskrivelse |
| ----------------------------------------------- | ------- | ----------------------------------------- | | ----------------------------------------------- | ------- | ---------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Aktiver alle eksperimentelle funktioner | | `OPENCODE_EXPERIMENTAL` | boolean | Aktiver eksperimentelle funktioner under samleflaget |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Aktiver ikonopdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Deaktiver kopi ved valg i TUI | | `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_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_OUTPUT_TOKEN_MAX` | nummer | Maks. output-tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Aktiver filovervågning for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Aktive eksperimenter LSP værktøj |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Deaktiver filovervågning |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Aktive eksperimenter Exa-funktioner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Aktiver plantilstand | | `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 |
+20 -14
View File
@@ -594,17 +594,23 @@ OpenCode kann mithilfe von Umgebungsvariablen konfiguriert werden.
Diese Umgebungsvariablen ermöglichen experimentelle Funktionen, die sich ändern oder entfernt werden können. Diese Umgebungsvariablen ermöglichen experimentelle Funktionen, die sich ändern oder entfernt werden können.
| Variable | Typ | Beschreibung | | Variable | Typ | Beschreibung |
| ----------------------------------------------- | --------------- | ------------------------------------------------------- | | ----------------------------------------------- | --------------- | --------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolescher Wert | Alle experimentellen Funktionen aktivieren | | `OPENCODE_EXPERIMENTAL` | boolescher Wert | Experimentelle Funktionen unter dem Sammelflag aktivieren |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolescher Wert | Symbolerkennung 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_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_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_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_FILEWATCHER` | boolescher Wert | Dateiüberwachung für das gesamte Verzeichnis aktivieren |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolescher Wert | Oxfmt-Formatierer aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolescher Wert | Experimentelles LSP-Tool aktivieren |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolescher Wert | Dateiüberwachung deaktivieren |
| `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren | | `OPENCODE_EXPERIMENTAL_EXA` | boolescher Wert | Experimentelle Exa-Funktionen aktivieren |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolescher Wert | TY LSP für Python-Dateien aktivieren |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolescher Wert | Planmodus 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 |
+7 -1
View File
@@ -596,7 +596,7 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
| Variable | Type | Description | | Variable | Type | Description |
| ----------------------------------------------- | -------- | ---------------------------------------------------------- | | ----------------------------------------------- | -------- | ---------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | booleano | Habilitar todas las funciones experimentales | | `OPENCODE_EXPERIMENTAL` | booleano | Habilitar funciones experimentales de la bandera global |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booleano | Habilitar descubrimiento de íconos | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booleano | Habilitar descubrimiento de íconos |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | booleano | Deshabilitar copia al seleccionar en TUI | | `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 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | número | Tiempo de espera predeterminado para comandos bash en ms |
@@ -608,3 +608,9 @@ Estas variables de entorno habilitan funciones experimentales que pueden cambiar
| `OPENCODE_EXPERIMENTAL_EXA` | booleano | Habilitar funciones experimentales de Exa | | `OPENCODE_EXPERIMENTAL_EXA` | booleano | Habilitar funciones experimentales de Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | booleano | Habilitar Habilitar TY LSP para archivos python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | booleano | Habilitar Habilitar TY LSP para archivos python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booleano | Habilitar modo de plan | | `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 |
+7 -1
View File
@@ -597,7 +597,7 @@ Ces variables d'environnement activent des fonctionnalités expérimentales qui
| Variables | Type | Description | | Variables | Type | Description |
| ----------------------------------------------- | ------- | --------------------------------------------------------------- | | ----------------------------------------------- | ------- | --------------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | booléen | Activer toutes les fonctionnalités expérimentales | | `OPENCODE_EXPERIMENTAL` | booléen | Activer les fonctionnalités expérimentales du drapeau global |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | booléen | Activer la découverte d'icônes | | `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_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 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | numéro | Délai d'expiration par défaut pour les commandes bash en ms |
@@ -609,3 +609,9 @@ 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_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_LSP_TY` | booléen | Activer TY LSP pour les fichiers python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | booléen | Activer le mode plan | | `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 |
+20 -14
View File
@@ -595,17 +595,23 @@ OpenCode può essere configurato tramite variabili d'ambiente.
Queste variabili d'ambiente abilitano funzionalità sperimentali che potrebbero cambiare o essere rimosse. Queste variabili d'ambiente abilitano funzionalità sperimentali che potrebbero cambiare o essere rimosse.
| Variabile | Tipo | Descrizione | | Variabile | Tipo | Descrizione |
| ----------------------------------------------- | ------- | ------------------------------------------ | | ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Abilita tutte le funzionalità sperimentali | | `OPENCODE_EXPERIMENTAL` | boolean | Abilita funzionalità sperimentali sotto il flag globale |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Abilita icon discovery |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | Disabilita copy on select nella TUI | | `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_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_OUTPUT_TOKEN_MAX` | number | Massimo token di output per risposte LLM |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | Abilita file watcher per l'intera dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Abilita formatter oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Abilita strumento LSP sperimentale |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Disabilita file watcher |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Abilita funzionalità Exa sperimentali |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Abilita TY LSP per i file python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Abilita plan mode | | `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 |
+7 -1
View File
@@ -596,7 +596,7 @@ OpenCode は環境変数を使用して構成できます。
| 変数 | タイプ | 説明 | | 変数 | タイプ | 説明 |
| ----------------------------------------------- | -------- | ------------------------------------------------ | | ----------------------------------------------- | -------- | ------------------------------------------------ |
| `OPENCODE_EXPERIMENTAL` | ブール値 | すべての実験的機能を有効にする | | `OPENCODE_EXPERIMENTAL` | ブール値 | アンブレラフラグ配下の実験的機能を有効にする |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | ブール値 | アイコン検出を有効にする | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | ブール値 | アイコン検出を有効にする |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | ブール値 | TUI で選択時のコピーを無効にする | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | ブール値 | TUI で選択時のコピーを無効にする |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | 数値 | bash コマンドのデフォルトのタイムアウト (ミリ秒) | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | 数値 | bash コマンドのデフォルトのタイムアウト (ミリ秒) |
@@ -608,3 +608,9 @@ OpenCode は環境変数を使用して構成できます。
| `OPENCODE_EXPERIMENTAL_EXA` | ブール値 | 実験的な Exa 機能を有効にする | | `OPENCODE_EXPERIMENTAL_EXA` | ブール値 | 実験的な Exa 機能を有効にする |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | ブール値 | python ファイルの TY LSP を有効にする | | `OPENCODE_EXPERIMENTAL_LSP_TY` | ブール値 | python ファイルの TY LSP を有効にする |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | ブール値 | プランモードを有効にする | | `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` | ブール値 | ワークスペースサポートを有効にする |
+20 -14
View File
@@ -594,17 +594,23 @@ OpenCode는 환경 변수로도 구성할 수 있습니다.
아래 환경 변수는 변경되거나 제거될 수 있는 실험 기능을 활성화합니다. 아래 환경 변수는 변경되거나 제거될 수 있는 실험 기능을 활성화합니다.
| 변수 | 타입 | 설명 | | 변수 | 타입 | 설명 |
| ----------------------------------------------- | ------- | -------------------------------- | | ----------------------------------------------- | ------- | ------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 모든 실험 기능 활성화 | | `OPENCODE_EXPERIMENTAL` | boolean | 엄브렐라 플래그로 제어되는 실험 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 아이콘 탐색 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | TUI에서 선택 시 복사 비활성화 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 명령 기본 타임아웃(ms) |
| `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 | | `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` | number | LLM 응답 최대 출력 토큰 수 |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolean | 전체 디렉터리 파일 감시 활성화 |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | oxfmt 포매터 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | 실험적 LSP 도구 활성화 |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | 파일 감시 비활성화 |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | 실험적 Exa 기능 활성화 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python 파일에 대해 TY LSP 활성화 |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Plan mode 활성화 | | `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 지원 활성화 |
+20 -14
View File
@@ -595,17 +595,23 @@ OpenCode kan konfigureres ved hjelp av miljøvariabler.
Disse miljøvariablene muliggjør eksperimentelle funksjoner som kan endres eller fjernes. Disse miljøvariablene muliggjør eksperimentelle funksjoner som kan endres eller fjernes.
| Variabel | Type | Beskrivelse | | Variabel | Type | Beskrivelse |
| ----------------------------------------------- | ------ | --------------------------------------------- | | ----------------------------------------------- | ------ | ----------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver alle eksperimentelle funksjoner | | `OPENCODE_EXPERIMENTAL` | boolsk | Aktiver eksperimentelle funksjoner under samleflagget |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolsk | Aktiver ikonoppdagelse |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolsk | Deaktiver kopi ved valg i TUI | | `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_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_OUTPUT_TOKEN_MAX` | tall | Maks Output Tokens for LLM-svar |
| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir | | `OPENCODE_EXPERIMENTAL_FILEWATCHER` | boolsk | Aktiver filovervåker for hele dir |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolsk | Aktiver oxfmt formatter |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolsk | Aktiver eksperimentelt LSP-verktøy |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolsk | Deaktiver filovervåking |
| `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner | | `OPENCODE_EXPERIMENTAL_EXA` | boolsk | Aktiver eksperimentelle Exa-funksjoner |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolsk | Aktiver TY LSP for python-filer |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolsk | Aktiver planmodus | | `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 |
+20 -14
View File
@@ -595,17 +595,23 @@ OpenCode można skonfigurować za pomocą zmiennych środowiskowych.
Te zmienne włączają funkcje eksperymentalne, które mogą ulec zmianie lub zostać usunięte. Te zmienne włączają funkcje eksperymentalne, które mogą ulec zmianie lub zostać usunięte.
| Zmienna | Typ | Opis | | Zmienna | Typ | Opis |
| ----------------------------------------------- | ------- | -------------------------------------------- | | ----------------------------------------------- | ------- | --------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Włącz wszystkie funkcje eksperymentalne | | `OPENCODE_EXPERIMENTAL` | boolean | Włącz funkcje eksperymentalne objęte flagą zbiorczą |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Włącz wykrywanie ikon | | `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_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_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_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_FILEWATCHER` | boolean | Włącz obserwatora plików dla całego katalogu |
| `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt | | `OPENCODE_EXPERIMENTAL_OXFMT` | boolean | Włącz formater oxfmt |
| `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP | | `OPENCODE_EXPERIMENTAL_LSP_TOOL` | boolean | Włącz eksperymentalne narzędzie LSP |
| `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików | | `OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER` | boolean | Wyłącz obserwatora plików |
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Włącz funkcje eksperymentalne Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Włącz TY LSP dla plików python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Włącz tryb planowania | | `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 |
+7 -1
View File
@@ -596,7 +596,7 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
| Variável | Tipo | Descrição | | Variável | Tipo | Descrição |
| ----------------------------------------------- | ------- | --------------------------------------------------------- | | ----------------------------------------------- | ------- | --------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Habilitar todos os recursos experimentais | | `OPENCODE_EXPERIMENTAL` | boolean | Habilitar recursos experimentais sob o sinalizador global |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Habilitar descoberta de ícones | | `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_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 | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | Tempo limite padrão para comandos bash em ms |
@@ -608,3 +608,9 @@ Essas variáveis de ambiente habilitam recursos experimentais que podem mudar ou
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | Habilitar recursos experimentais do Exa | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Habilitar recursos experimentais do Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Habilitar TY LSP para arquivos python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Habilitar TY LSP para arquivos python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Habilitar modo de plano | | `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 |
+7 -1
View File
@@ -597,7 +597,7 @@ opencode можно настроить с помощью переменных с
| Переменная | Тип | Описание | | Переменная | Тип | Описание |
| ----------------------------------------------- | ------------------- | ------------------------------------------------------ | | ----------------------------------------------- | ------------------- | ------------------------------------------------------ |
| `OPENCODE_EXPERIMENTAL` | логическое значение | Включить все экспериментальные функции | | `OPENCODE_EXPERIMENTAL` | логическое значение | Включить экспериментальные функции под общим флагом |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | логическое значение | Включить обнаружение значков | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | логическое значение | Включить обнаружение значков |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | логическое значение | Отключить копирование при выборе в TUI | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | логическое значение | Отключить копирование при выборе в TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | число | Таймаут по умолчанию для команд bash в мс | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | число | Таймаут по умолчанию для команд bash в мс |
@@ -609,3 +609,9 @@ opencode можно настроить с помощью переменных с
| `OPENCODE_EXPERIMENTAL_EXA` | логическое значение | Включить экспериментальные функции Exa | | `OPENCODE_EXPERIMENTAL_EXA` | логическое значение | Включить экспериментальные функции Exa |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | логическое значение | Включить TY LSP для файлов python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | логическое значение | Включить TY LSP для файлов python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | логическое значение | Включить режим плана | | `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` | логическое значение | Включить поддержку рабочих областей |
+7 -1
View File
@@ -598,7 +598,7 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
| ตัวแปร | ชนิด | คำอธิบาย | | ตัวแปร | ชนิด | คำอธิบาย |
| ----------------------------------------------- | ------- | ---------------------------------------------- | | ----------------------------------------------- | ------- | ---------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองทั้งหมด | | `OPENCODE_EXPERIMENTAL` | Boolean | เปิดใช้งานฟีเจอร์ทดลองภายใต้แฟล็กรวม |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | Boolean | การค้นหาไอคอนทดลอง | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | Boolean | การค้นหาไอคอนทดลอง |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | Boolean | ปิดใช้งานการคัดลอกเมื่อเลือกใน TUI | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | Boolean | ปิดใช้งานการคัดลอกเมื่อเลือกใน TUI |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Number | การหมดเวลาเริ่มต้นสำหรับคำสั่ง bash ในหน่วย ms | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | Number | การหมดเวลาเริ่มต้นสำหรับคำสั่ง bash ในหน่วย ms |
@@ -610,3 +610,9 @@ OpenCode สามารถกำหนดค่าโดยใช้ตัว
| `OPENCODE_EXPERIMENTAL_EXA` | Boolean | ฟีเจอร์ Exa ทดลอง | | `OPENCODE_EXPERIMENTAL_EXA` | Boolean | ฟีเจอร์ Exa ทดลอง |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | Boolean | เปิดใช้งาน TY LSP สำหรับไฟล์ python | | `OPENCODE_EXPERIMENTAL_LSP_TY` | Boolean | เปิดใช้งาน TY LSP สำหรับไฟล์ python |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | Boolean | เปิดใช้งาน Plan mode | | `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 |
+7 -1
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 | | Değişken | Tip | Tanım |
| ----------------------------------------------- | ------- | ------------------------------------------------------- | | ----------------------------------------------- | ------- | ------------------------------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | Tüm deneysel özellikleri etkinleştir | | `OPENCODE_EXPERIMENTAL` | boolean | Şemsiye bayrağa bağlı deneysel özellikleri etkinleştir |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | Simge bulmayı 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_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ı | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | MS cinsinden bash komutları için varsayılan zaman aşımı |
@@ -609,3 +609,9 @@ 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_EXA` | boolean | Deneysel Exa özelliklerini etkinleştirin |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | python dosyaları için TY LSP'yi etkinleştir | | `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_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 |
+7 -1
View File
@@ -597,7 +597,7 @@ OpenCode 可以通过环境变量进行配置。
| 变量 | 类型 | 描述 | | 变量 | 类型 | 描述 |
| ----------------------------------------------- | ------- | ------------------------------- | | ----------------------------------------------- | ------- | ------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 启用所有实验性功能 | | `OPENCODE_EXPERIMENTAL` | boolean | 启用受总开关控制的实验性功能 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 启用图标发现 | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 启用图标发现 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 禁用 TUI 中的选中即复制 | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 禁用 TUI 中的选中即复制 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 命令的默认超时时间(毫秒) | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 命令的默认超时时间(毫秒) |
@@ -609,3 +609,9 @@ OpenCode 可以通过环境变量进行配置。
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 启用实验性 Exa 功能 | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | 启用实验性 Exa 功能 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 为 python 文件启用 TY LSP | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 为 python 文件启用 TY LSP |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 启用计划模式 | | `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 | 启用工作区支持 |
+7 -1
View File
@@ -597,7 +597,7 @@ OpenCode 可以透過環境變數進行設定。
| 變數 | 類型 | 說明 | | 變數 | 類型 | 說明 |
| ----------------------------------------------- | ------- | ------------------------------- | | ----------------------------------------------- | ------- | ------------------------------- |
| `OPENCODE_EXPERIMENTAL` | boolean | 啟用所有實驗性功能 | | `OPENCODE_EXPERIMENTAL` | boolean | 啟用受總開關控制的實驗性功能 |
| `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 啟用圖示探索 | | `OPENCODE_EXPERIMENTAL_ICON_DISCOVERY` | boolean | 啟用圖示探索 |
| `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 停用 TUI 中的選取即複製 | | `OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT` | boolean | 停用 TUI 中的選取即複製 |
| `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 指令的預設逾時時間(毫秒) | | `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS` | number | bash 指令的預設逾時時間(毫秒) |
@@ -609,3 +609,9 @@ OpenCode 可以透過環境變數進行設定。
| `OPENCODE_EXPERIMENTAL_EXA` | boolean | 啟用實驗性 Exa 功能 | | `OPENCODE_EXPERIMENTAL_EXA` | boolean | 啟用實驗性 Exa 功能 |
| `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 為 python 檔案啟用 TY LSP | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | 為 python 檔案啟用 TY LSP |
| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | 啟用計畫模式 | | `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 | 啟用工作區支援 |