merge origin/dev into issue-13770-tool-output-docs
Resolve conflict in packages/opencode/src/tool/shell/prompt.ts to combine truncationGuidance helper (which omits the truncation note when truncation is disabled) with the configurable defaultTimeoutMs from origin/dev. Fix typecheck in tool/truncate.ts by narrowing the tool_output union before reading max_lines and max_bytes.
This commit is contained in:
commit
3fcdf94873
126 changed files with 2334 additions and 1254 deletions
104
bun.lock
104
bun.lock
|
|
@ -29,7 +29,7 @@
|
||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
|
|
@ -147,7 +147,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "3.0.64",
|
"@ai-sdk/anthropic": "3.0.64",
|
||||||
"@ai-sdk/openai": "3.0.48",
|
"@ai-sdk/openai": "3.0.48",
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
@ -193,21 +193,21 @@
|
||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|
@ -239,7 +239,7 @@
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"venice-ai-sdk-provider": "2.0.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -254,7 +254,7 @@
|
||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
|
|
@ -309,7 +309,7 @@
|
||||||
},
|
},
|
||||||
"packages/effect-drizzle-sqlite": {
|
"packages/effect-drizzle-sqlite": {
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -323,7 +323,7 @@
|
||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
|
|
@ -353,7 +353,7 @@
|
||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
|
|
@ -369,7 +369,7 @@
|
||||||
},
|
},
|
||||||
"packages/http-recorder": {
|
"packages/http-recorder": {
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -382,7 +382,7 @@
|
||||||
},
|
},
|
||||||
"packages/llm": {
|
"packages/llm": {
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/eventstream-codec": "4.2.14",
|
"@smithy/eventstream-codec": "4.2.14",
|
||||||
"@smithy/util-utf8": "4.2.2",
|
"@smithy/util-utf8": "4.2.2",
|
||||||
|
|
@ -400,7 +400,7 @@
|
||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -494,7 +494,7 @@
|
||||||
"tree-sitter-powershell": "0.25.10",
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"venice-ai-sdk-provider": "2.0.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
"web-tree-sitter": "0.25.10",
|
"web-tree-sitter": "0.25.10",
|
||||||
"which": "6.0.1",
|
"which": "6.0.1",
|
||||||
|
|
@ -538,7 +538,7 @@
|
||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
|
|
@ -576,7 +576,7 @@
|
||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "catalog:",
|
"cross-spawn": "catalog:",
|
||||||
},
|
},
|
||||||
|
|
@ -591,7 +591,7 @@
|
||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
|
|
@ -626,7 +626,7 @@
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
|
@ -675,7 +675,7 @@
|
||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
|
|
@ -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=="],
|
||||||
|
|
||||||
|
|
@ -4912,7 +4912,7 @@
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="],
|
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
|
||||||
|
|
||||||
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
|
||||||
|
|
||||||
|
|
@ -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=="],
|
||||||
|
|
||||||
|
|
@ -5956,7 +5966,11 @@
|
||||||
|
|
||||||
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||||
|
|
||||||
"venice-ai-sdk-provider/@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=="],
|
"venice-ai-sdk-provider/@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=="],
|
||||||
|
|
||||||
|
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||||
|
|
||||||
|
"venice-ai-sdk-provider/@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=="],
|
||||||
|
|
||||||
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
|
||||||
|
|
||||||
|
|
@ -6002,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=="],
|
||||||
|
|
@ -6012,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=="],
|
||||||
|
|
@ -6658,6 +6686,10 @@
|
||||||
|
|
||||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||||
|
|
||||||
|
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||||
|
|
||||||
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
stdenv,
|
stdenv,
|
||||||
bun,
|
bun,
|
||||||
nodejs,
|
nodejs,
|
||||||
|
darwin,
|
||||||
electron_41,
|
electron_41,
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
writableTmpDirAsHomeHook,
|
writableTmpDirAsHomeHook,
|
||||||
|
|
@ -14,7 +15,12 @@ let
|
||||||
in
|
in
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
pname = "opencode-desktop";
|
pname = "opencode-desktop";
|
||||||
inherit (opencode) version src node_modules;
|
inherit (opencode)
|
||||||
|
version
|
||||||
|
src
|
||||||
|
node_modules
|
||||||
|
patches
|
||||||
|
;
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
bun
|
bun
|
||||||
|
|
@ -23,6 +29,9 @@ stdenv.mkDerivation (finalAttrs: {
|
||||||
writableTmpDirAsHomeHook
|
writableTmpDirAsHomeHook
|
||||||
] ++ lib.optionals stdenv.hostPlatform.isLinux [
|
] ++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
autoPatchelfHook
|
autoPatchelfHook
|
||||||
|
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
|
||||||
|
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
|
||||||
|
darwin.autoSignDarwinBinariesHook
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"nodeModules": {
|
"nodeModules": {
|
||||||
"x86_64-linux": "sha256-pbVW7cOLT76Q7f++xaYYrwuN7eS6FRen80xoaVog3M4=",
|
"x86_64-linux": "sha256-aHOQ9TMdmfbSIuKee1uflHiuAQ3+69NH7RUMv6LztVY=",
|
||||||
"aarch64-linux": "sha256-nk/q4PezhQSf/UHhJuL/188q6e+Gr+PFgrlhEEG5ouo=",
|
"aarch64-linux": "sha256-DBBT/nLkLOtHlKL3ea/Q7ysTG5asnWUx0zQ90+D6cuI=",
|
||||||
"aarch64-darwin": "sha256-IfIJwgvsonrfjG+btK/5YedzX+MJtf/5bYuLrAmnHvU=",
|
"aarch64-darwin": "sha256-VIiz1RRTu/qdWd63tXv5kxVTFYjqZbnbaKaR7V++7lI=",
|
||||||
"x86_64-darwin": "sha256-Cwd9gEymjFn6XAgkkyX4ZmRTAE1JiurxF4VvVws9C/E="
|
"x86_64-darwin": "sha256-TUtJaedvRpZcM7Fs5/P6fkI/aT+rIo3Wsp9kZiN45Rg="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
78
packages/app/src/context/global-sync/bootstrap.test.ts
Normal file
78
packages/app/src/context/global-sync/bootstrap.test.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { QueryClient } from "@tanstack/solid-query"
|
||||||
|
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
|
import { bootstrapDirectory } from "./bootstrap"
|
||||||
|
import type { State, VcsCache } from "./types"
|
||||||
|
|
||||||
|
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||||
|
|
||||||
|
describe("bootstrapDirectory", () => {
|
||||||
|
test("marks a loading directory partial during bootstrap and complete after success", async () => {
|
||||||
|
const [store, setStore] = createStore<State>({
|
||||||
|
status: "loading",
|
||||||
|
agent: [],
|
||||||
|
command: [],
|
||||||
|
project: "",
|
||||||
|
projectMeta: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
provider_ready: true,
|
||||||
|
provider,
|
||||||
|
config: {},
|
||||||
|
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
||||||
|
session: [],
|
||||||
|
sessionTotal: 0,
|
||||||
|
session_status: {},
|
||||||
|
session_working(id: string) {
|
||||||
|
return this.session_status[id]?.type !== "idle"
|
||||||
|
},
|
||||||
|
session_diff: {},
|
||||||
|
todo: {},
|
||||||
|
permission: {},
|
||||||
|
question: {},
|
||||||
|
mcp_ready: true,
|
||||||
|
mcp: {},
|
||||||
|
lsp_ready: true,
|
||||||
|
lsp: [],
|
||||||
|
vcs: undefined,
|
||||||
|
limit: 5,
|
||||||
|
message: {},
|
||||||
|
part: {},
|
||||||
|
part_text_accum_delta: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
await bootstrapDirectory({
|
||||||
|
directory: "/project",
|
||||||
|
global: {
|
||||||
|
config: {} satisfies Config,
|
||||||
|
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
|
||||||
|
project: [{ id: "project", worktree: "/project" } as Project],
|
||||||
|
provider,
|
||||||
|
},
|
||||||
|
sdk: {
|
||||||
|
app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
|
||||||
|
config: { get: async () => ({ data: {} }) },
|
||||||
|
session: { status: async () => ({ data: {} }) },
|
||||||
|
vcs: { get: async () => ({ data: undefined }) },
|
||||||
|
command: { list: async () => ({ data: [] }) },
|
||||||
|
permission: { list: async () => ({ data: [] }) },
|
||||||
|
question: { list: async () => ({ data: [] }) },
|
||||||
|
mcp: { status: async () => ({ data: {} }) },
|
||||||
|
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
|
||||||
|
} as unknown as OpencodeClient,
|
||||||
|
store,
|
||||||
|
setStore,
|
||||||
|
vcsCache: { setStore() {} } as unknown as VcsCache,
|
||||||
|
loadSessions() {},
|
||||||
|
translate: (key) => key,
|
||||||
|
queryClient: new QueryClient(),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(store.status).toBe("partial")
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 80))
|
||||||
|
|
||||||
|
expect(store.status).toBe("complete")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -220,6 +220,7 @@ export async function bootstrapDirectory(input: {
|
||||||
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) {
|
||||||
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
input.setStore("config", reconcile(input.global.config, { merge: false }))
|
||||||
}
|
}
|
||||||
|
if (loading) input.setStore("status", "partial")
|
||||||
|
|
||||||
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
const rev = (providerRev.get(input.directory) ?? 0) + 1
|
||||||
providerRev.set(input.directory, rev)
|
providerRev.set(input.directory, rev)
|
||||||
|
|
@ -326,5 +327,7 @@ export async function bootstrapDirectory(input: {
|
||||||
description: formatServerError(slowErrs[0], input.translate),
|
description: formatServerError(slowErrs[0], input.translate),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,63 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { beforeAll, describe, expect, mock, test } from "bun:test"
|
||||||
import { createRoot, getOwner } from "solid-js"
|
import { createRoot, getOwner, type Owner } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
|
||||||
import type { State } from "./types"
|
import type { State } from "./types"
|
||||||
import { createChildStoreManager } from "./child-store"
|
import type { QueryOptionsApi } from "../global-sync"
|
||||||
|
|
||||||
|
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
|
||||||
|
|
||||||
const child = () => createStore({} as State)
|
const child = () => createStore({} as State)
|
||||||
|
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
|
||||||
|
|
||||||
|
const queryOptionsApi = {
|
||||||
|
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
|
||||||
|
projects: () => ({ queryKey: ["projects"], queryFn: async () => [] }),
|
||||||
|
providers: (directory: string | null) => ({ queryKey: [directory, "providers"], queryFn: async () => provider }),
|
||||||
|
path: (directory: string | null) => ({
|
||||||
|
queryKey: [directory, "path"],
|
||||||
|
queryFn: async () => ({
|
||||||
|
state: "",
|
||||||
|
config: "",
|
||||||
|
worktree: "",
|
||||||
|
directory: directory ?? "",
|
||||||
|
home: "",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
|
||||||
|
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
|
||||||
|
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
|
||||||
|
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
|
||||||
|
} as unknown as QueryOptionsApi
|
||||||
|
|
||||||
|
function createOwner(callback: (owner: Owner) => void) {
|
||||||
|
return createRoot((dispose) => {
|
||||||
|
const owner = getOwner()
|
||||||
|
if (!owner) throw new Error("owner required")
|
||||||
|
callback(owner)
|
||||||
|
|
||||||
|
return dispose
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mock.module("@/utils/persist", () => ({
|
||||||
|
Persist: {
|
||||||
|
workspace: (...parts: string[]) => parts.join(":"),
|
||||||
|
},
|
||||||
|
persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true],
|
||||||
|
}))
|
||||||
|
mock.module("@tanstack/solid-query", () => ({
|
||||||
|
useQueries: () => [
|
||||||
|
{ isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } },
|
||||||
|
{ isLoading: false, data: {} },
|
||||||
|
{ isLoading: false, data: [] },
|
||||||
|
{ isLoading: false, data: provider },
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
createChildStoreManager = (await import("./child-store")).createChildStoreManager
|
||||||
|
})
|
||||||
|
|
||||||
describe("createChildStoreManager", () => {
|
describe("createChildStoreManager", () => {
|
||||||
test("does not evict the active directory during mark", () => {
|
test("does not evict the active directory during mark", () => {
|
||||||
|
|
@ -22,8 +75,8 @@ describe("createChildStoreManager", () => {
|
||||||
onBootstrap() {},
|
onBootstrap() {},
|
||||||
onDispose() {},
|
onDispose() {},
|
||||||
translate: (key) => key,
|
translate: (key) => key,
|
||||||
queryOptions: {} as any,
|
queryOptions: queryOptionsApi,
|
||||||
global: { provider: null! },
|
global: { provider },
|
||||||
})
|
})
|
||||||
|
|
||||||
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
|
||||||
|
|
@ -37,4 +90,35 @@ describe("createChildStoreManager", () => {
|
||||||
|
|
||||||
expect(manager.children[directory]).toBeDefined()
|
expect(manager.children[directory]).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("starts new child stores as loading and bootstraps them on first access", () => {
|
||||||
|
const bootstraps: string[] = []
|
||||||
|
let manager: ReturnType<typeof createChildStoreManager> | undefined
|
||||||
|
|
||||||
|
const dispose = createOwner((owner) => {
|
||||||
|
manager = createChildStoreManager({
|
||||||
|
owner,
|
||||||
|
isBooting: () => false,
|
||||||
|
isLoadingSessions: () => false,
|
||||||
|
onBootstrap(directory) {
|
||||||
|
bootstraps.push(directory)
|
||||||
|
},
|
||||||
|
onDispose() {},
|
||||||
|
translate: (key) => key,
|
||||||
|
queryOptions: queryOptionsApi,
|
||||||
|
global: { provider },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!manager) throw new Error("manager required")
|
||||||
|
|
||||||
|
const [store] = manager.child("/project")
|
||||||
|
|
||||||
|
expect(store.status).toBe("loading")
|
||||||
|
expect(bootstraps).toEqual(["/project"])
|
||||||
|
} finally {
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export function createChildStoreManager(input: {
|
||||||
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
return { state: "", config: "", worktree: "", directory: "", home: "" }
|
||||||
return pathQuery.data
|
return pathQuery.data
|
||||||
},
|
},
|
||||||
status: "complete" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
session: [],
|
session: [],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Session } from "@opencode-ai/sdk/v2/client"
|
import type { Session } from "@opencode-ai/sdk/v2/client"
|
||||||
import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js"
|
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useQuery } from "@tanstack/solid-query"
|
import { useQuery } from "@tanstack/solid-query"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
|
@ -18,7 +18,6 @@ import { DateTime } from "luxon"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||||
import { DialogSelectServer } from "@/components/dialog-select-server"
|
import { DialogSelectServer } from "@/components/dialog-select-server"
|
||||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
|
||||||
import { useServer } from "@/context/server"
|
import { useServer } from "@/context/server"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
@ -467,11 +466,6 @@ function LegacyHome() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
|
||||||
const [promptText, setPromptText] = createSignal("")
|
|
||||||
const [selectedAgent, setSelectedAgent] = createSignal("frontend-specialist")
|
|
||||||
const [showProjectsDropdown, setShowProjectsDropdown] = createSignal(false)
|
|
||||||
|
|
||||||
const homedir = createMemo(() => sync.data.path.home)
|
const homedir = createMemo(() => sync.data.path.home)
|
||||||
const recent = createMemo(() => {
|
const recent = createMemo(() => {
|
||||||
return sync.data.project
|
return sync.data.project
|
||||||
|
|
@ -480,8 +474,6 @@ function LegacyHome() {
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentProject = createMemo(() => recent()[0]?.worktree)
|
|
||||||
|
|
||||||
const serverDotClass = createMemo(() => {
|
const serverDotClass = createMemo(() => {
|
||||||
const healthy = server.healthy()
|
const healthy = server.healthy()
|
||||||
if (healthy === true) return "bg-icon-success-base"
|
if (healthy === true) return "bg-icon-success-base"
|
||||||
|
|
@ -520,185 +512,69 @@ function LegacyHome() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModelSelect() {
|
|
||||||
dialog.show(() => <DialogSelectModel />)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAgent() {
|
|
||||||
const agents = ["frontend-specialist", "build", "general"]
|
|
||||||
const nextIndex = (agents.indexOf(selectedAgent()) + 1) % agents.length
|
|
||||||
setSelectedAgent(agents[nextIndex])
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
const projectToOpen = currentProject()
|
|
||||||
if (projectToOpen) {
|
|
||||||
openProject(projectToOpen)
|
|
||||||
} else {
|
|
||||||
chooseProject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeModelName = createMemo(() => {
|
|
||||||
const model = sync.data.config.model
|
|
||||||
if (!model) return "GPT-5.7 Pro"
|
|
||||||
const parts = model.split("/")
|
|
||||||
return parts[parts.length - 1]
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="mx-auto mt-24 w-full max-w-2xl px-6 flex flex-col items-center">
|
<div class="mx-auto mt-55 w-full md:w-auto px-4">
|
||||||
<div class="flex flex-col items-center gap-3 mb-10">
|
<Logo class="md:w-xl opacity-12" />
|
||||||
<div onClick={chooseProject} class="cursor-pointer hover:opacity-25 transition-opacity duration-200">
|
<Button
|
||||||
<Logo class="w-48 opacity-15" />
|
size="large"
|
||||||
</div>
|
variant="ghost"
|
||||||
<Button
|
class="mt-4 mx-auto text-14-regular text-text-weak"
|
||||||
size="normal"
|
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||||
variant="ghost"
|
>
|
||||||
class="text-12-regular text-text-weak px-3"
|
<div
|
||||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
classList={{
|
||||||
>
|
"size-2 rounded-full": true,
|
||||||
<div
|
[serverDotClass()]: true,
|
||||||
classList={{
|
}}
|
||||||
"size-1.5 rounded-full mr-2": true,
|
/>
|
||||||
[serverDotClass()]: true,
|
{server.name}
|
||||||
}}
|
</Button>
|
||||||
/>
|
|
||||||
{server.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={recent().length > 0}>
|
<Match when={sync.data.project.length > 0}>
|
||||||
<div class="w-full flex flex-col items-center gap-6">
|
<div class="mt-20 w-full flex flex-col gap-4">
|
||||||
<div class="text-20-medium text-text-strong text-center">{language.t("session.new.title")}</div>
|
<div class="flex gap-2 items-center justify-between pl-3">
|
||||||
|
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
|
||||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md relative">
|
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||||
<textarea
|
{language.t("command.project.open")}
|
||||||
class="bg-transparent border-none outline-none text-14-regular text-text-base placeholder-text-weak w-full resize-none h-20 focus:outline-none"
|
</Button>
|
||||||
placeholder="Ask anything, / for commands, @ for context..."
|
|
||||||
value={promptText()}
|
|
||||||
onInput={(e) => setPromptText(e.currentTarget.value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSubmit()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={toggleAgent}
|
|
||||||
>
|
|
||||||
<Icon name="sliders" size="small" class="shrink-0" />
|
|
||||||
<span>Agent: {selectedAgent()}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={handleModelSelect}
|
|
||||||
>
|
|
||||||
<Icon name="brain" size="small" class="shrink-0" />
|
|
||||||
<span>Model: {activeModelName()}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div class="relative">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={() => setShowProjectsDropdown(!showProjectsDropdown())}
|
|
||||||
>
|
|
||||||
<Icon name="folder" size="small" class="shrink-0" />
|
|
||||||
<span>Project: {currentProject() ? getFilename(currentProject()) : "Select Project"}</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Show when={showProjectsDropdown()}>
|
|
||||||
<div class="absolute left-0 mt-1 w-64 bg-surface-raised-base border border-border-base rounded-lg p-2 shadow-lg z-50 flex flex-col gap-1">
|
|
||||||
<div class="text-10-semibold text-text-weak px-2 py-1 uppercase tracking-wider">
|
|
||||||
{language.t("home.recentProjects")}
|
|
||||||
</div>
|
|
||||||
<For each={recent()}>
|
|
||||||
{(project) => (
|
|
||||||
<button
|
|
||||||
class="text-12-mono text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center justify-between w-full"
|
|
||||||
onClick={() => {
|
|
||||||
openProject(project.worktree)
|
|
||||||
setShowProjectsDropdown(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="truncate">{getFilename(project.worktree)}</span>
|
|
||||||
<span class="text-10-regular text-text-weak shrink-0 pl-2">
|
|
||||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="border-t border-border-weak-base my-1" />
|
|
||||||
<button
|
|
||||||
class="text-12-medium text-text-strong text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center gap-2 w-full"
|
|
||||||
onClick={() => {
|
|
||||||
setShowProjectsDropdown(false)
|
|
||||||
chooseProject()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon name="folder-add-left" size="small" />
|
|
||||||
{language.t("command.project.open")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base border border-border-weak-base rounded-md cursor-default pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icon name="branch" size="small" class="shrink-0" />
|
|
||||||
<span>Branch: dev</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<For each={recent()}>
|
||||||
|
{(project) => (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-14-mono text-left justify-between px-3"
|
||||||
|
onClick={() => openProject(project.worktree)}
|
||||||
|
>
|
||||||
|
{project.worktree.replace(homedir(), "~")}
|
||||||
|
<div class="text-14-regular text-text-weak">
|
||||||
|
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={!sync.ready}>
|
||||||
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
|
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
|
||||||
|
<Button class="px-3" onClick={chooseProject}>
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<div class="w-full flex flex-col items-center gap-6">
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
<div class="text-20-medium text-text-strong text-center">{language.t("home.empty.title")}</div>
|
<Icon name="folder-add-left" size="large" />
|
||||||
|
<div class="flex flex-col gap-1 items-center justify-center">
|
||||||
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md">
|
<div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
|
||||||
<div class="text-14-regular text-text-weak w-full min-h-[4rem] cursor-pointer" onClick={chooseProject}>
|
<div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
|
||||||
Ask anything, / for commands, @ for context...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={chooseProject}
|
|
||||||
>
|
|
||||||
<Icon name="folder" size="small" class="shrink-0" />
|
|
||||||
<span>Open project</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
|
|
||||||
onClick={handleModelSelect}
|
|
||||||
>
|
|
||||||
<Icon name="brain" size="small" class="shrink-0" />
|
|
||||||
<span>Model: {activeModelName()}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button class="px-3 mt-1" onClick={chooseProject}>
|
||||||
|
{language.t("command.project.open")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||||
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
|
||||||
|
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
|
||||||
import { Identifier } from "@/utils/id"
|
import { Identifier } from "@/utils/id"
|
||||||
import { diffs as list } from "@/utils/diffs"
|
import { diffs as list } from "@/utils/diffs"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
@ -263,7 +264,8 @@ export default function Page() {
|
||||||
|
|
||||||
const isDesktop = createMediaQuery("(min-width: 768px)")
|
const isDesktop = createMediaQuery("(min-width: 768px)")
|
||||||
const size = createSizing()
|
const size = createSizing()
|
||||||
const isV2NewSessionPage = () => import.meta.env.VITE_OPENCODE_CHANNEL === "prod" || !params.id
|
const isV2NewSessionPage = () =>
|
||||||
|
shouldUseV2NewSessionPage({ channel: import.meta.env.VITE_OPENCODE_CHANNEL, sessionID: params.id })
|
||||||
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
|
||||||
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
|
||||||
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
|
||||||
|
|
|
||||||
14
packages/app/src/pages/session/new-session-layout.test.ts
Normal file
14
packages/app/src/pages/session/new-session-layout.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { shouldUseV2NewSessionPage } from "./new-session-layout"
|
||||||
|
|
||||||
|
describe("shouldUseV2NewSessionPage", () => {
|
||||||
|
test("keeps prod session pages on the legacy layout", () => {
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "prod", sessionID: "ses_123" })).toBe(false)
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "prod" })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("uses the v2 layout only for non-prod new-session pages", () => {
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "dev" })).toBe(true)
|
||||||
|
expect(shouldUseV2NewSessionPage({ channel: "dev", sessionID: "ses_123" })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
3
packages/app/src/pages/session/new-session-layout.ts
Normal file
3
packages/app/src/pages/session/new-session-layout.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function shouldUseV2NewSessionPage(input: { channel?: "dev" | "beta" | "prod"; sessionID?: string }) {
|
||||||
|
return input.channel !== "prod" && !input.sessionID
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ export function createRateLimiter(modelId: string, rateLimit: number | undefined
|
||||||
const dict = i18n(localeFromRequest(request))
|
const dict = i18n(localeFromRequest(request))
|
||||||
|
|
||||||
const limits = Subscription.getFreeLimits()
|
const limits = Subscription.getFreeLimits()
|
||||||
const headersExist = Object.entries(limits.checkHeaders).every(
|
// temporarily disable check headers
|
||||||
([name, value]) => request.headers.get(name)?.toLowerCase().includes(value) ?? false,
|
//const headersExist = Object.entries(limits.checkHeaders).every(
|
||||||
)
|
// ([name, value]) => request.headers.get(name)?.toLowerCase().includes(value) ?? false,
|
||||||
|
//)
|
||||||
|
//const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
||||||
|
const headersExist = true
|
||||||
const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
const dailyLimit = !headersExist ? limits.dailyRequestsFallback : (rateLimit ?? limits.dailyRequests)
|
||||||
const isDefaultModel = headersExist && !rateLimit
|
const isDefaultModel = headersExist && !rateLimit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"name": "@opencode-ai/core",
|
"name": "@opencode-ai/core",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
"minimatch": "10.2.5",
|
"minimatch": "10.2.5",
|
||||||
"npm-package-arg": "13.0.2",
|
"npm-package-arg": "13.0.2",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"venice-ai-sdk-provider": "2.0.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"xdg-basedir": "5.1.0",
|
"xdg-basedir": "5.1.0",
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -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* () {
|
||||||
|
|
|
||||||
114
packages/core/src/git.ts
Normal file
114
packages/core/src/git.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
export * as Git from "./git"
|
||||||
|
|
||||||
|
import path from "path"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { ChildProcess } from "effect/unstable/process"
|
||||||
|
import { AbsolutePath } from "./schema"
|
||||||
|
import { AppFileSystem } from "./filesystem"
|
||||||
|
import { AppProcess } from "./process"
|
||||||
|
|
||||||
|
export interface Repo {
|
||||||
|
/**
|
||||||
|
* The root directory of the working tree that contains the input path.
|
||||||
|
*
|
||||||
|
* For `/home/me/app/src/file.ts` in a normal clone, this is `/home/me/app`.
|
||||||
|
* For `/home/me/app-feature/src/file.ts` in a linked worktree, this is
|
||||||
|
* `/home/me/app-feature`.
|
||||||
|
*/
|
||||||
|
readonly directory: AbsolutePath
|
||||||
|
/**
|
||||||
|
* The shared Git storage directory used by this repo and any linked worktrees.
|
||||||
|
*
|
||||||
|
* For a normal clone at `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||||
|
* For a linked worktree at `/home/me/app-feature` whose main checkout is
|
||||||
|
* `/home/me/app`, this is usually `/home/me/app/.git`.
|
||||||
|
*/
|
||||||
|
readonly store: AbsolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly find: (input: AbsolutePath) => Effect.Effect<Repo | undefined>
|
||||||
|
readonly remote: (repo: Repo, name?: string) => Effect.Effect<string | undefined>
|
||||||
|
readonly roots: (repo: Repo) => Effect.Effect<string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/GitV2") {}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const proc = yield* AppProcess.Service
|
||||||
|
|
||||||
|
const find = Effect.fn("Git.find")(function* (input: AbsolutePath) {
|
||||||
|
const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe(
|
||||||
|
Effect.map((matches) => matches[0]),
|
||||||
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
|
)
|
||||||
|
if (!dotgit) return undefined
|
||||||
|
|
||||||
|
const cwd = path.dirname(dotgit)
|
||||||
|
const git = run(cwd, proc)
|
||||||
|
const topLevel = yield* git(["rev-parse", "--show-toplevel"])
|
||||||
|
const commonDir = yield* git(["rev-parse", "--git-common-dir"])
|
||||||
|
if (commonDir.exitCode !== 0) return undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
directory: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd),
|
||||||
|
store: AbsolutePath.make(resolvePath(cwd, commonDir.text)),
|
||||||
|
} satisfies Repo
|
||||||
|
})
|
||||||
|
|
||||||
|
const remote = Effect.fn("Git.remote")(function* (repo: Repo, name = "origin") {
|
||||||
|
const result = yield* run(repo.directory, proc)(["remote", "get-url", name])
|
||||||
|
if (result.exitCode !== 0) return undefined
|
||||||
|
return result.text.trim() || undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const roots = Effect.fn("Git.roots")(function* (repo: Repo) {
|
||||||
|
const result = yield* run(repo.directory, proc)(["rev-list", "--max-parents=0", "HEAD"])
|
||||||
|
if (result.exitCode !== 0) return []
|
||||||
|
return result.text
|
||||||
|
.split("\n")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.toSorted()
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({ find, remote, roots })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(
|
||||||
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
|
Layer.provide(AppProcess.defaultLayer),
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
readonly exitCode: number
|
||||||
|
readonly text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(cwd: string, proc: AppProcess.Interface) {
|
||||||
|
return (args: string[]) =>
|
||||||
|
proc
|
||||||
|
.run(
|
||||||
|
ChildProcess.make("git", args, {
|
||||||
|
cwd,
|
||||||
|
extendEnv: true,
|
||||||
|
stdin: "ignore",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
Effect.map((result) => ({ exitCode: result.exitCode, text: result.stdout.toString("utf8") }) satisfies Result),
|
||||||
|
Effect.catch(() => Effect.succeed({ exitCode: 1, text: "" } satisfies Result)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePath(cwd: string, value: string) {
|
||||||
|
const trimmed = value.replace(/[\r\n]+$/, "")
|
||||||
|
if (!trimmed) return cwd
|
||||||
|
const normalized = AppFileSystem.windowsPath(trimmed)
|
||||||
|
if (path.isAbsolute(normalized)) return path.normalize(normalized)
|
||||||
|
return path.resolve(cwd, normalized)
|
||||||
|
}
|
||||||
129
packages/core/src/project.ts
Normal file
129
packages/core/src/project.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
export * as Project from "./project"
|
||||||
|
|
||||||
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
|
import path from "path"
|
||||||
|
import { AbsolutePath, withStatics } from "./schema"
|
||||||
|
import { AppFileSystem } from "./filesystem"
|
||||||
|
import { Git } from "./git"
|
||||||
|
import { Hash } from "./util/hash"
|
||||||
|
|
||||||
|
export const ID = Schema.String.pipe(
|
||||||
|
Schema.brand("Project.ID"),
|
||||||
|
withStatics((schema) => ({
|
||||||
|
global: schema.make("global"),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
|
export const Vcs = Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
type: Schema.Literal("git"),
|
||||||
|
store: AbsolutePath,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
export type Vcs = typeof Vcs.Type
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Project.Info")({
|
||||||
|
id: ID,
|
||||||
|
vcs: Schema.optional(Vcs),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
readonly resolve: (input: AbsolutePath) => Effect.Effect<
|
||||||
|
{
|
||||||
|
previous?: ID
|
||||||
|
id: ID
|
||||||
|
directory: AbsolutePath
|
||||||
|
vcs?: Vcs
|
||||||
|
},
|
||||||
|
never
|
||||||
|
>
|
||||||
|
/**
|
||||||
|
* Temporary bridge method for writing the resolved project ID to the repo-local cache.
|
||||||
|
*
|
||||||
|
* This exists while the old opencode project service and this core project
|
||||||
|
* service work together: core resolves the ID, while the old service still owns
|
||||||
|
* database migration and persistence. The old service should call this after it
|
||||||
|
* finishes migrating from `resolve().previous` to `resolve().id`; once project
|
||||||
|
* persistence moves into core, this separate bridge method can go away.
|
||||||
|
*/
|
||||||
|
readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectV2") {}
|
||||||
|
|
||||||
|
export const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const git = yield* Git.Service
|
||||||
|
|
||||||
|
const cached = Effect.fnUntraced(function* (dir: string) {
|
||||||
|
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
|
||||||
|
Effect.map((value) => value.trim()),
|
||||||
|
Effect.map((value) => (value ? ID.make(value) : undefined)),
|
||||||
|
Effect.catch(() => Effect.succeed(undefined)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const remote = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||||
|
const origin = yield* git.remote(repo)
|
||||||
|
if (!origin) return undefined
|
||||||
|
const normalized = url(origin)
|
||||||
|
if (!normalized) return undefined
|
||||||
|
return ID.make(Hash.fast(`git-remote:${normalized}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
function url(input: string) {
|
||||||
|
const value = input.trim()
|
||||||
|
if (!value) return undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value)
|
||||||
|
if (parsed.protocol === "file:") return undefined
|
||||||
|
return parts(parsed.hostname, parsed.pathname)
|
||||||
|
} catch {
|
||||||
|
const scp = value.match(/^([^@/:]+@)?([^/:]+):(.+)$/)
|
||||||
|
if (scp) return parts(scp[2], scp[3])
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parts(host: string, name: string) {
|
||||||
|
const pathname = name
|
||||||
|
.replace(/^\/+/, "")
|
||||||
|
.replace(/\.git\/?$/, "")
|
||||||
|
.replace(/\/+$/, "")
|
||||||
|
if (!host || !pathname) return undefined
|
||||||
|
return `${host.toLowerCase()}/${pathname}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = Effect.fnUntraced(function* (repo: Git.Repo) {
|
||||||
|
const root = (yield* git.roots(repo))[0]
|
||||||
|
return root ? ID.make(root) : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) {
|
||||||
|
const repo = yield* git.find(input)
|
||||||
|
if (!repo) return { id: ID.global, directory: input, vcs: undefined }
|
||||||
|
|
||||||
|
const previous = yield* cached(repo.store)
|
||||||
|
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
|
||||||
|
|
||||||
|
return {
|
||||||
|
previous,
|
||||||
|
id: id ?? ID.global,
|
||||||
|
directory: repo.directory,
|
||||||
|
vcs: { type: "git" as const, store: repo.store },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) {
|
||||||
|
yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({ resolve, commit })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Git.defaultLayer))
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { Option, Schema, SchemaGetter } from "effect"
|
import { Option, Schema, SchemaGetter } from "effect"
|
||||||
|
|
||||||
|
export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
|
||||||
|
export type AbsolutePath = typeof AbsolutePath.Type
|
||||||
|
|
||||||
|
export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath"))
|
||||||
|
export type RelativePath = typeof RelativePath.Type
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer greater than zero.
|
* Integer greater than zero.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
220
packages/core/test/project.test.ts
Normal file
220
packages/core/test/project.test.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { $ } from "bun"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import path from "path"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Project } from "@opencode-ai/core/project"
|
||||||
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
import { Hash } from "@opencode-ai/core/util/hash"
|
||||||
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
|
const it = testEffect(Project.defaultLayer)
|
||||||
|
|
||||||
|
function remoteID(remote: string) {
|
||||||
|
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
function abs(value: string) {
|
||||||
|
return AbsolutePath.make(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function real(value: string) {
|
||||||
|
return Effect.promise(() => fs.realpath(value)).pipe(Effect.map((value) => AbsolutePath.make(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initRepo(dir: string, opts?: { commit?: boolean; remote?: string }) {
|
||||||
|
await $`git init`.cwd(dir).quiet()
|
||||||
|
await $`git config core.fsmonitor false`.cwd(dir).quiet()
|
||||||
|
await $`git config commit.gpgsign false`.cwd(dir).quiet()
|
||||||
|
await $`git config user.email test@opencode.test`.cwd(dir).quiet()
|
||||||
|
await $`git config user.name Test`.cwd(dir).quiet()
|
||||||
|
if (opts?.commit) await $`git commit --allow-empty -m root`.cwd(dir).quiet()
|
||||||
|
if (opts?.remote) await $`git remote add origin ${opts.remote}`.cwd(dir).quiet()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rootCommit(dir: string) {
|
||||||
|
return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ProjectV2.resolve", () => {
|
||||||
|
it.live("returns global for non-git directory", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.id).toBe(Project.ID.make("global"))
|
||||||
|
expect(path.resolve(result.directory)).toBe(path.resolve(tmp.path))
|
||||||
|
expect(result.previous).toBeUndefined()
|
||||||
|
expect(result.vcs).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("returns git global for repo with no commits and no remote", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.id).toBe(Project.ID.make("global"))
|
||||||
|
expect(result.directory).toBe(yield* real(tmp.path))
|
||||||
|
expect(result.previous).toBeUndefined()
|
||||||
|
expect(result.vcs?.type).toBe("git")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("falls back to root commit when origin is missing", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||||
|
expect(result.directory).toBe(yield* real(tmp.path))
|
||||||
|
expect(result.previous).toBeUndefined()
|
||||||
|
expect(result.vcs?.type).toBe("git")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("prefers normalized origin over root commit", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:Acme/App.git" }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.id).toBe(remoteID("github.com/Acme/App"))
|
||||||
|
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||||
|
expect(result.directory).toBe(yield* real(tmp.path))
|
||||||
|
expect(result.vcs?.type).toBe("git")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("normalizes ssh and https remotes to the same id", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const ssh = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
const https = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||||
|
yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const a = yield* project.resolve(abs(ssh.path))
|
||||||
|
const b = yield* project.resolve(abs(https.path))
|
||||||
|
|
||||||
|
expect(a.id).toBe(remoteID("github.com/owner/repo"))
|
||||||
|
expect(b.id).toBe(a.id)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("ignores file remotes and falls back to root commit", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: `file://${tmp.path}` }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("returns previous cached id from common dir", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||||
|
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||||
|
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("does not write the cache while resolving", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, ".git", "opencode")).exists())).toBe(false)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("resolves from nested directories to repo root", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true }))
|
||||||
|
yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "a", "b"), { recursive: true }))
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(path.join(tmp.path, "a", "b")))
|
||||||
|
|
||||||
|
expect(result.directory).toBe(yield* real(tmp.path))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("linked worktree returns opened worktree directory and previous from common dir", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
|
)
|
||||||
|
const worktree = `${tmp.path}-worktree`
|
||||||
|
yield* Effect.addFinalizer(() =>
|
||||||
|
Effect.promise(() => $`rm -rf ${worktree}`.quiet().nothrow()).pipe(Effect.ignore),
|
||||||
|
)
|
||||||
|
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
||||||
|
yield* Effect.promise(() => Bun.write(path.join(tmp.path, ".git", "opencode"), "old-id"))
|
||||||
|
yield* Effect.promise(() => $`git worktree add ${worktree} -b test-${Date.now()}`.cwd(tmp.path).quiet())
|
||||||
|
const project = yield* Project.Service
|
||||||
|
|
||||||
|
const result = yield* project.resolve(abs(worktree))
|
||||||
|
|
||||||
|
expect(result.directory).toBe(yield* real(worktree))
|
||||||
|
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||||
|
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
||||||
|
expect(result.vcs?.type).toBe("git")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://opencode.ai",
|
"homepage": "https://opencode.ai",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import windowState from "electron-window-state"
|
import windowState from "electron-window-state"
|
||||||
|
import { resolveThemeVariant } from "@opencode-ai/ui/theme/resolve"
|
||||||
|
import type { DesktopTheme } from "@opencode-ai/ui/theme/types"
|
||||||
|
import oc2ThemeJson from "../../../ui/src/theme/themes/oc-2.json"
|
||||||
import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron"
|
import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } from "electron"
|
||||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||||
|
|
@ -15,6 +18,11 @@ const rendererHost = "renderer"
|
||||||
const clipboardWritePermission = "clipboard-sanitized-write"
|
const clipboardWritePermission = "clipboard-sanitized-write"
|
||||||
const notificationPermission = "notifications"
|
const notificationPermission = "notifications"
|
||||||
const rendererPermissions = new Set([clipboardWritePermission, notificationPermission])
|
const rendererPermissions = new Set([clipboardWritePermission, notificationPermission])
|
||||||
|
const oc2Theme = oc2ThemeJson as DesktopTheme
|
||||||
|
const oc2Background = {
|
||||||
|
light: resolveThemeVariant(oc2Theme.light, false)["background-base"],
|
||||||
|
dark: resolveThemeVariant(oc2Theme.dark, true)["background-base"],
|
||||||
|
}
|
||||||
const documentPolicyHeader = "Document-Policy"
|
const documentPolicyHeader = "Document-Policy"
|
||||||
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
|
||||||
|
|
||||||
|
|
@ -46,6 +54,7 @@ export function setRelaunchHandler(handler: () => void) {
|
||||||
|
|
||||||
export function setBackgroundColor(color: string) {
|
export function setBackgroundColor(color: string) {
|
||||||
backgroundColor = color
|
backgroundColor = color
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => win.setBackgroundColor(color))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBackgroundColor(): string | undefined {
|
export function getBackgroundColor(): string | undefined {
|
||||||
|
|
@ -65,6 +74,10 @@ function tone() {
|
||||||
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
return nativeTheme.shouldUseDarkColors ? "dark" : "light"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultBackgroundColor() {
|
||||||
|
return oc2Background[tone()]
|
||||||
|
}
|
||||||
|
|
||||||
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
function overlay(theme: Partial<TitlebarTheme> = {}, zoom = 1) {
|
||||||
const mode = theme.mode ?? tone()
|
const mode = theme.mode ?? tone()
|
||||||
return {
|
return {
|
||||||
|
|
@ -120,7 +133,7 @@ export function createMainWindow() {
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: backgroundColor ?? defaultBackgroundColor(),
|
||||||
...(process.platform === "darwin"
|
...(process.platform === "darwin"
|
||||||
? {
|
? {
|
||||||
titleBarStyle: "hidden" as const,
|
titleBarStyle: "hidden" as const,
|
||||||
|
|
@ -178,7 +191,7 @@ export function createLoadingWindow() {
|
||||||
show: true,
|
show: true,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
backgroundColor,
|
backgroundColor: backgroundColor ?? defaultBackgroundColor(),
|
||||||
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
||||||
...(process.platform === "win32"
|
...(process.platform === "win32"
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.15.9"
|
version = "1.15.10"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/anomalyco/opencode"
|
repository = "https://github.com/anomalyco/opencode"
|
||||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.9/opencode-darwin-arm64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.9/opencode-darwin-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.9/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.9/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.9/opencode-windows-x64.zip"
|
archive = "https://github.com/anomalyco/opencode/releases/download/v1.15.10/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"name": "@opencode-ai/http-recorder",
|
"name": "@opencode-ai/http-recorder",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"name": "@opencode-ai/llm",
|
"name": "@opencode-ai/llm",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,9 @@ const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultConte
|
||||||
// Text / json / error results stay as a string for backward compatibility
|
// Text / json / error results stay as a string for backward compatibility
|
||||||
// with existing cassettes and provider expectations.
|
// with existing cassettes and provider expectations.
|
||||||
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
|
||||||
return yield* Effect.forEach(part.result.value, lowerToolResultContentItem)
|
// Preserve the narrowed array element type when compiled through a consumer package.
|
||||||
|
const content: ReadonlyArray<ToolResultContentPart> = part.result.value
|
||||||
|
return yield* Effect.forEach(content, lowerToolResultContentItem)
|
||||||
})
|
})
|
||||||
|
|
||||||
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
||||||
|
|
|
||||||
|
|
@ -57,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
|
||||||
|
|
@ -72,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,
|
||||||
|
|
@ -86,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,7 +127,7 @@ const OpenAIResponsesCoreFields = {
|
||||||
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
tool_choice: Schema.optional(OpenAIResponsesToolChoice),
|
||||||
store: Schema.optional(Schema.Boolean),
|
store: Schema.optional(Schema.Boolean),
|
||||||
prompt_cache_key: Schema.optional(Schema.String),
|
prompt_cache_key: Schema.optional(Schema.String),
|
||||||
include: optionalArray(Schema.Literal("reasoning.encrypted_content")),
|
include: optionalArray(OpenAIOptions.OpenAIResponseIncludable),
|
||||||
reasoning: Schema.optional(
|
reasoning: Schema.optional(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
effort: Schema.optional(OpenAIOptions.OpenAIReasoningEffort),
|
||||||
|
|
@ -193,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(
|
||||||
|
|
@ -216,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
|
||||||
|
|
@ -245,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,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 })) })
|
||||||
|
|
@ -322,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") {
|
||||||
|
|
@ -352,7 +395,14 @@ 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 = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
|
const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (request: LLMRequest) {
|
||||||
|
|
@ -362,14 +412,14 @@ const lowerOptions = Effect.fn("OpenAIResponses.lowerOptions")(function* (reques
|
||||||
if (effort && !OpenAIOptions.isReasoningEffort(effort))
|
if (effort && !OpenAIOptions.isReasoningEffort(effort))
|
||||||
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
|
return yield* invalid(`OpenAI Responses does not support reasoning effort ${effort}`)
|
||||||
const summary = OpenAIOptions.reasoningSummary(request)
|
const summary = OpenAIOptions.reasoningSummary(request)
|
||||||
const encryptedState = OpenAIOptions.encryptedReasoning(request)
|
const include = OpenAIOptions.include(request)
|
||||||
const verbosity = OpenAIOptions.textVerbosity(request)
|
const verbosity = OpenAIOptions.textVerbosity(request)
|
||||||
const instructions = OpenAIOptions.instructions(request)
|
const instructions = OpenAIOptions.instructions(request)
|
||||||
return {
|
return {
|
||||||
...(instructions ? { instructions } : {}),
|
...(instructions ? { instructions } : {}),
|
||||||
...(store !== undefined ? { store } : {}),
|
...(store !== undefined ? { store } : {}),
|
||||||
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
|
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
|
||||||
...(encryptedState ? { include: ["reasoning.encrypted_content"] as const } : {}),
|
...(include ? { include } : {}),
|
||||||
...(effort || summary ? { reasoning: { effort, summary } } : {}),
|
...(effort || summary ? { reasoning: { effort, summary } } : {}),
|
||||||
...(verbosity ? { text: { verbosity } } : {}),
|
...(verbosity ? { text: { verbosity } } : {}),
|
||||||
}
|
}
|
||||||
|
|
@ -517,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[] = []
|
||||||
|
|
@ -555,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,
|
||||||
|
|
@ -615,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 }))
|
||||||
|
|
@ -683,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)
|
||||||
|
|
@ -709,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),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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[],
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,28 @@ export const OpenAIReasoningEfforts = ReasoningEfforts.filter(
|
||||||
)
|
)
|
||||||
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 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 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 =>
|
const isAnyReasoningEffort = (effort: unknown): effort is ReasoningEffort =>
|
||||||
typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
typeof effort === "string" && REASONING_EFFORTS.has(effort)
|
||||||
|
|
@ -35,12 +51,20 @@ export const reasoningEffort = (request: LLMRequest): ReasoningEffort | undefine
|
||||||
return isAnyReasoningEffort(value) ? value : undefined
|
return isAnyReasoningEffort(value) ? value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reasoningSummary = (request: LLMRequest): "auto" | undefined => {
|
export const reasoningSummary = (request: LLMRequest): "auto" | undefined =>
|
||||||
return options(request)?.reasoningSummary === "auto" ? "auto" : undefined
|
options(request)?.reasoningSummary === "auto" ? "auto" : undefined
|
||||||
}
|
|
||||||
|
|
||||||
export const encryptedReasoning = (request: LLMRequest) =>
|
// Resolve the OpenAI Responses `include` field. Filters out unknown
|
||||||
options(request)?.includeEncryptedReasoning === true ? true : undefined
|
// 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) => {
|
export const promptCacheKey = (request: LLMRequest) => {
|
||||||
const value = options(request)?.promptCacheKey
|
const value = options(request)?.promptCacheKey
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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,6 +407,108 @@ describe("OpenAI Responses route", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("accepts the full ResponseIncludable union", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
prompt: "hi",
|
||||||
|
providerOptions: {
|
||||||
|
openai: {
|
||||||
|
include: ["reasoning.encrypted_content", "code_interpreter_call.outputs", "web_search_call.results"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toEqual([
|
||||||
|
"reasoning.encrypted_content",
|
||||||
|
"code_interpreter_call.outputs",
|
||||||
|
"web_search_call.results",
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("filters unknown includable values out of the include array", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({
|
||||||
|
model,
|
||||||
|
prompt: "hi",
|
||||||
|
// The user passed one invalid entry alongside a valid one. Keep the
|
||||||
|
// valid one so the request still succeeds rather than failing on a
|
||||||
|
// typo from upstream config.
|
||||||
|
providerOptions: { openai: { include: ["reasoning.encrypted_content", "bogus.thing"] } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("treats an explicit empty include as no include at all", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: [] } } }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("treats an all-invalid include as no include at all", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({ model, prompt: "hi", providerOptions: { openai: { include: ["bogus.thing"] } } }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("omits include when no include is set", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({ model, prompt: "hi", providerOptions: { openai: { store: false } } }),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("requests encrypted reasoning by default for GPT-5 reasoning models", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
// The native OpenAI facade configures GPT-5 stateless (store: false) with
|
||||||
|
// reasoningSummary: "auto" by default. Without `include`, a follow-up
|
||||||
|
// turn cannot replay reasoning state, so the facade also opts into
|
||||||
|
// `reasoning.encrypted_content` automatically.
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({
|
||||||
|
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||||
|
prompt: "hi",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.store).toBe(false)
|
||||||
|
expect(prepared.body.include).toEqual(["reasoning.encrypted_content"])
|
||||||
|
expect(prepared.body.reasoning).toEqual({ effort: "medium", summary: "auto" })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("lets callers opt out of the GPT-5 default include", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
LLM.request({
|
||||||
|
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).responses("gpt-5.2"),
|
||||||
|
prompt: "hi",
|
||||||
|
providerOptions: { openai: { include: [] } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.include).toBeUndefined()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("request OpenAI provider options override route defaults", () =>
|
it.effect("request OpenAI provider options override route defaults", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
|
||||||
|
|
@ -547,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(
|
||||||
|
|
@ -570,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(
|
||||||
|
|
@ -627,6 +818,7 @@ describe("OpenAI Responses route", () => {
|
||||||
{ type: "text", text: "After." },
|
{ type: "text", text: "After." },
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
providerOptions: { openai: { store: false } },
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -643,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(
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
"tree-sitter-powershell": "0.25.10",
|
"tree-sitter-powershell": "0.25.10",
|
||||||
"turndown": "7.2.0",
|
"turndown": "7.2.0",
|
||||||
"ulid": "catalog:",
|
"ulid": "catalog:",
|
||||||
"venice-ai-sdk-provider": "2.0.1",
|
"venice-ai-sdk-provider": "2.0.2",
|
||||||
"vscode-jsonrpc": "8.2.1",
|
"vscode-jsonrpc": "8.2.1",
|
||||||
"web-tree-sitter": "0.25.10",
|
"web-tree-sitter": "0.25.10",
|
||||||
"which": "6.0.1",
|
"which": "6.0.1",
|
||||||
|
|
|
||||||
|
|
@ -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,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,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"
|
||||||
|
|
|
||||||
|
|
@ -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,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"
|
||||||
|
|
|
||||||
2
packages/opencode/src/env/index.ts
vendored
2
packages/opencode/src/env/index.ts
vendored
|
|
@ -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,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,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,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,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,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,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"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { and } from "drizzle-orm"
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { ProjectTable } from "./project.sql"
|
import { ProjectTable } from "./project.sql"
|
||||||
import { SessionTable } from "../session/session.sql"
|
import { PermissionTable, SessionTable } from "../session/session.sql"
|
||||||
|
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
|
@ -12,13 +13,14 @@ import { ProjectID } from "./schema"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Command } from "@/command"
|
import { Command } from "@/command"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect"
|
import { Effect, Layer, Scope, Context, Stream, Types, Schema } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { NodePath } from "@effect/platform-node"
|
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
|
import { Project as ProjectV2 } from "@opencode-ai/core/project"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { 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" })
|
||||||
|
|
@ -86,6 +88,10 @@ export function fromRow(row: Row): Info {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergePermissionRules<T extends readonly unknown[]>(oldRules: T, newRules: T): T {
|
||||||
|
return [...new Map([...oldRules, ...newRules].map((rule) => [JSON.stringify(rule), rule])).values()] as unknown as T
|
||||||
|
}
|
||||||
|
|
||||||
export const UpdateInput = Schema.Struct({
|
export const UpdateInput = Schema.Struct({
|
||||||
projectID: ProjectID,
|
projectID: ProjectID,
|
||||||
name: Schema.optional(Schema.String),
|
name: Schema.optional(Schema.String),
|
||||||
|
|
@ -132,16 +138,13 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
|
||||||
|
|
||||||
type GitResult = { code: number; text: string; stderr: string }
|
type GitResult = { code: number; text: string; stderr: string }
|
||||||
|
|
||||||
export const layer: Layer.Layer<
|
export const layer = Layer.effect(
|
||||||
Service,
|
|
||||||
never,
|
|
||||||
AppFileSystem.Service | Path.Path | ChildProcessSpawner.ChildProcessSpawner | Bus.Service | RuntimeFlags.Service
|
|
||||||
> = Layer.effect(
|
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const fs = yield* AppFileSystem.Service
|
const fs = yield* AppFileSystem.Service
|
||||||
const pathSvc = yield* Path.Path
|
const proc = yield* AppProcess.Service
|
||||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||||
|
const projectV2 = yield* ProjectV2.Service
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const flags = yield* RuntimeFlags.Service
|
const flags = yield* RuntimeFlags.Service
|
||||||
|
|
||||||
|
|
@ -175,115 +178,74 @@ export const layer: Layer.Layer<
|
||||||
|
|
||||||
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS)
|
const fakeVcs = Schema.decodeUnknownSync(Schema.optional(ProjectVcs))(Flag.OPENCODE_FAKE_VCS)
|
||||||
|
|
||||||
const resolveGitPath = (cwd: string, name: string) => {
|
|
||||||
if (!name) return cwd
|
|
||||||
name = name.replace(/[\r\n]+$/, "")
|
|
||||||
if (!name) return cwd
|
|
||||||
name = AppFileSystem.windowsPath(name)
|
|
||||||
if (pathSvc.isAbsolute(name)) return pathSvc.normalize(name)
|
|
||||||
return pathSvc.resolve(cwd, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
|
||||||
const readCachedProjectId = Effect.fnUntraced(function* (dir: string) {
|
const migrateProjectId = Effect.fn("Project.migrateProjectId")(function* (
|
||||||
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
oldID: ProjectID | undefined,
|
||||||
Effect.map((x) => x.trim()),
|
newID: ProjectID,
|
||||||
Effect.map((x) => ProjectID.make(x)),
|
) {
|
||||||
Effect.catch(() => Effect.void),
|
if (!oldID) return
|
||||||
|
if (oldID === ProjectID.global) return
|
||||||
|
if (oldID === newID) return
|
||||||
|
|
||||||
|
yield* Effect.sync(() =>
|
||||||
|
Database.transaction(
|
||||||
|
(d) => {
|
||||||
|
const oldProject = d.select().from(ProjectTable).where(eq(ProjectTable.id, oldID)).get()
|
||||||
|
const newProject = d.select().from(ProjectTable).where(eq(ProjectTable.id, newID)).get()
|
||||||
|
if (oldProject && !newProject) {
|
||||||
|
d.insert(ProjectTable)
|
||||||
|
.values({
|
||||||
|
...oldProject,
|
||||||
|
id: newID,
|
||||||
|
time_updated: Date.now(),
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPermission = d.select().from(PermissionTable).where(eq(PermissionTable.project_id, oldID)).get()
|
||||||
|
const newPermission = d.select().from(PermissionTable).where(eq(PermissionTable.project_id, newID)).get()
|
||||||
|
if (oldPermission && newPermission) {
|
||||||
|
d.update(PermissionTable)
|
||||||
|
.set({
|
||||||
|
data: mergePermissionRules(oldPermission.data, newPermission.data),
|
||||||
|
time_created: Math.min(oldPermission.time_created, newPermission.time_created),
|
||||||
|
time_updated: Date.now(),
|
||||||
|
})
|
||||||
|
.where(eq(PermissionTable.project_id, newID))
|
||||||
|
.run()
|
||||||
|
d.delete(PermissionTable).where(eq(PermissionTable.project_id, oldID)).run()
|
||||||
|
}
|
||||||
|
if (oldPermission && !newPermission) {
|
||||||
|
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(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
||||||
|
|
||||||
|
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||||
|
},
|
||||||
|
{ behavior: "immediate" },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
|
const fromDirectory = Effect.fn("Project.fromDirectory")(function* (directory: string) {
|
||||||
log.info("fromDirectory", { directory })
|
log.info("fromDirectory", { directory })
|
||||||
|
|
||||||
// Phase 1: discover git info
|
const data = yield* projectV2.resolve(AbsolutePath.make(directory))
|
||||||
type DiscoveryResult = { id: ProjectID; worktree: string; sandbox: string; vcs: Info["vcs"] }
|
const worktree = data.id === ProjectV2.ID.make("global") && !data.vcs ? "/" : data.directory
|
||||||
|
|
||||||
const data: DiscoveryResult = yield* Effect.gen(function* () {
|
|
||||||
const dotgitMatches = yield* fs.up({ targets: [".git"], start: directory }).pipe(Effect.orDie)
|
|
||||||
const dotgit = dotgitMatches[0]
|
|
||||||
|
|
||||||
if (!dotgit) {
|
|
||||||
return {
|
|
||||||
id: ProjectID.global,
|
|
||||||
worktree: "/",
|
|
||||||
sandbox: "/",
|
|
||||||
vcs: fakeVcs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sandbox = pathSvc.dirname(dotgit)
|
|
||||||
const gitBinary = yield* Effect.sync(() => which("git"))
|
|
||||||
let id = yield* readCachedProjectId(dotgit)
|
|
||||||
|
|
||||||
if (!gitBinary) {
|
|
||||||
return {
|
|
||||||
id: id ?? ProjectID.global,
|
|
||||||
worktree: sandbox,
|
|
||||||
sandbox,
|
|
||||||
vcs: fakeVcs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonDir = yield* git(["rev-parse", "--git-common-dir"], { cwd: sandbox })
|
|
||||||
if (commonDir.code !== 0) {
|
|
||||||
return {
|
|
||||||
id: id ?? ProjectID.global,
|
|
||||||
worktree: sandbox,
|
|
||||||
sandbox,
|
|
||||||
vcs: fakeVcs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const common = resolveGitPath(sandbox, commonDir.text.trim())
|
|
||||||
const bareCheck = yield* git(["config", "--bool", "core.bare"], { cwd: sandbox })
|
|
||||||
const isBareRepo = bareCheck.code === 0 && bareCheck.text.trim() === "true"
|
|
||||||
const worktree = common === sandbox ? sandbox : isBareRepo ? common : pathSvc.dirname(common)
|
|
||||||
|
|
||||||
if (id == null) {
|
|
||||||
id = yield* readCachedProjectId(common)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
const revList = yield* git(["rev-list", "--max-parents=0", "HEAD"], { cwd: sandbox })
|
|
||||||
const roots = revList.text
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.toSorted()
|
|
||||||
|
|
||||||
id = roots[0] ? ProjectID.make(roots[0]) : undefined
|
|
||||||
if (id) {
|
|
||||||
yield* fs.writeFileString(pathSvc.join(common, "opencode"), id).pipe(Effect.ignore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return { id: ProjectID.global, worktree: sandbox, sandbox, vcs: "git" as const }
|
|
||||||
}
|
|
||||||
|
|
||||||
const topLevel = yield* git(["rev-parse", "--show-toplevel"], { cwd: sandbox })
|
|
||||||
if (topLevel.code !== 0) {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
worktree: sandbox,
|
|
||||||
sandbox,
|
|
||||||
vcs: fakeVcs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sandbox = resolveGitPath(sandbox, topLevel.text.trim())
|
|
||||||
|
|
||||||
return { id, sandbox, worktree, vcs: "git" as const }
|
|
||||||
})
|
|
||||||
|
|
||||||
// Phase 2: upsert
|
// Phase 2: upsert
|
||||||
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, data.id)).get())
|
const projectID = ProjectID.make(data.id)
|
||||||
|
yield* migrateProjectId(data.previous ? ProjectID.make(data.previous) : undefined, projectID)
|
||||||
|
const row = yield* db((d) => d.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get())
|
||||||
const existing = row
|
const existing = row
|
||||||
? fromRow(row)
|
? fromRow(row)
|
||||||
: {
|
: {
|
||||||
id: data.id,
|
id: projectID,
|
||||||
worktree: data.worktree,
|
worktree,
|
||||||
vcs: data.vcs,
|
vcs: data.vcs?.type ?? fakeVcs,
|
||||||
sandboxes: [] as string[],
|
sandboxes: [] as string[],
|
||||||
time: { created: Date.now(), updated: Date.now() },
|
time: { created: Date.now(), updated: Date.now() },
|
||||||
}
|
}
|
||||||
|
|
@ -292,12 +254,16 @@ export const layer: Layer.Layer<
|
||||||
|
|
||||||
const result: Info = {
|
const result: Info = {
|
||||||
...existing,
|
...existing,
|
||||||
worktree: data.worktree,
|
worktree: projectID === ProjectID.global ? worktree : existing.worktree,
|
||||||
vcs: data.vcs,
|
vcs: data.vcs?.type ?? fakeVcs,
|
||||||
time: { ...existing.time, updated: Date.now() },
|
time: { ...existing.time, updated: Date.now() },
|
||||||
}
|
}
|
||||||
if (data.sandbox !== result.worktree && !result.sandboxes.includes(data.sandbox))
|
if (
|
||||||
result.sandboxes.push(data.sandbox)
|
projectID !== ProjectID.global &&
|
||||||
|
data.directory !== result.worktree &&
|
||||||
|
!result.sandboxes.includes(data.directory)
|
||||||
|
)
|
||||||
|
result.sandboxes.push(data.directory)
|
||||||
result.sandboxes = yield* Effect.forEach(
|
result.sandboxes = yield* Effect.forEach(
|
||||||
result.sandboxes,
|
result.sandboxes,
|
||||||
(s) =>
|
(s) =>
|
||||||
|
|
@ -343,18 +309,21 @@ export const layer: Layer.Layer<
|
||||||
.run(),
|
.run(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (data.id !== ProjectID.global) {
|
if (projectID !== ProjectID.global) {
|
||||||
yield* db((d) =>
|
yield* db((d) =>
|
||||||
d
|
d
|
||||||
.update(SessionTable)
|
.update(SessionTable)
|
||||||
.set({ project_id: data.id })
|
.set({ project_id: projectID })
|
||||||
.where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.worktree)))
|
.where(and(eq(SessionTable.project_id, ProjectID.global), eq(SessionTable.directory, data.directory)))
|
||||||
.run(),
|
.run(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* emitUpdated(result)
|
yield* emitUpdated(result)
|
||||||
return { project: result, sandbox: data.sandbox }
|
if (projectID !== ProjectID.global && data.vcs?.type === "git") {
|
||||||
|
yield* projectV2.commit({ store: data.vcs.store, id: data.id })
|
||||||
|
}
|
||||||
|
return { project: result, sandbox: data.vcs ? data.directory : worktree }
|
||||||
})
|
})
|
||||||
|
|
||||||
const discover = Effect.fn("Project.discover")(function* (input: Info) {
|
const discover = Effect.fn("Project.discover")(function* (input: Info) {
|
||||||
|
|
@ -510,9 +479,10 @@ export const layer: Layer.Layer<
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(
|
||||||
Layer.provide(Bus.defaultLayer),
|
Layer.provide(Bus.defaultLayer),
|
||||||
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
|
Layer.provide(AppProcess.defaultLayer),
|
||||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
|
||||||
Layer.provide(RuntimeFlags.defaultLayer),
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,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"
|
||||||
|
|
|
||||||
|
|
@ -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,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,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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ export const ShellTool = Tool.define(
|
||||||
const trunc = yield* Truncate.Service
|
const trunc = yield* Truncate.Service
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
const flags = yield* RuntimeFlags.Service
|
const flags = yield* RuntimeFlags.Service
|
||||||
const defaultTimeout = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
|
const defaultTimeoutMs = flags.bashDefaultTimeoutMs ?? 2 * 60 * 1000
|
||||||
|
|
||||||
const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) {
|
const cygpath = Effect.fn("ShellTool.cygpath")(function* (shell: string, text: string) {
|
||||||
const lines = yield* spawner
|
const lines = yield* spawner
|
||||||
|
|
@ -601,7 +601,7 @@ export const ShellTool = Tool.define(
|
||||||
const shell = Shell.acceptable(cfg.shell)
|
const shell = Shell.acceptable(cfg.shell)
|
||||||
const name = Shell.name(shell)
|
const name = Shell.name(shell)
|
||||||
const limits = yield* trunc.limits()
|
const limits = yield* trunc.limits()
|
||||||
const prompt = ShellPrompt.render(name, process.platform, limits)
|
const prompt = ShellPrompt.render(name, process.platform, limits, defaultTimeoutMs)
|
||||||
log.info("shell tool using shell", { shell })
|
log.info("shell tool using shell", { shell })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -616,7 +616,7 @@ export const ShellTool = Tool.define(
|
||||||
if (params.timeout !== undefined && params.timeout < 0) {
|
if (params.timeout !== undefined && params.timeout < 0) {
|
||||||
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
|
||||||
}
|
}
|
||||||
const timeout = params.timeout ?? defaultTimeout
|
const timeout = params.timeout ?? defaultTimeoutMs
|
||||||
const ps = Shell.ps(shell)
|
const ps = Shell.ps(shell)
|
||||||
yield* Effect.scoped(
|
yield* Effect.scoped(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ function truncationGuidance(limits: Limits, commands: string) {
|
||||||
return `\n - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use ${commands} to limit output; the full output will already be captured to a file for more precise searching.`
|
return `\n - If the output exceeds ${limits.maxLines} lines or ${limits.maxBytes} bytes, it will be truncated and the full output will be written to a file. You can use Read with offset/limit to read specific sections or Grep to search the full content. Do NOT use ${commands} to limit output; the full output will already be captured to a file for more precise searching.`
|
||||||
}
|
}
|
||||||
|
|
||||||
function bashCommandSection(chain: string, limits: Limits) {
|
function bashCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
|
||||||
return `Before executing the command, please follow these steps:
|
return `Before executing the command, please follow these steps:
|
||||||
|
|
||||||
1. Directory Verification:
|
1. Directory Verification:
|
||||||
|
|
@ -108,7 +108,7 @@ function bashCommandSection(chain: string, limits: Limits) {
|
||||||
|
|
||||||
Usage notes:
|
Usage notes:
|
||||||
- The command argument is required.
|
- The command argument is required.
|
||||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
|
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`head`, `tail`, or other truncation commands")}
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`head`, `tail`, or other truncation commands")}
|
||||||
|
|
||||||
- Avoid using Bash with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
- Avoid using Bash with the \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||||
|
|
@ -132,7 +132,13 @@ Usage notes:
|
||||||
</bad-example>`
|
</bad-example>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function powershellCommandSection(name: string, chain: string, pathSep: string, limits: Limits) {
|
function powershellCommandSection(
|
||||||
|
name: string,
|
||||||
|
chain: string,
|
||||||
|
pathSep: string,
|
||||||
|
limits: Limits,
|
||||||
|
defaultTimeoutMs: number,
|
||||||
|
) {
|
||||||
return `${powershellNotes(name)}
|
return `${powershellNotes(name)}
|
||||||
|
|
||||||
Before executing the command, please follow these steps:
|
Before executing the command, please follow these steps:
|
||||||
|
|
@ -153,7 +159,7 @@ Before executing the command, please follow these steps:
|
||||||
|
|
||||||
Usage notes:
|
Usage notes:
|
||||||
- The command argument is required.
|
- The command argument is required.
|
||||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
|
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`Select-Object -First`, `Select-Object -Last`, or other truncation commands")}
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`Select-Object -First`, `Select-Object -Last`, or other truncation commands")}
|
||||||
|
|
||||||
- Avoid using Shell with PowerShell file/content cmdlets unless explicitly instructed or when these cmdlets are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
- Avoid using Shell with PowerShell file/content cmdlets unless explicitly instructed or when these cmdlets are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||||
|
|
@ -177,7 +183,7 @@ Usage notes:
|
||||||
</bad-example>`
|
</bad-example>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmdCommandSection(chain: string, limits: Limits) {
|
function cmdCommandSection(chain: string, limits: Limits, defaultTimeoutMs: number) {
|
||||||
return `# cmd.exe shell notes
|
return `# cmd.exe shell notes
|
||||||
- Use double quotes for paths with spaces.
|
- Use double quotes for paths with spaces.
|
||||||
- Use %VAR% for environment variables.
|
- Use %VAR% for environment variables.
|
||||||
|
|
@ -202,7 +208,7 @@ Before executing the command, please follow these steps:
|
||||||
|
|
||||||
Usage notes:
|
Usage notes:
|
||||||
- The command argument is required.
|
- The command argument is required.
|
||||||
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after 120000ms (2 minutes).
|
- You can specify an optional timeout in milliseconds. If not specified, commands will time out after ${defaultTimeoutMs}ms.
|
||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`more` or other pagination commands")}
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.${truncationGuidance(limits, "`more` or other pagination commands")}
|
||||||
|
|
||||||
- Avoid using Shell with cmd.exe file/content commands unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
- Avoid using Shell with cmd.exe file/content commands unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||||
|
|
@ -226,7 +232,7 @@ Usage notes:
|
||||||
</bad-example>`
|
</bad-example>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
|
function profile(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
|
||||||
const isPowerShell = PS.has(name)
|
const isPowerShell = PS.has(name)
|
||||||
const chain = chainGuidance(name)
|
const chain = chainGuidance(name)
|
||||||
if (CMD.has(name)) {
|
if (CMD.has(name)) {
|
||||||
|
|
@ -234,7 +240,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
|
||||||
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
|
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
|
||||||
workdirSection:
|
workdirSection:
|
||||||
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
|
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
|
||||||
commandSection: cmdCommandSection(chain, limits),
|
commandSection: cmdCommandSection(chain, limits, defaultTimeoutMs),
|
||||||
gitCommands: "git commands",
|
gitCommands: "git commands",
|
||||||
gitCommandRestriction: "git commands",
|
gitCommandRestriction: "git commands",
|
||||||
createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
|
createPrInstruction: "Create PR using a temporary body file so cmd.exe quoting stays simple.",
|
||||||
|
|
@ -247,7 +253,13 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
|
||||||
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
|
intro: `Executes a given ${shellDisplayName(name)} command with optional timeout, ensuring proper handling and security measures.`,
|
||||||
workdirSection:
|
workdirSection:
|
||||||
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
|
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.",
|
||||||
commandSection: powershellCommandSection(name, chain, platform === "win32" ? "\\" : "/", limits),
|
commandSection: powershellCommandSection(
|
||||||
|
name,
|
||||||
|
chain,
|
||||||
|
platform === "win32" ? "\\" : "/",
|
||||||
|
limits,
|
||||||
|
defaultTimeoutMs,
|
||||||
|
),
|
||||||
gitCommands: "git commands",
|
gitCommands: "git commands",
|
||||||
gitCommandRestriction: "git commands",
|
gitCommandRestriction: "git commands",
|
||||||
createPrInstruction: "Create PR using gh pr create with a PowerShell here-string to pass the body correctly.",
|
createPrInstruction: "Create PR using gh pr create with a PowerShell here-string to pass the body correctly.",
|
||||||
|
|
@ -263,7 +275,7 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
|
||||||
"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
|
"Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.",
|
||||||
workdirSection:
|
workdirSection:
|
||||||
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.",
|
"All commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.",
|
||||||
commandSection: bashCommandSection(chain, limits),
|
commandSection: bashCommandSection(chain, limits, defaultTimeoutMs),
|
||||||
gitCommands: "bash commands",
|
gitCommands: "bash commands",
|
||||||
gitCommandRestriction: "git bash commands",
|
gitCommandRestriction: "git bash commands",
|
||||||
createPrInstruction:
|
createPrInstruction:
|
||||||
|
|
@ -275,8 +287,8 @@ function profile(name: string, platform: NodeJS.Platform, limits: Limits) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render(name: string, platform: NodeJS.Platform, limits: Limits) {
|
export function render(name: string, platform: NodeJS.Platform, limits: Limits, defaultTimeoutMs: number) {
|
||||||
const selected = profile(name, platform, limits)
|
const selected = profile(name, platform, limits, defaultTimeoutMs)
|
||||||
return {
|
return {
|
||||||
description: renderPrompt(DESCRIPTION, {
|
description: renderPrompt(DESCRIPTION, {
|
||||||
intro: selected.intro,
|
intro: selected.intro,
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,12 @@ export const layer = Layer.effect(
|
||||||
const configSvc = yield* Effect.serviceOption(Config.Service)
|
const configSvc = yield* Effect.serviceOption(Config.Service)
|
||||||
if (Option.isNone(configSvc)) return { enabled: true, maxLines: MAX_LINES, maxBytes: MAX_BYTES }
|
if (Option.isNone(configSvc)) return { enabled: true, maxLines: MAX_LINES, maxBytes: MAX_BYTES }
|
||||||
const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
|
||||||
|
const tool_output = cfg?.tool_output
|
||||||
|
if (tool_output?.truncate === false) return { enabled: false, maxLines: MAX_LINES, maxBytes: MAX_BYTES }
|
||||||
return {
|
return {
|
||||||
enabled: cfg?.tool_output?.truncate !== false,
|
enabled: true,
|
||||||
maxLines: cfg?.tool_output?.max_lines ?? MAX_LINES,
|
maxLines: tool_output?.max_lines ?? MAX_LINES,
|
||||||
maxBytes: cfg?.tool_output?.max_bytes ?? MAX_BYTES,
|
maxBytes: tool_output?.max_bytes ?? MAX_BYTES,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -7,10 +7,21 @@ 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"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
|
import { AppProcess } from "@opencode-ai/core/process"
|
||||||
|
import { Project as ProjectV2 } from "@opencode-ai/core/project"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
|
@ -29,6 +40,10 @@ function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function remoteProjectID(remote: string) {
|
||||||
|
return ProjectID.make(Hash.fast(`git-remote:${remote}`))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mock ChildProcessSpawner layer that intercepts git subcommands
|
* 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
|
||||||
|
|
@ -66,7 +81,9 @@ function mockGitFailure(failArg: string) {
|
||||||
|
|
||||||
function projectLayerWithFailure(failArg: string) {
|
function projectLayerWithFailure(failArg: string) {
|
||||||
return Project.layer.pipe(
|
return Project.layer.pipe(
|
||||||
|
Layer.provide(AppProcess.layer.pipe(Layer.provide(mockGitFailure(failArg)))),
|
||||||
Layer.provide(mockGitFailure(failArg)),
|
Layer.provide(mockGitFailure(failArg)),
|
||||||
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.provide(Bus.defaultLayer),
|
Layer.provide(Bus.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
|
|
@ -77,6 +94,8 @@ function projectLayerWithFailure(failArg: string) {
|
||||||
function projectLayerWithRuntimeFlags(flags: Parameters<typeof RuntimeFlags.layer>[0]) {
|
function projectLayerWithRuntimeFlags(flags: Parameters<typeof RuntimeFlags.layer>[0]) {
|
||||||
return Project.layer.pipe(
|
return Project.layer.pipe(
|
||||||
Layer.provide(Bus.defaultLayer),
|
Layer.provide(Bus.defaultLayer),
|
||||||
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
|
Layer.provide(AppProcess.defaultLayer),
|
||||||
Layer.provide(AppFileSystem.defaultLayer),
|
Layer.provide(AppFileSystem.defaultLayer),
|
||||||
Layer.provide(NodePath.layer),
|
Layer.provide(NodePath.layer),
|
||||||
Layer.provide(RuntimeFlags.layer(flags)),
|
Layer.provide(RuntimeFlags.layer(flags)),
|
||||||
|
|
@ -128,9 +147,6 @@ describe("Project.fromDirectory", () => {
|
||||||
expect(project.id).not.toBe(ProjectID.global)
|
expect(project.id).not.toBe(ProjectID.global)
|
||||||
expect(project.vcs).toBe("git")
|
expect(project.vcs).toBe("git")
|
||||||
expect(project.worktree).toBe(tmp)
|
expect(project.worktree).toBe(tmp)
|
||||||
|
|
||||||
const opencodeFile = path.join(tmp, ".git", "opencode")
|
|
||||||
expect(yield* Effect.promise(() => Bun.file(opencodeFile).exists())).toBe(true)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -150,6 +166,94 @@ describe("Project.fromDirectory", () => {
|
||||||
expect(b.id).toBe(a.id)
|
expect(b.id).toBe(a.id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("prefers normalized origin remote over root commit", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* tmpdirScoped({ git: true })
|
||||||
|
yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet())
|
||||||
|
|
||||||
|
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||||
|
|
||||||
|
expect(project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo"))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("normalizes equivalent origin URL forms to the same project ID", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const ssh = yield* tmpdirScoped({ git: true })
|
||||||
|
const https = yield* tmpdirScoped({ git: true })
|
||||||
|
yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(ssh).quiet())
|
||||||
|
yield* Effect.promise(() => $`git remote add origin https://github.com/owner/repo.git`.cwd(https).quiet())
|
||||||
|
|
||||||
|
const { project: a } = yield* run((svc) => svc.fromDirectory(ssh))
|
||||||
|
const { project: b } = yield* run((svc) => svc.fromDirectory(https))
|
||||||
|
|
||||||
|
expect(a.id).toBe(remoteProjectID("github.com/owner/repo"))
|
||||||
|
expect(b.id).toBe(a.id)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("migrates cached root project data when origin becomes available", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tmp = yield* tmpdirScoped({ git: true })
|
||||||
|
const projects = yield* Project.Service
|
||||||
|
const { project: rootProject } = yield* projects.fromDirectory(tmp)
|
||||||
|
const remoteID = remoteProjectID("github.com/acme/app")
|
||||||
|
const sessionID = crypto.randomUUID() as SessionID
|
||||||
|
const workspaceID = WorkspaceID.ascending()
|
||||||
|
|
||||||
|
yield* Effect.sync(() => {
|
||||||
|
Database.use((db) => {
|
||||||
|
db.insert(SessionTable)
|
||||||
|
.values({
|
||||||
|
id: sessionID,
|
||||||
|
project_id: rootProject.id,
|
||||||
|
slug: sessionID,
|
||||||
|
directory: tmp,
|
||||||
|
title: "test",
|
||||||
|
version: "0.0.0-test",
|
||||||
|
time_created: Date.now(),
|
||||||
|
time_updated: Date.now(),
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
db.insert(PermissionTable)
|
||||||
|
.values({
|
||||||
|
project_id: rootProject.id,
|
||||||
|
data: [{ permission: "edit", pattern: "*", action: "allow" }],
|
||||||
|
time_created: Date.now(),
|
||||||
|
time_updated: Date.now(),
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
db.insert(WorkspaceTable)
|
||||||
|
.values({
|
||||||
|
id: workspaceID,
|
||||||
|
type: "local",
|
||||||
|
name: "test",
|
||||||
|
project_id: rootProject.id,
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
|
||||||
|
|
||||||
|
const { project } = yield* projects.fromDirectory(tmp)
|
||||||
|
|
||||||
|
expect(project.id).toBe(remoteID)
|
||||||
|
expect(
|
||||||
|
Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get()),
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())?.project_id,
|
||||||
|
).toBe(remoteID)
|
||||||
|
expect(
|
||||||
|
Database.use((db) => db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get()),
|
||||||
|
).toBeDefined()
|
||||||
|
expect(
|
||||||
|
Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get())
|
||||||
|
?.project_id,
|
||||||
|
).toBe(remoteID)
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Project.fromDirectory git failure paths", () => {
|
describe("Project.fromDirectory git failure paths", () => {
|
||||||
|
|
@ -200,7 +304,7 @@ describe("Project.fromDirectory with worktrees", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("should set worktree to root when called from a worktree", () =>
|
it.live("tracks a linked worktree as the opened project directory", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* tmpdirScoped({ git: true })
|
const tmp = yield* tmpdirScoped({ git: true })
|
||||||
|
|
||||||
|
|
@ -217,9 +321,9 @@ describe("Project.fromDirectory with worktrees", () => {
|
||||||
|
|
||||||
const { project, sandbox } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
const { project, sandbox } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
||||||
|
|
||||||
expect(project.worktree).toBe(tmp)
|
expect(project.worktree).toBe(worktreePath)
|
||||||
expect(sandbox).toBe(worktreePath)
|
expect(sandbox).toBe(worktreePath)
|
||||||
expect(project.sandboxes).toContain(worktreePath)
|
expect(project.sandboxes).not.toContain(worktreePath)
|
||||||
expect(project.sandboxes).not.toContain(tmp)
|
expect(project.sandboxes).not.toContain(tmp)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -245,7 +349,6 @@ describe("Project.fromDirectory with worktrees", () => {
|
||||||
|
|
||||||
expect(wt.id).toBe(main.id)
|
expect(wt.id).toBe(main.id)
|
||||||
|
|
||||||
// Cache should live in the common .git dir, not the worktree's .git file
|
|
||||||
const cache = path.join(tmp, ".git", "opencode")
|
const cache = path.join(tmp, ".git", "opencode")
|
||||||
const exists = yield* Effect.promise(() => Bun.file(cache).exists())
|
const exists = yield* Effect.promise(() => Bun.file(cache).exists())
|
||||||
expect(exists).toBe(true)
|
expect(exists).toBe(true)
|
||||||
|
|
@ -300,8 +403,7 @@ describe("Project.fromDirectory with worktrees", () => {
|
||||||
yield* run((svc) => svc.fromDirectory(worktree1))
|
yield* run((svc) => svc.fromDirectory(worktree1))
|
||||||
const { project } = yield* run((svc) => svc.fromDirectory(worktree2))
|
const { project } = yield* run((svc) => svc.fromDirectory(worktree2))
|
||||||
|
|
||||||
expect(project.worktree).toBe(tmp)
|
expect(project.worktree).toBe(worktree1)
|
||||||
expect(project.sandboxes).toContain(worktree1)
|
|
||||||
expect(project.sandboxes).toContain(worktree2)
|
expect(project.sandboxes).toContain(worktree2)
|
||||||
expect(project.sandboxes).not.toContain(tmp)
|
expect(project.sandboxes).not.toContain(tmp)
|
||||||
}),
|
}),
|
||||||
|
|
@ -640,7 +742,7 @@ describe("Project.fromDirectory with bare repos", () => {
|
||||||
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
||||||
|
|
||||||
expect(project.id).not.toBe(ProjectID.global)
|
expect(project.id).not.toBe(ProjectID.global)
|
||||||
expect(project.worktree).toBe(barePath)
|
expect(project.worktree).toBe(worktreePath)
|
||||||
|
|
||||||
const correctCache = path.join(barePath, "opencode")
|
const correctCache = path.join(barePath, "opencode")
|
||||||
const wrongCache = path.join(parentDir, ".git", "opencode")
|
const wrongCache = path.join(parentDir, ".git", "opencode")
|
||||||
|
|
@ -703,7 +805,7 @@ describe("Project.fromDirectory with bare repos", () => {
|
||||||
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
const { project } = yield* run((svc) => svc.fromDirectory(worktreePath))
|
||||||
|
|
||||||
expect(project.id).not.toBe(ProjectID.global)
|
expect(project.id).not.toBe(ProjectID.global)
|
||||||
expect(project.worktree).toBe(barePath)
|
expect(project.worktree).toBe(worktreePath)
|
||||||
|
|
||||||
const correctCache = path.join(barePath, "opencode")
|
const correctCache = path.join(barePath, "opencode")
|
||||||
expect(yield* Effect.promise(() => Bun.file(correctCache).exists())).toBe(true)
|
expect(yield* Effect.promise(() => Bun.file(correctCache).exists())).toBe(true)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -591,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?"),
|
||||||
|
|
@ -608,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" }] })
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1085,10 +1085,15 @@ describe("tool.shell abort", () => {
|
||||||
runIn(
|
runIn(
|
||||||
projectRoot,
|
projectRoot,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const result = yield* run({
|
const tool = yield* initShell()
|
||||||
command: `echo started && sleep 60`,
|
expect(tool.description).toContain("commands will time out after 500ms")
|
||||||
description: "Default timeout test",
|
const result = yield* tool.execute(
|
||||||
})
|
{
|
||||||
|
command: `echo started && sleep 60`,
|
||||||
|
description: "Default timeout test",
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
expect(result.output).toContain("started")
|
expect(result.output).toContain("started")
|
||||||
expect(result.output).toContain("exceeding timeout 500 ms")
|
expect(result.output).toContain("exceeding timeout 500 ms")
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.15.9",
|
"version": "1.15.10",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,8 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/<model-id>`. على سبيل المثال، بالنسبة إلى Kimi K2.6، ستستخدم `opencode-go/kimi-k2.6` في إعداداتك.
|
يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/<model-id>`. على سبيل المثال، بالنسبة إلى Kimi K2.6، ستستخدم `opencode-go/kimi-k2.6` في إعداداتك.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
|
||||||
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
||||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,8 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa.
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
[Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji
|
[Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji
|
||||||
koristi format `opencode-go/<model-id>`. Na primjer, za Kimi K2.6, koristili biste
|
koristi format `opencode-go/<model-id>`. Na primjer, za Kimi K2.6, koristili biste
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
|
||||||
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
||||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,8 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints.
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
Dit [model id](/docs/config/#models) i din OpenCode config
|
Dit [model id](/docs/config/#models) i din OpenCode config
|
||||||
bruger formatet `opencode-go/<model-id>`. For eksempel for Kimi K2.6, vil du
|
bruger formatet `opencode-go/<model-id>`. For eksempel for Kimi K2.6, vil du
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
|
||||||
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
||||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,8 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen.
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/<model-id>`. Für Kimi K2.6 würdest du beispielsweise `opencode-go/kimi-k2.6` in deiner Config verwenden.
|
Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/<model-id>`. Für Kimi K2.6 würdest du beispielsweise `opencode-go/kimi-k2.6` in deiner Config verwenden.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
|
||||||
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
||||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,8 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode
|
El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode
|
||||||
usa el formato `opencode-go/<model-id>`. Por ejemplo, para Kimi K2.6, usarías
|
usa el formato `opencode-go/<model-id>`. Por ejemplo, para Kimi K2.6, usarías
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,8 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
|
||||||
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` |
|
||||||
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,8 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d'
|
||||||
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` |
|
| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
|
||||||
L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/<model-id>`. Par exemple, pour Kimi K2.6, vous utiliseriez `opencode-go/kimi-k2.6` dans votre configuration.
|
L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/<model-id>`. Par exemple, pour Kimi K2.6, vous utiliseriez `opencode-go/kimi-k2.6` dans votre configuration.
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue