From 726ae6f54125a27246cc08b46f05337eb504159c Mon Sep 17 00:00:00 2001 From: Victor Navarro Date: Tue, 5 May 2026 16:08:28 +0200 Subject: [PATCH 01/70] chore: configure alerting and monitoring (#25857) --- .github/workflows/deploy.yml | 2 + bun.lock | 9 + infra/console.ts | 5 + infra/monitoring.ts | 320 ++++++++++++++++++ packages/console/app/package.json | 1 + .../app/src/routes/incident/webhook.ts | 75 ++++ packages/console/core/sst-env.d.ts | 8 + packages/console/function/sst-env.d.ts | 8 + packages/console/resource/sst-env.d.ts | 8 + packages/enterprise/sst-env.d.ts | 8 + packages/function/sst-env.d.ts | 8 + sst-env.d.ts | 8 + sst.config.ts | 11 + 13 files changed, 471 insertions(+) create mode 100644 infra/monitoring.ts create mode 100644 packages/console/app/src/routes/incident/webhook.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e346d0cd5c..10b8dc180b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -36,6 +36,8 @@ jobs: PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }} PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }} STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }} + HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} + INCIDENT_API_KEY: ${{ secrets.INCIDENT_API_KEY }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} diff --git a/bun.lock b/bun.lock index ccbbb59656..35075c1441 100644 --- a/bun.lock +++ b/bun.lock @@ -107,6 +107,7 @@ "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", + "svix": "1.92.2", "vite": "catalog:", "zod": "catalog:", }, @@ -2159,6 +2160,8 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], @@ -3169,6 +3172,8 @@ "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], @@ -4643,6 +4648,8 @@ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -4711,6 +4718,8 @@ "sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="], + "svix": ["svix@1.92.2", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ=="], + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], diff --git a/infra/console.ts b/infra/console.ts index 201d5bdc65..d92fcaa8e2 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -221,6 +221,9 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", { properties: { value: stripeWebhook.secret }, }) +const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET") +const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL") + const gatewayKv = new sst.cloudflare.Kv("GatewayKv") //////////////// @@ -251,6 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", { database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, + INCIDENT_WEBHOOK_SIGNING_SECRET, + DISCORD_INCIDENT_WEBHOOK_URL, STRIPE_SECRET_KEY, EMAILOCTOPUS_API_KEY, AWS_SES_ACCESS_KEY_ID, diff --git a/infra/monitoring.ts b/infra/monitoring.ts new file mode 100644 index 0000000000..f500b099a0 --- /dev/null +++ b/infra/monitoring.ts @@ -0,0 +1,320 @@ +const displayName = (s: string) => + s + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ") + .replace(/(?<=\d) (?=\d)/g, ".") + +const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "") + +const varSpec = (label: string, name: string) => + $jsonStringify({ + content: [ + { + content: [ + { + attrs: { + name, + label, + missing: false, + }, + type: "varSpec", + }, + ], + type: "paragraph", + }, + ], + type: "doc", + }) + +const fields = { + model: incident.getAlertAttributeOutput({ name: "Model" }), + product: incident.getAlertAttributeOutput({ name: "Product" }), +} + +const alertSource = new incident.AlertSource("HoneycombAlertSource", { + name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`, + sourceType: "honeycomb", + template: { + title: { + literal: varSpec("Payload -> Title", "title"), + }, + description: { + literal: varSpec("Payload -> Description", "description"), + }, + attributes: [ + { + alertAttributeId: fields.model.id, + binding: { + value: { + reference: 'expressions["model"]', + }, + mergeStrategy: "first_wins", + }, + }, + { + alertAttributeId: fields.product.id, + binding: { + value: { + reference: 'expressions["product"]', + }, + mergeStrategy: "first_wins", + }, + }, + ], + expressions: [ + { + label: "Model", + operations: [ + { + operationType: "parse", + parse: { + returns: { + array: false, + type: fields.model.type, + }, + source: "$['model']", + }, + }, + ], + reference: "model", + rootReference: "payload", + }, + { + label: "Product", + operations: [ + { + operationType: "parse", + parse: { + returns: { + array: false, + type: fields.product.type, + }, + source: "$['product']", + }, + }, + ], + reference: "product", + rootReference: "payload", + }, + ], + }, +}) + +const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, { + name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`, + url: alertSource.alertEventsUrl, + secret: alertSource.secretToken, + templates: [ + { + type: "trigger", + body: $jsonStringify({ + title: "{{ .Name }}", + description: "{{ .Description }}", + status: "{{ .Alert.Status }}", + deduplication_key: "{{ .Alert.InstanceID }}", + source_url: "{{ .Result.URL }}", + model: "{{ .Vars.model }}", + product: "{{ .Vars.product }}", + }), + }, + ], + variables: [ + { + name: "model", + }, + { + name: "product", + }, + ], +}) + +new incident.AlertRoute("HoneycombAlertRoute", { + name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`, + enabled: true, + isPrivate: false, + alertSources: [ + { + alertSourceId: alertSource.id, + conditionGroups: [ + { + conditions: [ + { + subject: "alert.title", + operation: "is_set", + paramBindings: [], + }, + ], + }, + ], + }, + ], + conditionGroups: [ + { + conditions: [ + { + subject: "alert.title", + operation: "is_set", + paramBindings: [], + }, + ], + }, + ], + expressions: [], + escalationConfig: { + autoCancelEscalations: true, + escalationTargets: [], + }, + incidentConfig: { + autoDeclineEnabled: true, + enabled: true, + conditionGroups: [], + deferTimeSeconds: 0, + groupingKeys: [ + { + reference: $interpolate`alert.attributes.${fields.model.id}`, + }, + { + reference: $interpolate`alert.attributes.${fields.product.id}`, + }, + ], + groupingWindowSeconds: 900, + }, + incidentTemplate: { + name: { + value: { + literal: varSpec("Alert -> Title", "alert.title"), + }, + }, + summary: { + value: { + literal: varSpec("Alert -> Description", "alert.description"), + }, + }, + startInTriage: { + value: { + literal: "true", + }, + }, + severity: { + mergeStrategy: "first-wins", + }, + incidentMode: { + value: { + literal: $app.stage === "production" ? "standard" : "test", + }, + }, + }, +}) + +type Product = "go" | "zen" + +type Trigger = (opts: { model: string; product: Product }) => { + id: string + title: string + description: string + json: honeycomb.GetQuerySpecificationOutputArgs + threshold: { op: ">=" | "<="; value: number } + baseline: 3600 | 86400 +} + +type Model = { id: string; products: Product[]; triggers: Trigger[] } + +const httpErrors: Trigger = ({ model, product }) => ({ + id: "increased-http-errors", + title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`, + description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`, + json: { + calculations: [ + { + op: "COUNT", + name: "TOTAL", + filterCombination: "AND", + filters: [ + { column: "model", op: "=", value: model }, + { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, + ], + }, + { + op: "COUNT", + name: "FAILED", + filterCombination: "AND", + filters: [ + { column: "model", op: "=", value: model }, + { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, + { column: "status", op: ">=", value: "400" }, + { column: "status", op: "!=", value: "401" }, + ], + }, + ], + formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }], + timeRange: 900, + }, + // Alert when errors surge 50% compared to the previous period + threshold: { op: ">=", value: 50 }, + // What previous time period to evaluate against + baseline: 3600, +}) + +const models: Model[] = [ + { id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] }, + // { id: "glm-5", products: ["go"], triggers: [httpErrors] }, + { id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] }, + { id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] }, + // { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] }, + { id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] }, + // { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] }, + // { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] }, + // { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] }, + { id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] }, + // { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] }, + // { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] }, + { id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] }, + { id: "big-pickle", products: ["zen"], triggers: [httpErrors] }, + // { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] }, + // { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] }, + // { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] }, + // { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] }, + // { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] }, +] + +if ($app.stage !== "production") { + models.splice(1) +} + +for (const model of models) { + for (const product of model.products) { + for (const trigger of model.triggers) { + const spec = trigger({ model: model.id, product }) + + new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), { + name: spec.title, + description: spec.description, + queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json, + alertType: "on_change", + // This is the minimum when using % change detection + frequency: 900, + baselineDetails: [{ type: "percentage", offsetMinutes: spec.baseline / 60 }], + thresholds: [{ ...spec.threshold, exceededLimit: 1 }], + recipients: [ + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [ + { name: "model", value: model.id }, + { name: "product", value: product }, + ], + }, + ], + }, + ], + }) + } + } +} diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 3d07a87cfd..78a4a1fd44 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -31,6 +31,7 @@ "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", + "svix": "1.92.2", "vite": "catalog:", "zod": "catalog:" }, diff --git a/packages/console/app/src/routes/incident/webhook.ts b/packages/console/app/src/routes/incident/webhook.ts new file mode 100644 index 0000000000..3f4aa5f7ce --- /dev/null +++ b/packages/console/app/src/routes/incident/webhook.ts @@ -0,0 +1,75 @@ +import type { APIEvent } from "@solidjs/start/server" +import { Resource } from "@opencode-ai/console-resource" +import { Webhook } from "svix" + +type Incident = { + mode?: "test" | "standard" + name?: string + permalink?: string + summary?: string +} + +type IncidentWebhookPayload = { + event_type?: string + "public_incident.incident_created_v2"?: Incident +} + +const verifyWebhook = async (request: Request) => { + const body = await request.text() + try { + return new Webhook(Resource.INCIDENT_WEBHOOK_SIGNING_SECRET.value).verify( + body, + Object.fromEntries(request.headers.entries()), + ) as IncidentWebhookPayload + } catch { + return undefined + } +} + +const postDiscordMessage = async (incident: Incident) => { + return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: [ + `**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`, + incident.summary, + "", + "@everyone", + "", + incident.permalink, + ] + .filter((line) => line !== undefined) + .join("\n"), + allowed_mentions: { + parse: ["everyone"], + }, + flags: 4, + }), + }) +} + +export async function POST(input: APIEvent) { + const payload = await verifyWebhook(input.request) + if (!payload) { + return Response.json({ message: "invalid signature" }, { status: 401 }) + } + + if (payload.event_type !== "public_incident.incident_created_v2") { + return Response.json({ message: "ignored event" }, { status: 200 }) + } + + const incident = payload["public_incident.incident_created_v2"] + if (!incident) { + return Response.json({ message: "missing incident" }, { status: 400 }) + } + + const response = await postDiscordMessage(incident) + if (!response.ok) { + return Response.json({ message: "discord webhook failed" }, { status: 502 }) + } + + return Response.json({ message: "sent" }, { status: 200 }) +} diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index 288e73d0cb..bc56bd789d 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -35,6 +35,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -87,6 +91,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "R2AccessKey": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 288e73d0cb..bc56bd789d 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -35,6 +35,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -87,6 +91,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "R2AccessKey": { "type": "sst.sst.Secret" "value": string diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 288e73d0cb..bc56bd789d 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -35,6 +35,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -87,6 +91,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "R2AccessKey": { "type": "sst.sst.Secret" "value": string diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index 288e73d0cb..bc56bd789d 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -35,6 +35,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -87,6 +91,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "R2AccessKey": { "type": "sst.sst.Secret" "value": string diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 288e73d0cb..bc56bd789d 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -35,6 +35,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -87,6 +91,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "R2AccessKey": { "type": "sst.sst.Secret" "value": string diff --git a/sst-env.d.ts b/sst-env.d.ts index bb6287a157..52702acd7c 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -50,6 +50,10 @@ declare module "sst" { "type": "sst.cloudflare.SolidStart" "url": string } + "DISCORD_INCIDENT_WEBHOOK_URL": { + "type": "sst.sst.Secret" + "value": string + } "DISCORD_SUPPORT_BOT_TOKEN": { "type": "sst.sst.Secret" "value": string @@ -110,6 +114,10 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } + "INCIDENT_WEBHOOK_SIGNING_SECRET": { + "type": "sst.sst.Secret" + "value": string + } "LogProcessor": { "type": "sst.cloudflare.Worker" } diff --git a/sst.config.ts b/sst.config.ts index b8e56473bc..a7e513ca0a 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -12,6 +12,14 @@ export default $config({ apiKey: process.env.STRIPE_SECRET_KEY!, }, planetscale: "0.4.1", + honeycomb: { + version: "0.49.0", + apiKey: process.env.HONEYCOMB_API_KEY!, + }, + incident: { + version: "5.35.0", + apiKey: process.env.INCIDENT_API_KEY!, + }, }, } }, @@ -19,5 +27,8 @@ export default $config({ await import("./infra/app.js") await import("./infra/console.js") await import("./infra/enterprise.js") + if ($app.stage === "production") { + await import("./infra/monitoring.js") + } }, }) From fdb4b7c4a53eb75fa87d3d5f547ae6182bdca026 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 14:27:06 +0000 Subject: [PATCH 02/70] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 441d0de8d9..dc4ab9a32e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-YBTnGuKDthi9wM4UrY0CMNqAzwnM6rN5XROyJOqYbQ8=", - "aarch64-linux": "sha256-7B1dxtYOc9t+e3lzF8O02YtjsvogyuZjHSanWw1XPio=", - "aarch64-darwin": "sha256-y0CzJRL4WHMxVbZPg3O7Dd+66TbITJbiv0oqhZ6URWw=", - "x86_64-darwin": "sha256-KW0Cx/ddKM4sQcpKhKwYu8qL6zYlm12kcUlgp66Wf50=" + "x86_64-linux": "sha256-Oo27Xkoo5HOzLaRs7FmSobzb1SNyidKIqk1+/BWtcqg=", + "aarch64-linux": "sha256-/d3ukZERWvV7egmc2Rtxg5vroZaXkCs7yVcIjIa4CUE=", + "aarch64-darwin": "sha256-1CX6n+9Wo2vAuPLekGsdjByReHQBbpKHwuK3L7Pfous=", + "x86_64-darwin": "sha256-Jqx3LDSoLSy8em7c/455xLEy9Pn4DmoYLHDemA1i+9w=" } } From 576480b5dc3645d97d5418795647751ad4a48309 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 5 May 2026 10:34:20 -0500 Subject: [PATCH 03/70] fix: ensure mistral medium 3.5 has variants properly setup (#25887) --- packages/opencode/src/provider/transform.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 2fa7649c75..e2aafe4d12 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -761,7 +761,12 @@ export function variants(model: Provider.Model): Record mistralId.includes(id))) return {} return { From 25ecf0af6b8a022d284f9a5a9e9155ced6a37041 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 5 May 2026 10:39:25 -0500 Subject: [PATCH 04/70] fix: retry server_is_overloaded errors (#25888) --- packages/opencode/src/provider/error.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 3877dcb7f3..7363b5ce59 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -151,6 +151,7 @@ export function parseStreamError(input: unknown): ParsedStreamError | undefined isRetryable: false, responseBody, } + case "server_is_overloaded": case "server_error": return { type: "api_error", From 8a797ed9a1f7cbeea2c69e5ce7ab7894430e1c64 Mon Sep 17 00:00:00 2001 From: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Date: Tue, 5 May 2026 21:51:40 +0200 Subject: [PATCH 05/70] fix(TUI): update agent create target path from "/agent" to "/agents" (#14427) --- packages/opencode/src/cli/cmd/agent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index 2026d82324..60526a6200 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -84,7 +84,7 @@ const AgentCreateCommand = effectCmd({ // Determine scope/path let targetPath: string if (cliPath) { - targetPath = path.join(cliPath, "agent") + targetPath = path.join(cliPath, "agents") } else { let scope: "global" | "project" = "global" if (project.vcs === "git") { @@ -106,7 +106,7 @@ const AgentCreateCommand = effectCmd({ if (prompts.isCancel(scopeResult)) throw new UI.CancelledError() scope = scopeResult } - targetPath = path.join(scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"), "agent") + targetPath = path.join(scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"), "agents") } // Get description From 8e182c77829c2590bb64d6ee9e8b4adb05f74817 Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 5 May 2026 15:53:05 -0400 Subject: [PATCH 06/70] fix(core): better state handling of editor context (#25911) --- .../cli/cmd/tui/component/prompt/index.tsx | 23 ++-- .../src/cli/cmd/tui/context/editor.ts | 44 +++++-- .../opencode/src/cli/cmd/tui/routes/home.tsx | 8 +- .../test/cli/tui/editor-context.test.tsx | 116 +++++++++++++----- 4 files changed, 140 insertions(+), 51 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 74332c77be..0ae258c96e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -173,8 +173,7 @@ export function Prompt(props: PromptProps) { if (!file) return return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3)))) }) - const [editorContextHover, setEditorContextHover] = createSignal(false) - let lastSubmittedEditorSelectionKey: string | undefined + const editorContextLabelState = createMemo(() => editor.labelState()) const [auto, setAuto] = createSignal() const [workspaceSelection, setWorkspaceSelection] = createSignal() const [workspaceCreating, setWorkspaceCreating] = createSignal(false) @@ -916,9 +915,8 @@ export function Prompt(props: PromptProps) { // Capture mode before it gets reset const currentMode = store.mode const editorSelection = editorContext() - const currentEditorSelectionKey = editorSelectionKey(editorSelection) const editorParts = - editorSelection && currentEditorSelectionKey !== lastSubmittedEditorSelectionKey + editorSelection && editor.labelState() === "pending" ? [ { id: PartID.ascending(), @@ -996,7 +994,7 @@ export function Prompt(props: PromptProps) { ], }) .catch(() => {}) - lastSubmittedEditorSelectionKey = currentEditorSelectionKey + if (editorParts.length > 0) editor.markSelectionSent() } history.append({ ...store.prompt, @@ -1011,13 +1009,15 @@ export function Prompt(props: PromptProps) { props.onSubmit?.() // temporary hack to make sure the message is sent - if (!props.sessionID) + if (!props.sessionID) { + if (editorParts.length > 0) editor.preserveSelectionFromNewSession() setTimeout(() => { route.navigate({ type: "session", sessionID, }) }, 50) + } input.clear() return true } @@ -1608,16 +1608,9 @@ export function Prompt(props: PromptProps) { - + {(file) => ( - setEditorContextHover(true)} - onMouseOut={() => setEditorContextHover(false)} - onMouseUp={dismissEditorContext} - > - {editorContextHover() ? `x ${file()}` : file()} - + {file()} )} diff --git a/packages/opencode/src/cli/cmd/tui/context/editor.ts b/packages/opencode/src/cli/cmd/tui/context/editor.ts index 06dd6fd042..6d9e04cf84 100644 --- a/packages/opencode/src/cli/cmd/tui/context/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/context/editor.ts @@ -87,6 +87,7 @@ const EditorServerInfoSchema = z.object({ type JsonRpcMessage = z.infer export type EditorSelection = z.infer export type EditorMention = z.infer +export type EditorLabelState = "pending" | "sent" | "none" type EditorServerInfo = z.infer type EditorConnection = { @@ -111,10 +112,12 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const [store, setStore] = createStore<{ status: "disabled" | "connecting" | "connected" selection: EditorSelection | undefined + selectionSent: boolean server: EditorServerInfo | undefined }>({ status: "disabled", selection: undefined, + selectionSent: false, server: undefined, }) @@ -126,8 +129,24 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create let zedSelection: Promise | undefined let lastZedSelectionKey: string | undefined let directory = process.cwd() + let preserveSelectionOnReconnect = false const pending = new Map() + const setSelection = (selection: EditorSelection | undefined) => { + const changed = editorSelectionKey(selection) !== editorSelectionKey(store.selection) + setStore("selection", selection) + if (changed) setStore("selectionSent", false) + } + + const clearSelectionForReconnect = (options?: { resetZedSelectionKey?: boolean }) => { + if (preserveSelectionOnReconnect) { + preserveSelectionOnReconnect = false + return + } + if (options?.resetZedSelectionKey) lastZedSelectionKey = undefined + setSelection(undefined) + } + const send = (payload: JsonRpcMessage) => { if (!socket || socket.readyState !== 1) return socket.send(JSON.stringify({ jsonrpc: "2.0", ...payload })) @@ -158,7 +177,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const key = editorSelectionKey(selection) if (key !== lastZedSelectionKey) { lastZedSelectionKey = key - setStore("selection", selection) + setSelection(selection) setStore("status", selection ? "connected" : "disabled") } }) @@ -198,7 +217,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const selection = message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined if (selection?.success) { - setStore("selection", { ...selection.data, source: "websocket" }) + setSelection({ ...selection.data, source: "websocket" }) return } @@ -252,12 +271,13 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const reconnectWithDirectory = (nextDirectory?: string) => { const resolved = nextDirectory || process.cwd() - if (directory === resolved) return + const sameDirectory = directory === resolved + clearSelectionForReconnect({ resetZedSelectionKey: !sameDirectory }) + if (sameDirectory) return directory = resolved attempt = 0 pending.clear() - lastZedSelectionKey = undefined if (reconnect) clearTimeout(reconnect) reconnect = undefined if (socket) { @@ -266,7 +286,6 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create current.close() } setStore("status", "disabled") - setStore("selection", undefined) setStore("server", undefined) connect() } @@ -293,7 +312,19 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create }, clearSelection() { lastZedSelectionKey = undefined - setStore("selection", undefined) + zedSelection = undefined + setSelection(undefined) + }, + preserveSelectionFromNewSession() { + preserveSelectionOnReconnect = true + }, + markSelectionSent() { + if (!store.selection) return + setStore("selectionSent", true) + }, + labelState(): EditorLabelState { + if (!store.selection) return "none" + return store.selectionSent ? "sent" : "pending" }, onMention(listener: (mention: EditorMention) => void) { mentionListeners.add(listener) @@ -303,7 +334,6 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create return store.server }, reconnect(directory?: string) { - setStore("selection", undefined) reconnectWithDirectory(directory) }, } diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 4c1cd1babd..43a52082be 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,5 +1,5 @@ import { Prompt, type PromptRef } from "@tui/component/prompt" -import { createEffect, createSignal } from "solid-js" +import { createEffect, createSignal, onMount } from "solid-js" import { Logo } from "../component/logo" import { useProject } from "../context/project" import { useSync } from "../context/sync" @@ -9,6 +9,7 @@ import { useRouteData } from "@tui/context/route" import { usePromptRef } from "../context/prompt" import { useLocal } from "../context/local" import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" +import { useEditorContext } from "@tui/context/editor" let once = false const placeholder = { @@ -24,8 +25,13 @@ export function Home() { const [ref, setRef] = createSignal() const args = useArgs() const local = useLocal() + const editor = useEditorContext() let sent = false + onMount(() => { + editor.clearSelection() + }) + const bind = (r: PromptRef | undefined) => { setRef(r) promptRef.set(r) diff --git a/packages/opencode/test/cli/tui/editor-context.test.tsx b/packages/opencode/test/cli/tui/editor-context.test.tsx index 14dead86ac..2c5aa7fa6c 100644 --- a/packages/opencode/test/cli/tui/editor-context.test.tsx +++ b/packages/opencode/test/cli/tui/editor-context.test.tsx @@ -59,6 +59,39 @@ function createWebSocketImpl(...sockets: FakeWebSocket[]) { } as unknown as typeof WebSocket } +function sendSelection(socket: FakeWebSocket, filePath: string, text = "foo") { + socket.message( + JSON.stringify({ + jsonrpc: "2.0", + method: "selection_changed", + params: { + text, + filePath, + selection: { + start: { line: 1, character: 1 }, + end: { line: 1, character: 4 }, + }, + }, + }), + ) +} + +function expectedSelection(filePath: string, text = "foo") { + return { + filePath, + source: "websocket" as const, + ranges: [ + { + text, + selection: { + start: { line: 1, character: 1 }, + end: { line: 1, character: 4 }, + }, + }, + ], + } +} + test("useEditorContext reconnect switches editor server by session directory", async () => { await using tmp = await tmpdir() const startupDirectory = path.join(tmp.path, "startup") @@ -93,12 +126,18 @@ test("useEditorContext reconnect switches editor server by session directory", a await nextTick() expect(firstSocket.closed).toBeFalse() + sendSelection(firstSocket, path.join(startupDirectory, "file.ts")) + + expect(mounted.editor.selection()).toEqual(expectedSelection(path.join(startupDirectory, "file.ts"))) + expect(mounted.editor.labelState()).toBe("pending") mounted.editor.reconnect(sessionDirectory) await nextTick() expect(firstSocket.closed).toBeTrue() expect(secondSocket.closed).toBeFalse() + expect(mounted.editor.selection()).toBeUndefined() + expect(mounted.editor.labelState()).toBe("none") mounted.dispose() }) @@ -131,7 +170,7 @@ test("useEditorContext favors configured port over lock files", async () => { mounted.dispose() }) -test("useEditorContext resets selection when reconnecting", async () => { +test("useEditorContext clears selection when reconnecting", async () => { await using tmp = await tmpdir() const startupDirectory = path.join(tmp.path, "startup") const ideDirectory = path.join(tmp.path, ".claude", "ide") @@ -169,45 +208,66 @@ test("useEditorContext resets selection when reconnecting", async () => { }, }), ) - socket.message( - JSON.stringify({ - jsonrpc: "2.0", - method: "selection_changed", - params: { - text: "foo", - filePath: path.join(startupDirectory, "file.ts"), - selection: { - start: { line: 1, character: 1 }, - end: { line: 1, character: 4 }, - }, - }, - }), - ) + sendSelection(socket, path.join(startupDirectory, "file.ts")) expect(mounted.editor.connected()).toBeTrue() expect(mounted.editor.server()).toEqual({ protocolVersion: "2025-11-25", serverInfo: { name: "test", version: "0.0.0" }, }) - expect(mounted.editor.selection()).toEqual({ - filePath: path.join(startupDirectory, "file.ts"), - source: "websocket", - ranges: [ - { - text: "foo", - selection: { - start: { line: 1, character: 1 }, - end: { line: 1, character: 4 }, - }, - }, - ], - }) + expect(mounted.editor.selection()).toEqual(expectedSelection(path.join(startupDirectory, "file.ts"))) + expect(mounted.editor.labelState()).toBe("pending") + mounted.editor.markSelectionSent() + expect(mounted.editor.labelState()).toBe("sent") mounted.editor.reconnect(startupDirectory) expect(socket.closed).toBeFalse() expect(mounted.editor.connected()).toBeTrue() expect(mounted.editor.selection()).toBeUndefined() + expect(mounted.editor.labelState()).toBe("none") + + mounted.dispose() +}) + +test("useEditorContext preserves selection for the next reconnect when requested", async () => { + await using tmp = await tmpdir() + const startupDirectory = path.join(tmp.path, "startup") + const ideDirectory = path.join(tmp.path, ".claude", "ide") + await mkdir(startupDirectory, { recursive: true }) + await mkdir(ideDirectory, { recursive: true }) + await writeFile( + path.join(ideDirectory, "3001.lock"), + JSON.stringify({ + transport: "ws", + workspaceFolders: [startupDirectory], + }), + ) + + process.env.CLAUDE_CODE_SSE_PORT = undefined + process.env.OPENCODE_EDITOR_SSE_PORT = undefined + spyOn(process, "cwd").mockImplementation(() => startupDirectory) + spyOn(os, "homedir").mockImplementation(() => tmp.path) + const socket = new FakeWebSocket("ws://127.0.0.1:3001") + + const mounted = mountEditorContext(createWebSocketImpl(socket)) + await nextTick() + + sendSelection(socket, path.join(startupDirectory, "file.ts")) + expect(mounted.editor.selection()).toEqual(expectedSelection(path.join(startupDirectory, "file.ts"))) + + mounted.editor.markSelectionSent() + mounted.editor.preserveSelectionFromNewSession() + mounted.editor.reconnect(startupDirectory) + + expect(socket.closed).toBeFalse() + expect(mounted.editor.selection()).toEqual(expectedSelection(path.join(startupDirectory, "file.ts"))) + expect(mounted.editor.labelState()).toBe("sent") + + mounted.editor.reconnect(startupDirectory) + + expect(mounted.editor.selection()).toBeUndefined() + expect(mounted.editor.labelState()).toBe("none") mounted.dispose() }) From 12f3d1f5052092d0f6ad5a22b7e06c5ffa6b3840 Mon Sep 17 00:00:00 2001 From: James Long Date: Tue, 5 May 2026 16:39:37 -0400 Subject: [PATCH 07/70] fix(core): use current workspace with /new; fix warping into local project (#25894) --- .../tui/component/dialog-workspace-create.tsx | 4 ++-- .../src/cli/cmd/tui/component/prompt/index.tsx | 17 ++++++++++++++--- .../server/routes/instance/httpapi/public.ts | 10 ++++++++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index ad40637575..e372c59b99 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -129,11 +129,11 @@ export function DialogWorkspaceSelect(props: { .toSorted((a, b) => b.time.updated - a.time.updated) .flatMap((session) => (session.workspaceID ? [session.workspaceID] : [])) .filter((workspaceID, index, list) => list.indexOf(workspaceID) === index) - .slice(0, 3) .flatMap((workspaceID) => { const workspace = project.workspace.get(workspaceID) - return workspace ? [workspace] : [] + return workspace && project.workspace.status(workspace.id) === "connected" ? [workspace] : [] }) + .slice(0, 3) return [ ...list.map((adapter) => ({ title: adapter.name, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 0ae258c96e..41e32539ee 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -181,6 +181,7 @@ export function Prompt(props: PromptProps) { const [warpNotice, setWarpNotice] = createSignal() const currentProviderLabel = createMemo(() => local.model.parsed().provider) const hasRightContent = createMemo(() => Boolean(props.right)) + const defaultWorkspaceID = createMemo(() => props.workspaceID ?? project.workspace.current()) function selectWorkspace(selection: WorkspaceSelection | undefined) { setWorkspaceSelection(selection) @@ -860,14 +861,14 @@ export function Prompt(props: PromptProps) { if (sessionID == null) { const workspace = workspaceSelection() const workspaceID = iife(() => { - if (!workspace) return undefined + if (!workspace) return defaultWorkspaceID() if (workspace.type === "none") return undefined if (workspace.type === "existing") return workspace.workspaceID return undefined }) const res = await sdk.client.session.create({ - workspace: props.workspaceID, + workspace: workspaceID, agent: agent.name, model: { providerID: selectedModel.providerID, @@ -1145,7 +1146,17 @@ export function Prompt(props: PromptProps) { | undefined >(() => { const selected = workspaceSelection() - if (!selected) return + if (!selected) { + const workspaceID = defaultWorkspaceID() + if (props.sessionID || !workspaceID) return + const workspace = project.workspace.get(workspaceID) + return { + type: "existing", + workspaceType: workspace?.type ?? "unknown", + workspaceName: workspace?.name ?? workspaceID, + status: project.workspace.status(workspaceID) ?? "error", + } + } if (selected.type === "none") return if (props.sessionID && !workspaceCreating()) return if (selected.type === "new") { diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index c9668336ae..b2ac719a2a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -146,6 +146,16 @@ function matchLegacyOpenApi(input: Record) { if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] } if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] } } + if (path === "/experimental/workspace/warp" && method === "post") { + const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace( + "#/components/schemas/", + "", + ) + const properties = ref + ? spec.components?.schemas?.[ref]?.properties + : operation.requestBody.content?.["application/json"]?.schema?.properties + if (properties?.id) properties.id = { anyOf: [properties.id, { type: "null" }] } + } } for (const response of Object.values(operation.responses ?? {})) { for (const content of Object.values(response.content ?? {})) { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index ffc0970c0e..fba70b5bf6 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -1019,7 +1019,7 @@ export class Workspace extends HeyApiClient { parameters?: { directory?: string workspace?: string - id?: string + id?: string | null sessionID?: string }, options?: Options, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7734ca53eb..d8ea6d94e5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -6656,7 +6656,7 @@ export type ExperimentalWorkspaceRemoveResponse = export type ExperimentalWorkspaceWarpData = { body?: { - id: string + id: string | null sessionID: string } path?: never From 25547e9337a9e0c0f6d922f8269d115c21572e7d Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 20:41:12 +0000 Subject: [PATCH 08/70] chore: generate --- packages/sdk/openapi.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index fea9dd5a95..007da60269 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8405,7 +8405,14 @@ "type": "object", "properties": { "id": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "sessionID": { "type": "string" From ca77b8f8e9d59f88c0df626ca4997620f81d2a25 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Wed, 6 May 2026 07:55:08 +1000 Subject: [PATCH 09/70] fix(cf-ai-gateway): route provider options through openaiCompatible key (#24432) (#25573) --- packages/opencode/src/provider/transform.ts | 77 +++++++--- .../test/provider/cf-ai-gateway-e2e.test.ts | 135 ++++++++++++++++++ .../opencode/test/provider/transform.test.ts | 109 ++++++++++++++ 3 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index e2aafe4d12..37a70bca41 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -41,6 +41,13 @@ function sdkKey(npm: string): string | undefined { return "gateway" case "@openrouter/ai-sdk-provider": return "openrouter" + case "ai-gateway-provider": + // ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }), + // and @ai-sdk/openai-compatible parses compatibleOptions from one of + // "openai-compatible" / "openaiCompatible" / "Unified" / "unified". The + // "openai-compatible" key emits a deprecation warning at runtime, so we + // pick the camelCase form the SDK now treats as canonical. + return "openaiCompatible" } return undefined } @@ -427,6 +434,36 @@ export function topK(model: Provider.Model) { const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"] const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"] +// OpenAI rolled out the `none` reasoning_effort tier on this date (Responses API). +// Models released before it 400 on `reasoning_effort: "none"`, so we only expose +// it as a variant for models new enough to accept it. +const OPENAI_NONE_EFFORT_RELEASE_DATE = "2025-11-13" + +// OpenAI rolled out the `xhigh` reasoning_effort tier on this date. Same reasoning. +const OPENAI_XHIGH_EFFORT_RELEASE_DATE = "2025-12-04" + +// Matches members of the gpt-5 family across the id formats we encounter: +// "gpt-5", "gpt-5-nano", "gpt-5.4", "openai/gpt-5.4-codex". +// Anchored to start-of-string or "/" so it doesn't false-match "gpt-50" or "gpt-5o". +const GPT5_FAMILY_RE = /(?:^|\/)gpt-5(?:[.-]|$)/ + +// Computes the reasoning_effort tiers an OpenAI (or OpenAI-compatible upstream +// routed through it, e.g. cf-ai-gateway) model exposes. Returns null for models +// with no tunable effort knob (gpt-5-pro). Effort order: weakest to strongest. +function openaiReasoningEfforts(apiId: string, releaseDate: string): string[] | null { + const id = apiId.toLowerCase() + if (id === "gpt-5-pro" || id === "openai/gpt-5-pro") return null + if (id.includes("codex")) { + if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] + return [...WIDELY_SUPPORTED_EFFORTS] + } + const efforts = [...WIDELY_SUPPORTED_EFFORTS] + if (GPT5_FAMILY_RE.test(id)) efforts.unshift("minimal") + if (releaseDate >= OPENAI_NONE_EFFORT_RELEASE_DATE) efforts.unshift("none") + if (releaseDate >= OPENAI_XHIGH_EFFORT_RELEASE_DATE) efforts.push("xhigh") + return efforts +} + function anthropicAdaptiveEfforts(apiId: string): string[] | null { if (["opus-4-7", "opus-4.7"].some((v) => apiId.includes(v))) { return ["low", "medium", "high", "xhigh", "max"] @@ -476,6 +513,21 @@ export function variants(model: Provider.Model): Record [effort, { reasoning: { effort } }])) + case "ai-gateway-provider": { + // Cloudflare AI Gateway routes every upstream through its OpenAI-compatible + // /v1/compat endpoint, so the body is always OAI-shaped. The gateway + // translates `reasoning_effort` to the upstream provider's native control + // (e.g. Anthropic thinking budgets) when needed. Variants therefore stay + // OAI-style for all upstreams, with an extended effort set for OpenAI + // models that support it. + if (model.api.id.startsWith("openai/")) { + const efforts = openaiReasoningEfforts(model.api.id, model.release_date) + if (!efforts) return {} + return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])) + } + return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) + } + case "@ai-sdk/gateway": if (model.id.includes("anthropic")) { if (adaptiveEfforts) { @@ -595,28 +647,12 @@ export function variants(model: Provider.Model): Record { - if (id.includes("codex")) { - if (id.includes("5.2") || id.includes("5.3")) return [...WIDELY_SUPPORTED_EFFORTS, "xhigh"] - return WIDELY_SUPPORTED_EFFORTS - } - const arr = [...WIDELY_SUPPORTED_EFFORTS] - if (id.includes("gpt-5-") || id === "gpt-5") { - arr.unshift("minimal") - } - if (model.release_date >= "2025-11-13") { - arr.unshift("none") - } - if (model.release_date >= "2025-12-04") { - arr.push("xhigh") - } - return arr - }) + const efforts = openaiReasoningEfforts(model.api.id, model.release_date) + if (!efforts) return {} return Object.fromEntries( - openaiEfforts.map((effort) => [ + efforts.map((effort) => [ effort, { reasoningEffort: effort, @@ -625,6 +661,7 @@ export function variants(model: Provider.Model): Record> + +const realFetch = globalThis.fetch +let captured: Captured | null = null + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +beforeEach(() => { + captured = null + const handle = async ( + input: Parameters[0], + init?: Parameters[1], + ): Promise => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url + if (url.startsWith("https://gateway.ai.cloudflare.com/")) { + const bodyText = typeof init?.body === "string" ? init.body : "" + captured = { url, outerBody: bodyText ? JSON.parse(bodyText) : null } + return new Response( + JSON.stringify({ + id: "chatcmpl-test", + object: "chat.completion", + created: 0, + model: "openai/gpt-5.4", + choices: [{ index: 0, message: { role: "assistant", content: "ok" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ) + } + return realFetch(input, init) + } + // `typeof fetch` includes Bun's `preconnect` method; preserve it from realFetch. + const stubFetch: typeof fetch = Object.assign(handle, { preconnect: realFetch.preconnect.bind(realFetch) }) + globalThis.fetch = stubFetch +}) + +afterEach(() => { + globalThis.fetch = realFetch +}) + +const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ({ + id: ModelID.make(`cloudflare-ai-gateway/${apiId}`), + providerID: ProviderID.make("cloudflare-ai-gateway"), + name: apiId, + api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: "ai-gateway-provider" }, + capabilities: { + reasoning: true, + temperature: false, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 1, output: 1, cache: { read: 0, write: 0 } }, + limit: { context: 1_000_000, output: 128_000 }, + status: "active", + options: {}, + headers: {}, + release_date: releaseDate, +}) + +// ai-gateway-provider sends an array of step descriptors; each entry's `query` +// is the body forwarded to the upstream provider. +function extractUpstreamQuery(body: unknown): Record | undefined { + if (!Array.isArray(body) || body.length === 0) return undefined + const first = body[0] + if (!isRecord(first)) return undefined + const query = first.query + return isRecord(query) ? query : undefined +} + +async function callThroughGateway(apiId: string, providerOptions: ProviderOptions) { + const aigateway = createAiGateway({ accountId: "test", gateway: "test", apiKey: "test" }) + const unified = createUnified() + await generateText({ model: aigateway(unified(apiId)), prompt: "hi", providerOptions }) + return extractUpstreamQuery(captured?.outerBody) +} + +describe("cf-ai-gateway end-to-end (regression: #24432)", () => { + test("ProviderTransform.providerOptions output puts reasoning_effort on the wire", async () => { + // The full chain the runtime exercises: + // transform.providerOptions() -> openaiCompatible key + // -> @ai-sdk/openai-compatible reads it as compatibleOptions + // -> emits body.reasoning_effort + // -> ai-gateway-provider wraps the body and forwards to gateway.ai.cloudflare.com + const opts = ProviderTransform.providerOptions(cfModel("openai/gpt-5.4"), { reasoningEffort: "xhigh" }) + expect(opts).toEqual({ openaiCompatible: { reasoningEffort: "xhigh" } }) + + const upstream = await callThroughGateway("openai/gpt-5.4", opts) + expect(upstream?.reasoning_effort).toBe("xhigh") + }) + + test("variants() output for openai/gpt-5.4 lands xhigh on the wire", async () => { + // The other half of the bug: workflow `variant: xhigh` flows through variants() + // and must reach the wire. variants() returns the providerOptions payload + // unwrapped; providerOptions() wraps it under the SDK key. + const variants = ProviderTransform.variants(cfModel("openai/gpt-5.4")) + expect(variants.xhigh).toEqual({ reasoningEffort: "xhigh" }) + + const opts = ProviderTransform.providerOptions(cfModel("openai/gpt-5.4"), variants.xhigh) + const upstream = await callThroughGateway("openai/gpt-5.4", opts) + expect(upstream?.reasoning_effort).toBe("xhigh") + }) + + test("legacy buggy key 'cloudflare-ai-gateway' does NOT reach the wire (proves the bug)", async () => { + // Sanity: confirms the bug class. If a future change accidentally restores + // providerID-keyed providerOptions, this test fails before users notice. + const upstream = await callThroughGateway("openai/gpt-5.4", { + "cloudflare-ai-gateway": { reasoningEffort: "high" }, + }) + expect(upstream?.reasoning_effort).toBeUndefined() + }) +}) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9b66eaa77c..81b0f9ead6 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2883,6 +2883,36 @@ describe("ProviderTransform.variants", () => { const result = ProviderTransform.variants(model) expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) }) + + test("dotted gpt-5.x ids include 'minimal' (regression: matcher used to miss gpt-5.4)", () => { + const model = createMockModel({ + id: "gpt-5.4", + providerID: "openai", + api: { + id: "gpt-5.4", + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + release_date: "2026-03-05", + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) + }) + + test("gpt-50 (lookalike) does not get gpt-5 family treatment", () => { + const model = createMockModel({ + id: "gpt-50", + providerID: "openai", + api: { + id: "gpt-50", + url: "https://api.openai.com", + npm: "@ai-sdk/openai", + }, + release_date: "2024-01-01", + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + }) }) describe("@ai-sdk/anthropic", () => { @@ -3330,4 +3360,83 @@ describe("ProviderTransform.variants", () => { expect(result).toEqual({}) }) }) + + describe("ai-gateway-provider (cloudflare-ai-gateway)", () => { + const cfModel = (apiId: string, releaseDate = "2024-01-01") => + createMockModel({ + id: `cloudflare-ai-gateway/${apiId}`, + providerID: "cloudflare-ai-gateway", + api: { + id: apiId, + url: "https://gateway.ai.cloudflare.com/v1/compat", + npm: "ai-gateway-provider", + }, + release_date: releaseDate, + }) + + test("openai gpt-5.4 includes xhigh effort (regression: variant=xhigh used to be silently ignored)", () => { + const result = ProviderTransform.variants(cfModel("openai/gpt-5.4", "2026-03-05")) + expect(result.xhigh).toEqual({ reasoningEffort: "xhigh" }) + expect(result.high).toEqual({ reasoningEffort: "high" }) + expect(Object.keys(result)).toContain("minimal") + }) + + test("openai gpt-5.2-codex includes xhigh", () => { + const result = ProviderTransform.variants(cfModel("openai/gpt-5.2-codex", "2025-12-11")) + expect(result.xhigh).toEqual({ reasoningEffort: "xhigh" }) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh"]) + }) + + test("openai gpt-4o (no reasoning) returns empty", () => { + const model = cfModel("openai/gpt-4o") + model.capabilities.reasoning = false + const result = ProviderTransform.variants(model) + expect(result).toEqual({}) + }) + + test("non-openai upstream falls back to widely-supported OAI efforts", () => { + const result = ProviderTransform.variants(cfModel("anthropic/claude-sonnet-4-6")) + expect(result).toEqual({ + low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "medium" }, + high: { reasoningEffort: "high" }, + }) + }) + }) +}) + +describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { + const createModel = (overrides: Partial = {}) => + ({ + id: "cloudflare-ai-gateway/openai/gpt-5.4", + providerID: "cloudflare-ai-gateway", + api: { + id: "openai/gpt-5.4", + url: "https://gateway.ai.cloudflare.com/v1/compat", + npm: "ai-gateway-provider", + }, + capabilities: { + temperature: false, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 1, output: 1, cache: { read: 0, write: 0 } }, + limit: { context: 1_000_000, output: 128_000 }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-03-05", + ...overrides, + }) as any + + test("routes options under openaiCompatible (the key @ai-sdk/openai-compatible reads)", () => { + // Regression: previously fell back to providerID="cloudflare-ai-gateway", + // which @ai-sdk/openai-compatible never reads, silently dropping reasoningEffort. + const result = ProviderTransform.providerOptions(createModel(), { reasoningEffort: "high" }) + expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } }) + }) }) From 837cc92586010dcdeeafd919c7bc166d2fe74c3b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 21:56:18 +0000 Subject: [PATCH 10/70] chore: generate --- packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts index 5fab6a492e..0c692c50c8 100644 --- a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts +++ b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts @@ -27,12 +27,8 @@ function isRecord(value: unknown): value is Record { beforeEach(() => { captured = null - const handle = async ( - input: Parameters[0], - init?: Parameters[1], - ): Promise => { - const url = - typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url + const handle = async (input: Parameters[0], init?: Parameters[1]): Promise => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url if (url.startsWith("https://gateway.ai.cloudflare.com/")) { const bodyText = typeof init?.body === "string" ? init.body : "" captured = { url, outerBody: bodyText ? JSON.parse(bodyText) : null } From 6409aceb1ad732b25865ab8ff52c9f205db75866 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 5 May 2026 18:07:23 -0500 Subject: [PATCH 11/70] fix: sanitize surrogates (#25934) --- packages/opencode/src/provider/transform.ts | 69 +++++++++++- .../opencode/test/provider/transform.test.ts | 100 +++++++++++++++++- 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 37a70bca41..cd29e40822 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,4 +1,4 @@ -import type { ModelMessage } from "ai" +import type { ModelMessage, ToolResultPart } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" import type { JSONSchema } from "zod/v4/core" @@ -19,6 +19,10 @@ function mimeToModality(mime: string): Modality | undefined { export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000 +export function sanitizeSurrogates(content: string) { + return content.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?, ): ModelMessage[] { + const sanitizeToolResultOutput = (content: ToolResultPart) => { + if (content.output.type === "text" || content.output.type === "error-text") { + content.output.value = sanitizeSurrogates(content.output.value) + } + if (content.output.type === "content") { + content.output.value = content.output.value.map((item) => { + if (item.type === "text") { + item.text = sanitizeSurrogates(item.text) + } + return item + }) + } + return content + } + + msgs = msgs.map((msg) => { + switch (msg.role) { + case "tool": + if (!Array.isArray(msg.content)) return msg + msg.content = msg.content.map((content) => { + if (content.type === "tool-result") { + return sanitizeToolResultOutput(content) + } + return content + }) + return msg + + case "system": + msg.content = sanitizeSurrogates(msg.content) + return msg + + case "user": + if (typeof msg.content === "string") { + msg.content = sanitizeSurrogates(msg.content) + } else { + msg.content = msg.content.map((content) => { + if (content.type === "text") { + content.text = sanitizeSurrogates(content.text) + } + return content + }) + } + return msg + + case "assistant": + if (typeof msg.content === "string") { + msg.content = sanitizeSurrogates(msg.content) + } else { + msg.content = msg.content.map((content) => { + if (content.type === "text" || content.type === "reasoning") { + content.text = sanitizeSurrogates(content.text) + } + if (content.type === "tool-result") { + return sanitizeToolResultOutput(content) + } + return content + }) + } + return msg + } + }) + // Anthropic rejects messages with empty content - filter out empty string messages // and remove empty text/reasoning parts from array content if (model.api.npm === "@ai-sdk/anthropic") { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 81b0f9ead6..e022a6d9ad 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1123,6 +1123,98 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { }) }) +describe("ProviderTransform.message - surrogate sanitization", () => { + const model = { + id: "test/test-model", + providerID: "test", + api: { + id: "test-model", + url: "https://api.test.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 128000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + } as any + + test("replaces lone surrogates in model-visible text", () => { + const lone = "\uD83D" + const valid = "🚀" + const sanitized = "�" + const text = (label: string) => `${label} ${lone} and ${valid}` + const expected = (label: string) => `${label} ${sanitized} and ${valid}` + const msgs = [ + { role: "system", content: text("system") }, + { role: "user", content: text("user string") }, + { + role: "user", + content: [ + { type: "text", text: text("user text") }, + { type: "image", image: "data:image/png;base64,abcd" }, + ], + }, + { role: "assistant", content: text("assistant string") }, + { + role: "assistant", + content: [ + { type: "text", text: text("assistant text") }, + { type: "reasoning", text: text("assistant reasoning") }, + { type: "tool-call", toolCallId: "call-1", toolName: "Read", input: { filePath: ".opencode/tool/emoji.ts" } }, + { type: "tool-result", toolCallId: "call-2", toolName: "Read", output: { type: "text", value: text("assistant tool text") } }, + { type: "tool-result", toolCallId: "call-3", toolName: "Read", output: { type: "error-text", value: text("assistant tool error") } }, + { + type: "tool-result", + toolCallId: "call-4", + toolName: "Read", + output: { type: "content", value: [{ type: "text", text: text("assistant tool content") }] }, + }, + ], + }, + { + role: "tool", + content: [ + { type: "tool-result", toolCallId: "call-5", toolName: "Read", output: { type: "text", value: text("tool text") } }, + { type: "tool-result", toolCallId: "call-6", toolName: "Read", output: { type: "error-text", value: text("tool error") } }, + { + type: "tool-result", + toolCallId: "call-7", + toolName: "Read", + output: { type: "content", value: [{ type: "text", text: text("tool content") }] }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].content).toBe(expected("system")) + expect(result[1].content).toBe(expected("user string")) + expect(result[2].content[0].text).toBe(expected("user text")) + expect(result[3].content).toBe(expected("assistant string")) + expect(result[4].content[0].text).toBe(expected("assistant text")) + expect(result[4].content[1].text).toBe(expected("assistant reasoning")) + expect(result[4].content[3].output.value).toBe(expected("assistant tool text")) + expect(result[4].content[4].output.value).toBe(expected("assistant tool error")) + expect(result[4].content[5].output.value[0].text).toBe(expected("assistant tool content")) + expect(result[5].content[0].output.value).toBe(expected("tool text")) + expect(result[5].content[1].output.value).toBe(expected("tool error")) + expect(result[5].content[2].output.value[0].text).toBe(expected("tool content")) + expect(result[2].content[1]).toEqual({ type: "image", image: "data:image/png;base64,abcd" }) + }) +}) + describe("ProviderTransform.message - empty image handling", () => { const mockModel = { id: "anthropic/claude-3-5-sonnet", @@ -1993,7 +2085,7 @@ describe("ProviderTransform.message - bedrock caching with non-bedrock providerI const msgs = [ { role: "system", - content: [{ type: "text", text: "You are a helpful assistant" }], + content: "You are a helpful assistant", }, { role: "user", @@ -2007,7 +2099,7 @@ describe("ProviderTransform.message - bedrock caching with non-bedrock providerI expect(result[0].providerOptions?.bedrock).toEqual({ cachePoint: { type: "default" }, }) - expect(result[0].content[0].providerOptions?.bedrock).toBeUndefined() + expect(result[0].content).toBe("You are a helpful assistant") }) }) @@ -2044,7 +2136,7 @@ describe("ProviderTransform.message - cache control on gateway", () => { const msgs = [ { role: "system", - content: [{ type: "text", text: "You are a helpful assistant" }], + content: "You are a helpful assistant", }, { role: "user", @@ -2054,7 +2146,7 @@ describe("ProviderTransform.message - cache control on gateway", () => { const result = ProviderTransform.message(msgs, model, {}) as any[] - expect(result[0].content[0].providerOptions).toBeUndefined() + expect(result[0].content).toBe("You are a helpful assistant") expect(result[0].providerOptions).toBeUndefined() }) From 1fbc13a1b4957be71a150135f91412082ceae0fe Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 5 May 2026 23:08:37 +0000 Subject: [PATCH 12/70] chore: generate --- .../opencode/test/provider/transform.test.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index e022a6d9ad..c7a321d571 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1172,8 +1172,18 @@ describe("ProviderTransform.message - surrogate sanitization", () => { { type: "text", text: text("assistant text") }, { type: "reasoning", text: text("assistant reasoning") }, { type: "tool-call", toolCallId: "call-1", toolName: "Read", input: { filePath: ".opencode/tool/emoji.ts" } }, - { type: "tool-result", toolCallId: "call-2", toolName: "Read", output: { type: "text", value: text("assistant tool text") } }, - { type: "tool-result", toolCallId: "call-3", toolName: "Read", output: { type: "error-text", value: text("assistant tool error") } }, + { + type: "tool-result", + toolCallId: "call-2", + toolName: "Read", + output: { type: "text", value: text("assistant tool text") }, + }, + { + type: "tool-result", + toolCallId: "call-3", + toolName: "Read", + output: { type: "error-text", value: text("assistant tool error") }, + }, { type: "tool-result", toolCallId: "call-4", @@ -1185,8 +1195,18 @@ describe("ProviderTransform.message - surrogate sanitization", () => { { role: "tool", content: [ - { type: "tool-result", toolCallId: "call-5", toolName: "Read", output: { type: "text", value: text("tool text") } }, - { type: "tool-result", toolCallId: "call-6", toolName: "Read", output: { type: "error-text", value: text("tool error") } }, + { + type: "tool-result", + toolCallId: "call-5", + toolName: "Read", + output: { type: "text", value: text("tool text") }, + }, + { + type: "tool-result", + toolCallId: "call-6", + toolName: "Read", + output: { type: "error-text", value: text("tool error") }, + }, { type: "tool-result", toolCallId: "call-7", From e117397d0ff6c6e529c5cb6b160de0df8bda25c7 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 6 May 2026 09:32:51 +1000 Subject: [PATCH 13/70] fix(server): restore web terminal CSP allowances (#25937) --- packages/opencode/src/server/routes/ui.ts | 19 ++++++----- packages/opencode/src/server/shared/ui.ts | 17 ++++++---- .../opencode/test/server/httpapi-ui.test.ts | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/server/routes/ui.ts b/packages/opencode/src/server/routes/ui.ts index ce06b2b35e..608525b63a 100644 --- a/packages/opencode/src/server/routes/ui.ts +++ b/packages/opencode/src/server/routes/ui.ts @@ -1,10 +1,9 @@ import fs from "node:fs/promises" -import { createHash } from "node:crypto" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Hono } from "hono" import { proxy } from "hono/proxy" import { ProxyUtil } from "../proxy-util" -import { DEFAULT_CSP, UI_UPSTREAM, csp, embeddedUI, themePreloadHash, upstreamURL } from "../shared/ui" +import { UI_UPSTREAM, csp, cspForHtml, embeddedUI, upstreamURL } from "../shared/ui" export async function serveUI(request: Request) { const embeddedWebUI = await embeddedUI() @@ -17,8 +16,11 @@ export async function serveUI(request: Request) { if (await fs.exists(match)) { const mime = AppFileSystem.mimeType(match) const headers = new Headers({ "content-type": mime }) - if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP) - return new Response(new Uint8Array(await fs.readFile(match)), { headers }) + const body = new Uint8Array(await fs.readFile(match)) + if (mime.startsWith("text/html")) { + headers.set("content-security-policy", cspForHtml(new TextDecoder().decode(body))) + } + return new Response(body, { headers }) } return Response.json({ error: "Not Found" }, { status: 404 }) @@ -28,11 +30,10 @@ export async function serveUI(request: Request) { raw: request, headers: ProxyUtil.headers(request, { host: UI_UPSTREAM.host }), }) - const match = response.headers.get("content-type")?.includes("text/html") - ? themePreloadHash(await response.clone().text()) - : undefined - const hash = match ? createHash("sha256").update(match[2]).digest("base64") : "" - response.headers.set("Content-Security-Policy", csp(hash)) + response.headers.set( + "Content-Security-Policy", + response.headers.get("content-type")?.includes("text/html") ? cspForHtml(await response.clone().text()) : csp(), + ) return response } diff --git a/packages/opencode/src/server/shared/ui.ts b/packages/opencode/src/server/shared/ui.ts index 0328663da5..0e27dcf220 100644 --- a/packages/opencode/src/server/shared/ui.ts +++ b/packages/opencode/src/server/shared/ui.ts @@ -10,17 +10,21 @@ const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI : // @ts-expect-error - generated file at build time import("opencode-web-ui.gen.ts").then((module) => module.default as Record).catch(() => null) -export const DEFAULT_CSP = - "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src *" export const UI_UPSTREAM = new URL("https://app.opencode.ai") export const csp = (hash = "") => - `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src *` + `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src * data:` +export const DEFAULT_CSP = csp() export function themePreloadHash(body: string) { return body.match(/]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i) } +export function cspForHtml(body: string) { + const match = themePreloadHash(body) + return csp(match ? createHash("sha256").update(match[2]).digest("base64") : "") +} + function requestBody(request: HttpServerRequest.HttpServerRequest) { if (request.method === "GET" || request.method === "HEAD") return HttpBody.empty const len = request.headers["content-length"] @@ -53,7 +57,9 @@ function notFound() { function embeddedUIResponse(file: string, body: Uint8Array) { const mime = AppFileSystem.mimeType(file) const headers = new Headers({ "content-type": mime }) - if (mime.startsWith("text/html")) headers.set("content-security-policy", DEFAULT_CSP) + if (mime.startsWith("text/html")) { + headers.set("content-security-policy", cspForHtml(new TextDecoder().decode(body))) + } return HttpServerResponse.raw(body, { headers }) } @@ -91,8 +97,7 @@ export function serveUIEffect( if (response.headers["content-type"]?.includes("text/html")) { const body = yield* response.text - const match = themePreloadHash(body) - headers.set("Content-Security-Policy", csp(match ? createHash("sha256").update(match[2]).digest("base64") : "")) + headers.set("Content-Security-Policy", cspForHtml(body)) return HttpServerResponse.text(body, { status: response.status, headers }) } diff --git a/packages/opencode/test/server/httpapi-ui.test.ts b/packages/opencode/test/server/httpapi-ui.test.ts index 85162f6a92..440aeaecb5 100644 --- a/packages/opencode/test/server/httpapi-ui.test.ts +++ b/packages/opencode/test/server/httpapi-ui.test.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto" import { afterEach, describe, expect, test } from "bun:test" import { Flag } from "@opencode-ai/core/flag/flag" import * as Log from "@opencode-ai/core/util/log" @@ -260,6 +261,38 @@ describe("HttpApi UI fallback", () => { expect(await response.text()).toBe("console.log('embedded')") }) + test("allows embedded UI terminal wasm and theme preload CSP", async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + const script = 'document.documentElement.dataset.theme = "dark"' + + const response = await Effect.runPromise( + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + return yield* serveEmbeddedUIEffect( + "/", + { + ...fs, + readFile: (path) => { + return path === "/$bunfs/root/index.html" + ? Effect.succeed( + new TextEncoder().encode( + ``, + ), + ) + : Effect.die(`unexpected embedded UI path: ${path}`) + }, + }, + { "index.html": "/$bunfs/root/index.html" }, + ) + }).pipe(Effect.provide(AppFileSystem.defaultLayer), Effect.map(HttpServerResponse.toWeb)), + ) + + const csp = response.headers.get("content-security-policy") ?? "" + expect(csp).toContain("script-src 'self' 'wasm-unsafe-eval'") + expect(csp).toContain(`'sha256-${createHash("sha256").update(script).digest("base64")}'`) + expect(csp).toContain("connect-src * data:") + }) + test("keeps matched API routes ahead of the UI fallback", async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true From f5c3d352a1532ef55bce09cacc054f79388dcd68 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 6 May 2026 10:09:32 +1000 Subject: [PATCH 14/70] fix(app): require query functions for sync queries (#25939) --- .../app/src/components/dialog-select-mcp.tsx | 4 +- packages/app/src/components/prompt-input.tsx | 8 +- .../src/components/status-popover-body.tsx | 4 +- packages/app/src/context/global-sync.tsx | 30 +++-- .../app/src/context/global-sync/bootstrap.ts | 105 +++++------------- .../src/pages/layout/sidebar-workspace.tsx | 14 +-- 6 files changed, 66 insertions(+), 99 deletions(-) diff --git a/packages/app/src/components/dialog-select-mcp.tsx b/packages/app/src/components/dialog-select-mcp.tsx index 9bb36d32d8..576ec8fec4 100644 --- a/packages/app/src/components/dialog-select-mcp.tsx +++ b/packages/app/src/components/dialog-select-mcp.tsx @@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Switch } from "@opencode-ai/ui/switch" import { useLanguage } from "@/context/language" -import { loadMcpQuery } from "@/context/global-sync" +import { mcpQueryKey } from "@/context/global-sync" const statusLabels = { connected: "mcp.status.connected", @@ -32,7 +32,7 @@ export const DialogSelectMcp: Component = () => { if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name }) else await sdk.client.mcp.connect({ name }) }, - onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }), + onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }), })) const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 0a18096164..2417fa98e2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -16,6 +16,7 @@ import { } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" +import { useGlobalSDK } from "@/context/global-sdk" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" @@ -102,6 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/ export const PromptInput: Component = (props) => { const sdk = useSDK() + const globalSDK = useGlobalSDK() const sync = useSync() const local = useLocal() @@ -1253,7 +1255,11 @@ export const PromptInput: Component = (props) => { } const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({ - queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)], + queries: [ + loadAgentsQuery(sdk.directory, sdk.client), + loadProvidersQuery(null, globalSDK.client), + loadProvidersQuery(sdk.directory, sdk.client), + ], })) const agentsLoading = () => agentsQuery.isLoading diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx index 952e3eac64..bbac562784 100644 --- a/packages/app/src/components/status-popover-body.tsx +++ b/packages/app/src/components/status-popover-body.tsx @@ -15,7 +15,7 @@ import { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" -import { loadMcpQuery } from "@/context/global-sync" +import { mcpQueryKey } from "@/context/global-sync" const pollMs = 10_000 @@ -145,7 +145,7 @@ const useMcpToggleMutation = () => { const status = sync.data.mcp[name] await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) }, - onSuccess: () => queryClient.refetchQueries({ queryKey: loadMcpQuery(sync.directory).queryKey }), + onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }), onError: (err) => { showToast({ variant: "error", diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 6190deb1ee..31c90463d8 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -20,7 +20,6 @@ import { clearProviderRev, loadGlobalConfigQuery, loadPathQuery, - loadProjectsQuery, loadProvidersQuery, } from "./global-sync/bootstrap" import { createChildStoreManager } from "./global-sync/child-store" @@ -31,7 +30,7 @@ import { trimSessions } from "./global-sync/session-trim" import type { ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { formatServerError } from "@/utils/server-errors" -import { queryOptions, skipToken, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" +import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" @@ -49,19 +48,22 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -export const loadSessionsQuery = (directory: string) => - queryOptions({ queryKey: [directory, "loadSessions"], queryFn: skipToken }) +export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const -export const loadMcpQuery = (directory: string, sdk?: OpencodeClient) => +export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const + +export const loadMcpQuery = (directory: string, sdk: OpencodeClient) => queryOptions({ - queryKey: [directory, "mcp"], - queryFn: sdk ? () => sdk.mcp.status().then((r) => r.data ?? {}) : skipToken, + queryKey: mcpQueryKey(directory), + queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}), }) -export const loadLspQuery = (directory: string, sdk?: OpencodeClient) => +export const lspQueryKey = (directory: string) => [directory, "lsp"] as const + +export const loadLspQuery = (directory: string, sdk: OpencodeClient) => queryOptions({ - queryKey: [directory, "lsp"], - queryFn: sdk ? () => sdk.lsp.status().then((r) => r.data ?? []) : skipToken, + queryKey: lspQueryKey(directory), + queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), }) function createGlobalSync() { @@ -76,7 +78,11 @@ function createGlobalSync() { const sessionMeta = new Map() const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ - queries: [loadGlobalConfigQuery(), loadProvidersQuery(null), loadPathQuery(null), loadProjectsQuery()], + queries: [ + loadGlobalConfigQuery(globalSDK.client), + loadProvidersQuery(null, globalSDK.client), + loadPathQuery(null, globalSDK.client), + ], })) const [globalStore, setGlobalStore] = createStore({ @@ -233,7 +239,7 @@ function createGlobalSync() { const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) const promise = queryClient .fetchQuery({ - ...loadSessionsQuery(key), + queryKey: loadSessionsQueryKey(key), queryFn: () => loadRootSessionsWithFallback({ directory, diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index e85516bf14..531917bde6 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -18,7 +18,7 @@ import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" -import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query" +import { QueryClient, queryOptions } from "@tanstack/solid-query" import { loadMcpQuery } from "../global-sync" type GlobalStore = { @@ -83,44 +83,25 @@ function showErrors(input: { }) } -export const loadGlobalConfigQuery = ( - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadGlobalConfigQuery = (sdk: OpencodeClient) => queryOptions({ queryKey: ["config"], - queryFn: sdk - ? () => - retry(() => - sdk.global.config.get().then((x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)), }) -export const loadProjectsQuery = ( - sdk?: OpencodeClient, - transform?: (x: Awaited>["data"]) => void, -) => +export const loadProjectsQuery = (sdk: OpencodeClient) => queryOptions({ queryKey: ["project"], - queryFn: sdk - ? () => - retry(() => - sdk.project - .list() - .then((x) => { - return (x.data ?? []) - .filter((p) => !!p?.id) - .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .slice() - .sort((a, b) => cmp(a.id, b.id)) - }) - .then(transform), - ) - : skipToken, + queryFn: () => + retry(() => + sdk.project.list().then((x) => { + return (x.data ?? []) + .filter((p) => !!p?.id) + .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) + .slice() + .sort((a, b) => cmp(a.id, b.id)) + }), + ), }) export async function bootstrapGlobal(input: { @@ -136,9 +117,9 @@ export async function bootstrapGlobal(input: { () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)), () => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)), () => - input.queryClient.fetchQuery( - loadProjectsQuery(input.globalSDK, (data) => input.setGlobalStore("project", data ?? [])), - ), + input.queryClient + .fetchQuery(loadProjectsQuery(input.globalSDK)) + .then((data) => input.setGlobalStore("project", data)), ] await runAll(slow) // showErrors({ @@ -197,46 +178,22 @@ function warmSessions(input: { ).then(() => undefined) } -export const loadProvidersQuery = (directory: string | null, sdk?: OpencodeClient) => +export const loadProvidersQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "providers"], - queryFn: sdk ? () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))) : skipToken, + queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))), }) -export const loadAgentsQuery = ( - directory: string | null, - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadAgentsQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "agents"], - queryFn: sdk - ? () => - retry(() => - sdk.app.agents().then((x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))), }) -export const loadPathQuery = ( - directory: string | null, - sdk?: OpencodeClient, - transform?: (x: Awaited>) => void, -) => +export const loadPathQuery = (directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [directory, "path"], - queryFn: sdk - ? () => - retry(() => - sdk.path.get().then(async (x) => { - transform?.(x) - return x.data! - }), - ) - : skipToken, + queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)), }) export async function bootstrapDirectory(input: { @@ -271,9 +228,9 @@ export async function bootstrapDirectory(input: { const slow = [ () => Promise.resolve(input.loadSessions(input.directory)), () => - input.queryClient.ensureQueryData( - loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))), - ), + input.queryClient + .ensureQueryData(loadAgentsQuery(input.directory, input.sdk)) + .then((data) => input.setStore("agent", data)), () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))), () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))), @@ -281,12 +238,10 @@ export async function bootstrapDirectory(input: { (() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))), !seededPath && (() => - input.queryClient.ensureQueryData( - loadPathQuery(input.directory, input.sdk, (x) => { - const next = projectID(x.data?.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - }), - )), + input.queryClient.ensureQueryData(loadPathQuery(input.directory, input.sdk)).then((data) => { + const next = projectID(data.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + })), () => retry(() => input.sdk.vcs.get().then((x) => { diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index d2e887b444..9b80adac29 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -14,12 +14,12 @@ import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" import { type Session } from "@opencode-ai/sdk/v2/client" import { type LocalProject } from "@/context/layout" -import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync" +import { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" import { pathKey } from "@/utils/path-key" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { sortedRootSessions } from "./helpers" -import { useQuery } from "@tanstack/solid-query" +import { useIsFetching } from "@tanstack/solid-query" type InlineEditorComponent = (props: { id: string @@ -320,9 +320,9 @@ export const SortableWorkspace = (props: { const boot = createMemo(() => open() || active()) const count = createMemo(() => sessions()?.length ?? 0) const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) + const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) })) const busy = createMemo(() => props.ctx.isBusy(props.directory)) - const loading = () => query.isLoading && count() === 0 + const loading = () => fetching() > 0 && count() === 0 const touch = createMediaQuery("(hover: none)") const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const loadMore = async () => { @@ -427,7 +427,7 @@ export const SortableWorkspace = (props: { mobile={props.mobile} ctx={props.ctx} showNew={showNew} - loading={() => query.isLoading && count() === 0} + loading={loading} sessions={sessions} hasMore={hasMore} loadMore={loadMore} @@ -454,9 +454,9 @@ export const LocalWorkspace = (props: { const slug = createMemo(() => base64Encode(props.project.worktree)) const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const count = createMemo(() => sessions()?.length ?? 0) - const query = useQuery(() => ({ ...loadSessionsQuery(props.project.worktree) })) + const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) })) const hasMore = createMemo(() => workspace().store.sessionTotal > count()) - const loading = () => query.isLoading && count() === 0 + const loading = () => fetching() > 0 && count() === 0 const loadMore = async () => { workspace().setStore("limit", (limit) => (limit ?? 0) + 5) await globalSync.project.loadSessions(props.project.worktree) From 8555de81895845eb8572d4e4640f863f77059027 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 5 May 2026 21:33:47 -0400 Subject: [PATCH 15/70] Type session not-found errors (#25818) --- packages/opencode/specs/effect/errors.md | 329 ++++++++++++++++++ packages/opencode/src/cli/cmd/session.ts | 7 +- .../opencode/src/control-plane/workspace.ts | 17 +- .../server/routes/instance/httpapi/AGENTS.md | 2 + .../server/routes/instance/httpapi/errors.ts | 18 + .../routes/instance/httpapi/groups/pty.ts | 9 +- .../routes/instance/httpapi/groups/session.ts | 18 +- .../routes/instance/httpapi/groups/tui.ts | 3 +- .../routes/instance/httpapi/handlers/pty.ts | 7 +- .../httpapi/handlers/session-errors.ts | 9 + .../instance/httpapi/handlers/session.ts | 105 +++--- .../routes/instance/httpapi/handlers/tui.ts | 13 +- .../httpapi/middleware/workspace-routing.ts | 6 +- packages/opencode/src/session/prompt.ts | 8 +- packages/opencode/src/session/revert.ts | 14 +- packages/opencode/src/session/session.ts | 15 +- .../test/control-plane/workspace.test.ts | 4 +- .../test/server/httpapi-parity.test.ts | 23 +- .../opencode/test/server/httpapi-pty.test.ts | 18 +- .../opencode/test/server/httpapi-sdk.test.ts | 69 +++- .../test/server/httpapi-session.test.ts | 50 ++- .../opencode/test/server/httpapi-tui.test.ts | 15 +- 22 files changed, 625 insertions(+), 134 deletions(-) create mode 100644 packages/opencode/specs/effect/errors.md create mode 100644 packages/opencode/src/server/routes/instance/httpapi/errors.ts create mode 100644 packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts diff --git a/packages/opencode/specs/effect/errors.md b/packages/opencode/specs/effect/errors.md new file mode 100644 index 0000000000..746e658693 --- /dev/null +++ b/packages/opencode/specs/effect/errors.md @@ -0,0 +1,329 @@ +# Typed error migration + +Plan for moving `packages/opencode` from temporary defect/`NamedError` +compatibility toward typed Effect service errors and explicit HTTP error +contracts. + +## Goal + +- Expected service failures live on the Effect error channel. +- Service interfaces expose those failures in their return types. +- Domain errors are authored with Effect Schema so they are reusable by services, + tests, HTTP routes, tools, and OpenAPI generation. +- HTTP status codes and wire compatibility are handled at the HTTP boundary, not + inside service modules. +- `Effect.die`, `throw`, `catchDefect`, and global cause inspection are reserved + for defects, compatibility bridges, or final fallback behavior. + +## Current State + +- Many migrated services use Effect internally, but expected failures are still a + mix of `NamedError.create(...)`, `namedSchemaError(...)`, `class extends Error`, + `throw`, and `Effect.die(...)`. +- Some services already use `Schema.TaggedErrorClass`, for example `Account`, + `Auth`, `Permission`, `Question`, `Installation`, and parts of + `Workspace`. +- Legacy Hono error handling recognizes `NamedError`, `Session.BusyError`, and a + few name-based cases, then emits the legacy `{ name, data }` JSON body. +- Effect `HttpApi` only knows how to encode errors that are declared on the + endpoint, group, or middleware. Undeclared expected errors become defects and + eventually fall through to generic HTTP handling. +- The temporary HttpApi error middleware catches defect-wrapped legacy errors to + preserve runtime behavior, but it is intentionally a bridge rather than the + final model. + +## End State + +Service modules own domain failures. + +```ts +export class SessionBusyError extends Schema.TaggedErrorClass()("SessionBusyError", { + sessionID: SessionID, + message: Schema.String, +}) {} + +export type Error = Storage.Error | SessionBusyError + +export interface Interface { + readonly get: (id: SessionID) => Effect.Effect +} +``` + +HTTP modules own transport mapping. + +```ts +const get = Effect.fn("SessionHttpApi.get")(function* (ctx: { params: { sessionID: SessionID } }) { + return yield* session + .get(ctx.params.sessionID) + .pipe( + Effect.catchTag("StorageNotFoundError", () => new SessionNotFoundHttpError({ sessionID: ctx.params.sessionID })), + ) +}) +``` + +HTTP-visible error schemas carry their own response status through Effect +HttpApi's `httpApiStatus` annotation. Prefer `HttpApiSchema.status(...)`, or the +equivalent declaration annotation, instead of maintaining a parallel status map. + +```ts +export class SessionNotFoundHttpError extends Schema.TaggedErrorClass()( + "SessionNotFoundHttpError", + { + sessionID: SessionID, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} +``` + +Endpoint definitions still declare which HTTP-visible error schemas can be +emitted. The status annotation is only used if the error is part of the endpoint, +group, or middleware error schema and the handler fails with that error on the +typed error channel. + +```ts +HttpApiEndpoint.get("get", SessionPaths.get, { + success: Session.Info, + error: [SessionNotFoundHttpError, SessionBusyHttpError], +}) +``` + +The service error and HTTP error may be the same class when the wire shape is a +deliberate public contract. They should be different classes when the service +error contains internals, low-level causes, retry hints, or anything that should +not be exposed to API clients. + +## Rules + +- Use `Schema.TaggedErrorClass` for new expected domain errors. +- Include `cause: Schema.optional(Schema.Defect)` only when preserving an + underlying unknown failure is useful for logs or callers. +- Export a domain-level error union from each service module, for example + `export type Error = NotFoundError | BusyError | Storage.Error`. +- Put expected errors in service method signatures, for example + `Effect.Effect`. +- Use `yield* new DomainError(...)` for direct early failures inside + `Effect.gen` / `Effect.fn`. +- Use `Effect.try({ try, catch })`, `Effect.mapError`, or `Effect.catchTag` to + convert external exceptions into domain errors. +- Use `HttpApiSchema.status(...)` or `{ httpApiStatus: code }` on HTTP-visible + error schemas so Effect `HttpApiBuilder` and OpenAPI generation get the status + from the schema itself. +- Do not use `Effect.die(...)` for user, IO, validation, missing-resource, auth, + provider, worktree, or busy-state failures. +- Do not use `catchDefect` to recover expected domain errors. If recovery is + needed, the upstream effect should fail with a typed error instead. +- Do not make service modules import `HttpApiError`, `HttpServerResponse`, HTTP + status codes, or route-specific error schemas. +- Keep raw `HttpRouter` routes free to use `HttpServerRespondable` when that is + the right transport abstraction, but prefer declared `HttpApi` errors for + normal JSON API endpoints. + +## HTTP Boundary Shape + +Create an HttpApi-local error module, likely +`src/server/routes/instance/httpapi/errors.ts`. + +That module should provide: + +- Legacy-compatible public schemas for `{ name, data }` error bodies that must + remain SDK-compatible during the Hono migration. +- Small constructors or mapping helpers for common API errors such as not found, + bad request, conflict, and unknown internal errors. +- Route-group-specific adapters only when they encode domain-specific public + data. +- A single place to document which public error shape is legacy-compatible and + which shape is new Effect-native API surface. + +Avoid one giant `unknown -> status` mapper. Prefer small, explicit mappers close +to the handler or route group. + +```ts +const mapSessionError = (effect: Effect.Effect) => + effect.pipe( + Effect.catchTag("StorageNotFoundError", (error) => new SessionNotFoundHttpError({ message: error.message })), + Effect.catchTag("SessionBusyError", (error) => new SessionBusyHttpError({ message: error.message })), + ) +``` + +Use built-in `HttpApiError.BadRequest`, `HttpApiError.NotFound`, and related +types only when their generated response body and SDK surface are intentionally +acceptable. Use a custom schema-backed error when clients need the legacy +`{ name, data }` body or a domain-specific error payload. + +## Migration Phases + +### 1. Stabilize The Bridge + +Keep the temporary HttpApi error middleware only as a compatibility bridge while +typed errors are introduced. + +- Add tests that prove the bridge catches legacy `NamedError` defects. +- Add tests that prove declared HttpApi errors still use the declared endpoint + contract. +- Stop returning stack traces in unknown HTTP `500` responses; log the full + `Cause.pretty(cause)` server-side instead. +- Add a comment or TODO that names this plan and states the bridge must shrink + as route groups migrate. + +### 2. Define The Shared HTTP Error Helpers + +Add the `httpapi/errors.ts` module before converting route groups. + +- Define a legacy `{ name, data }` body helper for SDK-compatible errors. +- Define `UnknownError` for generic internal failures with a safe public message. +- Define `BadRequestError` and `NotFoundError` equivalents only if the actual + wire body must match the legacy Hono SDK surface. +- Put the HTTP status on the public schema with `HttpApiSchema.status(...)` or + `{ httpApiStatus: code }`; do not keep a separate name-to-status table. +- Keep conversion helpers pure and small. They should not inspect `Cause` or + accept `unknown` unless they are final fallback helpers. + +### 3. Convert One Vertical Slice + +Start with session read routes because they already have local `mapNotFound` +logic and are heavily covered by existing HttpApi tests. + +- Convert `Session.BusyError` from a plain `Error` to a typed service error, or + add a typed wrapper while preserving the old constructor until callers are + migrated. +- Replace `catchDefect` in `httpapi/handlers/session.ts` with typed error + mapping. +- Add endpoint error schemas for the affected session endpoints. +- Prove behavior with focused tests in `test/server/httpapi-session.test.ts`. +- Remove the migrated cases from the global compatibility middleware. + +### 4. Convert Legacy NamedError Domains + +Move legacy `NamedError.create(...)` services to Effect Schema-backed errors in +small domain PRs. + +Priority order: + +1. `storage/storage.ts` and `storage/db.ts` not-found errors. +2. `worktree/index.ts` `Worktree*` errors. +3. `provider/auth.ts` validation failures and `provider/provider.ts` model-not-found errors. +4. `mcp/index.ts`, `skill/index.ts`, `lsp/client.ts`, and `ide/index.ts` service errors. +5. Config and CLI-only errors after HTTP-facing domains are stable. + +For each domain: + +- Replace `NamedError.create(...)` with `Schema.TaggedErrorClass` when the error + is primarily a service error. +- Keep or add a separate HTTP error schema when the legacy `{ name, data }` wire + shape must remain stable. +- Update service interface return types to include the new error union. +- Replace `throw new X(...)` inside `Effect.fn` with `yield* new X(...)`. +- Replace async exceptions with `Effect.try({ catch })` or explicit `mapError`. +- Add service-level tests that assert the error tag and data, not just the HTTP + status. + +### 5. Declare HttpApi Errors Group By Group + +For each HttpApi group: + +- Inventory every service call and the typed errors it can return. +- Add only the public error schemas that endpoint can actually emit. +- Map service errors to HTTP errors in the handler file. +- Keep built-in `HttpApiError` only for generic request/validation failures where + the generated contract is accepted. +- Update `httpapi/public.ts` compatibility transforms only when the generated + spec cannot represent the desired source shape directly. +- Regenerate the SDK after OpenAPI-visible changes and verify the diff is + intentional. + +Suggested route order: + +1. `session` not-found and busy-state reads. +2. `experimental` worktree mutations. +3. `provider` auth and model selection errors. +4. `mcp` OAuth and connection errors. +5. Remaining route groups as Hono deletion work progresses. + +### 6. Remove Defect Recovery + +After enough route groups declare their expected errors: + +- Delete `catchDefect` recovery for domain errors. +- Delete name-prefix checks such as `error.name.startsWith("Worktree")` from + HTTP middleware. +- Delete `NamedError` branches from the Effect HttpApi compatibility middleware + once no Effect route depends on them. +- Leave one final unknown-defect fallback that logs server-side and returns a + safe generic `500` body. + +## Inventory Checklist + +Use this checklist when touching a service or route group. + +- [ ] Does the service interface expose every expected failure in the Effect + error type? +- [ ] Are user-caused, provider-caused, IO, auth, missing-resource, and busy-state + failures modeled as typed errors instead of defects? +- [ ] Does the service avoid importing HTTP status, `HttpApiError`, or response + classes? +- [ ] Does the handler map each service error into a declared endpoint error? +- [ ] Does the endpoint `error` field include every public error the handler can + emit? +- [ ] Does OpenAPI/SDK output either stay byte-identical or have an explicitly + reviewed diff? +- [ ] Do tests cover both service-level error typing and HTTP-level status/body? +- [ ] Did the PR remove any now-unneeded case from the temporary compatibility + middleware? + +## Testing Requirements + +For service conversions: + +- Test the service method directly with `testEffect(...)`. +- Assert on `_tag` or class identity and the structured fields. +- Avoid testing by string-matching `Cause.pretty(...)`. + +For HttpApi conversions: + +- Add or update the focused `test/server/httpapi-*.test.ts` file. +- Assert status code, content type, and exact JSON body for declared public + errors. +- Add a regression test that the temporary middleware is no longer needed for the + migrated route. +- Keep bridge/parity tests aligned with legacy Hono behavior until Hono is + deleted or the SDK contract intentionally changes. + +## Verification Commands + +Run from `packages/opencode` unless noted otherwise. + +```bash +bun run prettier --write +bunx oxlint +bun typecheck +bun run test -- test/server/httpapi-session.test.ts +``` + +Run SDK generation from the repo root when schemas or OpenAPI-visible errors +change. + +```bash +./packages/sdk/js/script/build.ts +``` + +## Open Questions + +- Should legacy V1 routes keep `{ name, data }` forever while V2 routes expose a + more Effect-native tagged error body? +- Should storage not-found remain generic, or should callers map it to + domain-specific not-found errors before crossing service boundaries? +- Should `namedSchemaError(...)` stay as a long-term public-wire helper, or only + as a migration bridge for old `NamedError` contracts? +- Which SDK version boundary lets us stop remapping built-in Effect HttpApi error + schemas in `httpapi/public.ts`? + +## Success Criteria + +- New service code no longer uses `die` for expected failures. +- A route reviewer can read an endpoint definition and see every public error it + can return. +- The temporary HttpApi error middleware shrinks over time instead of gaining new + name-based cases. +- Service tests prove domain error types without going through HTTP. +- HTTP tests prove status/body contracts without relying on defect recovery. diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 08c0df929c..33f1e78ac0 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -9,6 +9,7 @@ import { Locale } from "@/util/locale" import { Flag } from "@opencode-ai/core/flag/flag" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" +import { NotFoundError } from "@/storage/storage" import { EOL } from "os" import path from "path" import { which } from "../../util/which" @@ -59,9 +60,9 @@ export const SessionDeleteCommand = effectCmd({ handler: Effect.fn("Cli.session.delete")(function* (args) { const svc = yield* Session.Service const sessionID = SessionID.make(args.sessionID) - // Match legacy try/catch — Session.get surfaces NotFoundError as a defect. - yield* svc.get(sessionID).pipe(Effect.catchCause(() => fail(`Session not found: ${args.sessionID}`))) - yield* svc.remove(sessionID) + yield* svc.remove(sessionID).pipe( + Effect.catchIf(NotFoundError.isInstance, () => fail(`Session not found: ${args.sessionID}`)), + ) UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL) }), }) diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index fe651fe3e3..24ca0e61bf 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -24,11 +24,12 @@ import { Session } from "@/session/session" import { SessionPrompt } from "@/session/prompt" import { SessionTable } from "@/session/session.sql" import { SessionID } from "@/session/schema" +import { NotFoundError } from "@/storage/storage" import { errorData } from "@/util/error" import { waitEvent } from "./util" import { WorkspaceContext } from "./workspace-context" import { EffectBridge } from "@/effect/bridge" -import { NonNegativeInt, withStatics } from "@/util/schema" +import { withStatics } from "@/util/schema" import { zod as effectZod, zodObject } from "@/util/effect-zod" export const Info = WorkspaceInfoSchema @@ -739,9 +740,19 @@ export const layer = Layer.effect( const remove = Effect.fn("Workspace.remove")(function* (id: WorkspaceID) { const sessions = yield* db((db) => - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), + db + .select({ id: SessionTable.id, parentID: SessionTable.parent_id }) + .from(SessionTable) + .where(eq(SessionTable.workspace_id, id)) + .all(), + ) + const sessionIDs = new Set(sessions.map((sessionInfo) => sessionInfo.id)) + yield* Effect.forEach( + sessions.filter((sessionInfo) => !sessionInfo.parentID || !sessionIDs.has(sessionInfo.parentID)), + (sessionInfo) => + session.remove(sessionInfo.id).pipe(Effect.catchIf(NotFoundError.isInstance, () => Effect.void)), + { discard: true }, ) - yield* Effect.forEach(sessions, (sessionInfo) => session.remove(sessionInfo.id), { discard: true }) const row = yield* db((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) if (!row) return diff --git a/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md b/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md index 757d7aed0c..a6ccf794dd 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md +++ b/packages/opencode/src/server/routes/instance/httpapi/AGENTS.md @@ -32,4 +32,6 @@ Avoid `HttpRouter.provideRequest(...)` unless the dependency is intentionally re Use `Effect.provideService(...)` in middleware only for request-derived context, such as `WorkspaceRouteContext`, `InstanceRef`, or `WorkspaceRef`. Do not use it to smuggle stable services through request effects when they can be yielded at layer construction. +Public JSON errors should be explicit `Schema.ErrorClass` contracts declared on each endpoint. Use built-in `HttpApiError.*` classes only when their empty/tagged body is the intended wire shape; for SDK-visible errors with messages, define an API error schema such as `ApiNotFoundError` and fail with that exact declared error. Keep domain and storage services free of HttpApi types, and translate expected domain errors at the handler boundary. + When adding middleware, compose it at the layer boundary and keep the route tree explicit in `server.ts`. Shared router middleware such as auth, workspace routing, and instance context should stay visible where routes are assembled. diff --git a/packages/opencode/src/server/routes/instance/httpapi/errors.ts b/packages/opencode/src/server/routes/instance/httpapi/errors.ts new file mode 100644 index 0000000000..e5df6f5abf --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/errors.ts @@ -0,0 +1,18 @@ +import { Schema } from "effect" + +export class ApiNotFoundError extends Schema.ErrorClass("NotFoundError")( + { + name: Schema.Literal("NotFoundError"), + data: Schema.Struct({ + message: Schema.String, + }), + }, + { httpApiStatus: 404 }, +) {} + +export function notFound(message: string) { + return new ApiNotFoundError({ + name: "NotFoundError", + data: { message }, + }) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts index 3304ab9fbf..ad513e0ad4 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts @@ -6,6 +6,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "e import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" +import { ApiNotFoundError } from "../errors" import { described } from "./metadata" const root = "/pty" @@ -64,7 +65,7 @@ export const PtyApi = HttpApi.make("pty") HttpApiEndpoint.get("get", PtyPaths.get, { params: { ptyID: PtyID }, success: described(Pty.Info, "Session info"), - error: HttpApiError.NotFound, + error: ApiNotFoundError, }).annotateMerge( OpenApi.annotations({ identifier: "pty.get", @@ -76,7 +77,7 @@ export const PtyApi = HttpApi.make("pty") params: { ptyID: PtyID }, payload: Pty.UpdateInput, success: described(Pty.Info, "Updated session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "pty.update", @@ -87,7 +88,7 @@ export const PtyApi = HttpApi.make("pty") HttpApiEndpoint.delete("remove", PtyPaths.remove, { params: { ptyID: PtyID }, success: described(Schema.Boolean, "Session removed"), - error: HttpApiError.NotFound, + error: ApiNotFoundError, }).annotateMerge( OpenApi.annotations({ identifier: "pty.remove", @@ -98,7 +99,7 @@ export const PtyApi = HttpApi.make("pty") HttpApiEndpoint.post("connectToken", PtyPaths.connectToken, { params: { ptyID: PtyID }, success: described(PtyTicket.ConnectToken, "WebSocket connect token"), - error: [HttpApiError.Forbidden, HttpApiError.NotFound], + error: [HttpApiError.Forbidden, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "pty.connectToken", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts index 77d064ff5a..1159c88030 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts @@ -15,6 +15,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, Op import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" +import { ApiNotFoundError } from "../errors" import { described } from "./metadata" const root = "/session" @@ -123,7 +124,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.get("get", SessionPaths.get, { params: { sessionID: SessionID }, success: described(Session.Info, "Get session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.get", @@ -168,7 +169,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID }, query: MessagesQuery, success: described(Schema.Array(MessageV2.WithParts), "List of messages"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.messages", @@ -179,7 +180,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.get("message", SessionPaths.message, { params: { sessionID: SessionID, messageID: MessageID }, success: described(MessageV2.WithParts, "Message"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.message", @@ -201,7 +202,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.delete("remove", SessionPaths.remove, { params: { sessionID: SessionID }, success: described(Schema.Boolean, "Successfully deleted session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.delete", @@ -213,7 +214,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID }, payload: UpdatePayload, success: described(Session.Info, "Successfully updated session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.update", @@ -225,6 +226,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID }, payload: ForkPayload, success: described(Session.Info, "200"), + error: ApiNotFoundError, }).annotateMerge( OpenApi.annotations({ identifier: "session.fork", @@ -259,7 +261,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("share", SessionPaths.share, { params: { sessionID: SessionID }, success: described(Session.Info, "Successfully shared session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.share", @@ -270,7 +272,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.delete("unshare", SessionPaths.share, { params: { sessionID: SessionID }, success: described(Session.Info, "Successfully unshared session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.unshare", @@ -282,7 +284,7 @@ export const SessionApi = HttpApi.make("session") params: { sessionID: SessionID }, payload: SummarizePayload, success: described(Schema.Boolean, "Summarized session"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "session.summarize", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts index efe73d95d1..8ab43f6654 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts @@ -4,6 +4,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "e import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" +import { ApiNotFoundError } from "../errors" import { described } from "./metadata" const root = "/tui" @@ -155,7 +156,7 @@ export const TuiApi = HttpApi.make("tui") HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, { payload: TuiEvent.SessionSelect.properties, success: described(Schema.Boolean, "Session selected successfully"), - error: [HttpApiError.BadRequest, HttpApiError.NotFound], + error: [HttpApiError.BadRequest, ApiNotFoundError], }).annotateMerge( OpenApi.annotations({ identifier: "tui.selectSession", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts index e5ff300a2a..7b8395d809 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts @@ -15,6 +15,7 @@ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstab import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" import * as Socket from "effect/unstable/socket/Socket" import { InstanceHttpApi } from "../api" +import * as ApiError from "../errors" import { CursorQuery, Params, PtyPaths } from "../groups/pty" import { WebSocketTracker } from "../websocket-tracker" @@ -46,7 +47,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler const get = Effect.fn("PtyHttpApi.get")(function* (ctx: { params: { ptyID: PtyID } }) { const info = yield* pty.get(ctx.params.ptyID) - if (!info) return yield* new HttpApiError.NotFound({}) + if (!info) return yield* ApiError.notFound("Session not found") return info }) @@ -58,7 +59,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler ...ctx.payload, size: ctx.payload.size ? { ...ctx.payload.size } : undefined, }) - if (!info) return yield* new HttpApiError.NotFound({}) + if (!info) return yield* ApiError.notFound("Session not found") return info }) @@ -71,7 +72,7 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler const request = yield* HttpServerRequest.HttpServerRequest if (request.headers[PTY_CONNECT_TOKEN_HEADER] !== PTY_CONNECT_TOKEN_HEADER_VALUE || !validOrigin(request, cors)) return yield* new HttpApiError.Forbidden({}) - if (!(yield* pty.get(ctx.params.ptyID))) return yield* new HttpApiError.NotFound({}) + if (!(yield* pty.get(ctx.params.ptyID))) return yield* ApiError.notFound("Session not found") return yield* tickets.issue({ ptyID: ctx.params.ptyID, ...(yield* PtyTicket.scope) }) }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts new file mode 100644 index 0000000000..98ac2b9ad6 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts @@ -0,0 +1,9 @@ +import type { NotFoundError as StorageNotFoundError } from "@/storage/storage" +import { Effect } from "effect" +import * as ApiError from "../errors" + +type StorageNotFound = InstanceType + +export function mapStorageNotFound(self: Effect.Effect) { + return self.pipe(Effect.mapError((error) => ApiError.notFound(error.data.message))) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts index 4a67ba036e..56fa7adb15 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts @@ -37,14 +37,7 @@ import { SummarizePayload, UpdatePayload, } from "../groups/session" - -const mapNotFound = (self: Effect.Effect) => - self.pipe( - Effect.catchIf(NotFoundError.isInstance, () => Effect.fail(new HttpApiError.NotFound({}))), - Effect.catchDefect((error) => - NotFoundError.isInstance(error) ? Effect.fail(new HttpApiError.NotFound({})) : Effect.die(error), - ), - ) +import * as SessionError from "./session-errors" export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) => Effect.gen(function* () { @@ -79,7 +72,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", }) const get = Effect.fn("SessionHttpApi.get")(function* (ctx: { params: { sessionID: SessionID } }) { - return yield* mapNotFound(session.get(ctx.params.sessionID)) + return yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) }) const children = Effect.fn("SessionHttpApi.children")(function* (ctx: { params: { sessionID: SessionID } }) { @@ -101,51 +94,49 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", params: { sessionID: SessionID } query: typeof MessagesQuery.Type }) { - return yield* mapNotFound( - Effect.gen(function* () { - if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({}) - if (ctx.query.before) { - const before = ctx.query.before - yield* Effect.try({ - try: () => MessageV2.cursor.decode(before), - catch: () => new HttpApiError.BadRequest({}), - }) - } - if (ctx.query.limit === undefined || ctx.query.limit === 0) { - yield* session.get(ctx.params.sessionID) - return yield* session.messages({ sessionID: ctx.params.sessionID }) - } - - yield* session.get(ctx.params.sessionID) - const page = MessageV2.page({ - sessionID: ctx.params.sessionID, - limit: ctx.query.limit, - before: ctx.query.before, - }) - if (!page.cursor) return page.items - - const request = yield* HttpServerRequest.HttpServerRequest - // toURL() honors the Host + x-forwarded-proto headers, so the Link - // header echoes the real origin instead of a hard-coded localhost. - const url = Option.getOrElse(HttpServerRequest.toURL(request), () => new URL(request.url, "http://localhost")) - url.searchParams.set("limit", ctx.query.limit.toString()) - url.searchParams.set("before", page.cursor) - return HttpServerResponse.jsonUnsafe(page.items, { - headers: { - "Access-Control-Expose-Headers": "Link, X-Next-Cursor", - Link: `<${url.toString()}>; rel="next"`, - "X-Next-Cursor": page.cursor, - }, - }) - }), - ) + if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({}) + if (ctx.query.before) { + const before = ctx.query.before + yield* Effect.try({ + try: () => MessageV2.cursor.decode(before), + catch: () => new HttpApiError.BadRequest({}), + }) + } + yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) + if (ctx.query.limit === undefined || ctx.query.limit === 0) { + return yield* session.messages({ sessionID: ctx.params.sessionID }) + } + + const page = MessageV2.page({ + sessionID: ctx.params.sessionID, + limit: ctx.query.limit, + before: ctx.query.before, + }) + if (!page.cursor) return page.items + + const request = yield* HttpServerRequest.HttpServerRequest + // toURL() honors the Host + x-forwarded-proto headers, so the Link + // header echoes the real origin instead of a hard-coded localhost. + const url = Option.getOrElse(HttpServerRequest.toURL(request), () => new URL(request.url, "http://localhost")) + url.searchParams.set("limit", ctx.query.limit.toString()) + url.searchParams.set("before", page.cursor) + return HttpServerResponse.jsonUnsafe(page.items, { + headers: { + "Access-Control-Expose-Headers": "Link, X-Next-Cursor", + Link: `<${url.toString()}>; rel="next"`, + "X-Next-Cursor": page.cursor, + }, + }) }) const message = Effect.fn("SessionHttpApi.message")(function* (ctx: { params: { sessionID: SessionID; messageID: MessageID } }) { - return yield* mapNotFound( - Effect.sync(() => MessageV2.get({ sessionID: ctx.params.sessionID, messageID: ctx.params.messageID })), + return yield* SessionError.mapStorageNotFound( + Effect.try({ + try: () => MessageV2.get({ sessionID: ctx.params.sessionID, messageID: ctx.params.messageID }), + catch: (error) => error, + }).pipe(Effect.catch((error) => (NotFoundError.isInstance(error) ? Effect.fail(error) : Effect.die(error)))), ) }) @@ -170,7 +161,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", }) const remove = Effect.fn("SessionHttpApi.remove")(function* (ctx: { params: { sessionID: SessionID } }) { - yield* session.remove(ctx.params.sessionID) + yield* SessionError.mapStorageNotFound(session.remove(ctx.params.sessionID)) return true }) @@ -178,7 +169,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", params: { sessionID: SessionID } payload: typeof UpdatePayload.Type }) { - const current = yield* session.get(ctx.params.sessionID) + const current = yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) if (ctx.payload.title !== undefined) { yield* session.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title }) } @@ -191,14 +182,16 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", if (ctx.payload.time?.archived !== undefined) { yield* session.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived }) } - return yield* session.get(ctx.params.sessionID) + return yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) }) const fork = Effect.fn("SessionHttpApi.fork")(function* (ctx: { params: { sessionID: SessionID } payload: typeof ForkPayload.Type }) { - return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }) + return yield* SessionError.mapStorageNotFound( + session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }), + ) }) const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) { @@ -222,19 +215,19 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", const share = Effect.fn("SessionHttpApi.share")(function* (ctx: { params: { sessionID: SessionID } }) { yield* shareSvc.share(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) - return yield* session.get(ctx.params.sessionID) + return yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) }) const unshare = Effect.fn("SessionHttpApi.unshare")(function* (ctx: { params: { sessionID: SessionID } }) { yield* shareSvc.unshare(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) - return yield* session.get(ctx.params.sessionID) + return yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID)) }) const summarize = Effect.fn("SessionHttpApi.summarize")(function* (ctx: { params: { sessionID: SessionID } payload: typeof SummarizePayload.Type }) { - yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID)) + yield* revertSvc.cleanup(yield* SessionError.mapStorageNotFound(session.get(ctx.params.sessionID))) const messages = yield* session.messages({ sessionID: ctx.params.sessionID }) const defaultAgent = yield* agentSvc.defaultAgent() const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts index cc85321685..0ecebf451f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/tui.ts @@ -1,13 +1,12 @@ import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" -import { SessionTable } from "@/session/session.sql" -import * as Database from "@/storage/db" -import { eq } from "drizzle-orm" +import { Session } from "@/session/session" import { Effect } from "effect" import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" import { nextTuiRequest, submitTuiResponse } from "@/server/shared/tui-control" import { InstanceHttpApi } from "../api" import { CommandPayload, TuiPublishPayload } from "../groups/tui" +import * as SessionError from "./session-errors" const commandAliases = { session_new: "session.new", @@ -28,6 +27,7 @@ const commandAliases = { export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handlers) => Effect.gen(function* () { const bus = yield* Bus.Service + const session = yield* Session.Service const publishCommand = (command: typeof TuiEvent.CommandExecute.properties.Type.command | undefined) => bus.publish(TuiEvent.CommandExecute, { command } as typeof TuiEvent.CommandExecute.properties.Type) @@ -98,12 +98,7 @@ export const tuiHandlers = HttpApiBuilder.group(InstanceHttpApi, "tui", (handler payload: typeof TuiEvent.SessionSelect.properties.Type }) { if (!ctx.payload.sessionID.startsWith("ses")) return yield* new HttpApiError.BadRequest({}) - const row = yield* Effect.sync(() => - Database.use((db) => - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, ctx.payload.sessionID)).get(), - ), - ) - if (!row) return yield* new HttpApiError.NotFound({}) + yield* SessionError.mapStorageNotFound(session.get(ctx.payload.sessionID)) yield* bus.publish(TuiEvent.SessionSelect, ctx.payload) return true }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts index a91a9992df..8ec9f74860 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts @@ -7,6 +7,7 @@ import { Session } from "@/session/session" import { HttpApiProxy } from "./proxy" import * as Fence from "@/server/shared/fence" import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing" +import { NotFoundError } from "@/storage/storage" import { Flag } from "@opencode-ai/core/flag/flag" import { Context, Data, Effect, Layer } from "effect" import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" @@ -178,7 +179,10 @@ function routeHttpApiWorkspace( const request = yield* HttpServerRequest.HttpServerRequest const sessionID = getWorkspaceRouteSessionID(requestURL(request)) const session = sessionID - ? yield* Session.Service.use((svc) => svc.get(sessionID)).pipe(Effect.catchDefect(() => Effect.void)) + ? yield* Session.Service.use((svc) => svc.get(sessionID)).pipe( + Effect.catchIf(NotFoundError.isInstance, () => Effect.succeed(undefined)), + Effect.catchDefect(() => Effect.succeed(undefined)), + ) : undefined const plan = yield* planRequest(request, session?.workspaceID) return yield* routeWorkspace(client, effect, plan) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 8286ecf8e6..fef8c43836 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -744,7 +744,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const markReady = ready ? ready.open.pipe(Effect.asVoid) : Effect.void const { msg, part, cwd } = yield* Effect.gen(function* () { const ctx = yield* InstanceState.context - const session = yield* sessions.get(input.sessionID) + const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) if (session.revert) { yield* revert.cleanup(session) } @@ -1370,7 +1370,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const prompt: (input: PromptInput) => Effect.Effect = Effect.fn("SessionPrompt.prompt")( function* (input: PromptInput) { - const session = yield* sessions.get(input.sessionID) + const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) yield* revert.cleanup(session) const message = yield* createUserMessage(input) yield* sessions.touch(input.sessionID) @@ -1401,9 +1401,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the function* (sessionID: SessionID) { const ctx = yield* InstanceState.context const slog = elog.with({ sessionID }) - let structured: unknown | undefined + let structured: unknown let step = 0 - const session = yield* sessions.get(sessionID) + const session = yield* sessions.get(sessionID).pipe(Effect.orDie) while (true) { yield* status.set(sessionID, { type: "busy" }) diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 58d69a2040..abf7c3441f 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -44,7 +44,7 @@ export const layer = Layer.effect( yield* state.assertNotBusy(input.sessionID) const all = yield* sessions.messages({ sessionID: input.sessionID }) let lastUser: MessageV2.User | undefined - const session = yield* sessions.get(input.sessionID) + const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) let rev: Session.Info["revert"] const patches: Snapshot.Patch[] = [] @@ -75,8 +75,8 @@ export const layer = Layer.effect( rev.snapshot = session.revert?.snapshot ?? (yield* snap.track()) if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot) yield* snap.revert(patches) - if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string) - const range = all.filter((msg) => msg.info.id >= rev!.messageID) + if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot) + const range = all.filter((msg) => msg.info.id >= rev.messageID) const diffs = yield* summary.computeDiff({ messages: range }) yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) @@ -89,17 +89,17 @@ export const layer = Layer.effect( files: diffs.length, }, }) - return yield* sessions.get(input.sessionID) + return yield* sessions.get(input.sessionID).pipe(Effect.orDie) }) const unrevert = Effect.fn("SessionRevert.unrevert")(function* (input: { sessionID: SessionID }) { log.info("unreverting", input) yield* state.assertNotBusy(input.sessionID) - const session = yield* sessions.get(input.sessionID) + const session = yield* sessions.get(input.sessionID).pipe(Effect.orDie) if (!session.revert) return session - if (session.revert.snapshot) yield* snap.restore(session.revert!.snapshot!) + if (session.revert.snapshot) yield* snap.restore(session.revert.snapshot) yield* sessions.clearRevert(input.sessionID) - return yield* sessions.get(input.sessionID) + return yield* sessions.get(input.sessionID).pipe(Effect.orDie) }) const cleanup = Effect.fn("SessionRevert.cleanup")(function* (session: Session.Info) { diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 09d2c8c3c3..5c938ff693 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -3,7 +3,6 @@ import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Decimal } from "decimal.js" -import z from "zod" import { type ProviderMetadata, type LanguageModelUsage } from "ai" import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationVersion } from "@opencode-ai/core/installation/version" @@ -422,6 +421,8 @@ export class BusyError extends Error { } } +export type NotFound = InstanceType + export interface Interface { readonly list: (input?: ListInput) => Effect.Effect readonly create: (input?: { @@ -432,9 +433,9 @@ export interface Interface { permission?: Permission.Ruleset workspaceID?: WorkspaceID }) => Effect.Effect - readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect + readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect readonly touch: (sessionID: SessionID) => Effect.Effect - readonly get: (id: SessionID) => Effect.Effect + readonly get: (id: SessionID) => Effect.Effect readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect @@ -448,7 +449,7 @@ export interface Interface { readonly diff: (sessionID: SessionID) => Effect.Effect readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect readonly children: (parentID: SessionID) => Effect.Effect - readonly remove: (sessionID: SessionID) => Effect.Effect + readonly remove: (sessionID: SessionID) => Effect.Effect readonly updateMessage: (msg: T) => Effect.Effect readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect @@ -534,13 +535,13 @@ export const layer: Layer.Layer d.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + if (!row) return yield* Effect.fail(new NotFoundError({ message: `Session not found: ${id}` })) return fromRow(row) }) const list = Effect.fn("Session.list")(function* (input?: ListInput) { const ctx = yield* InstanceState.context - return Array.from(listByProject({ projectID: ctx.project.id, ...(input ?? {}) })) + return Array.from(listByProject({ projectID: ctx.project.id, ...input })) }) const children = Effect.fn("Session.children")(function* (parentID: SessionID) { @@ -555,8 +556,8 @@ export const layer: Layer.Layer { yield* eventuallyEffect( Effect.gen(function* () { - expect((yield* sessionSvc.get(session.id)).title).toBe("from history") + expect((yield* sessionSvc.get(session.id).pipe(Effect.orDie)).title).toBe("from history") }), ) expect(historyBodies).toEqual([{ [session.id]: historyNextSeq - 1 }]) @@ -1208,7 +1208,7 @@ describe("workspace-old sync state", () => { yield* eventuallyEffect( Effect.gen(function* () { - expect((yield* sessionSvc.get(session.id)).title).toBe("from sse") + expect((yield* sessionSvc.get(session.id).pipe(Effect.orDie)).title).toBe("from sse") }), ) expect( diff --git a/packages/opencode/test/server/httpapi-parity.test.ts b/packages/opencode/test/server/httpapi-parity.test.ts index 6922d8c43f..9d7eff4964 100644 --- a/packages/opencode/test/server/httpapi-parity.test.ts +++ b/packages/opencode/test/server/httpapi-parity.test.ts @@ -105,23 +105,22 @@ describe("404 mapping for missing session", () => { }) // ────────────────────────────────────────────────────────────────────────────── -// Reproducer 3: 404 response body shape should match Hono's NamedError -// envelope `{ name, data: { message } }`. HttpApi returns the typed-error -// shape `{ _tag }` instead. SDK consumers reading `error.data.message` -// see undefined. -// -// FIXME: unskip when error JSON shape policy is decided + applied (separate PR). +// Reproducer 3: 404 response body shape should match Hono's public NamedError +// envelope `{ name, data: { message } }`. SDK consumers read +// `error.data.message`, so returning an Effect built-in `{ _tag }` body is a +// compatibility break. // ────────────────────────────────────────────────────────────────────────────── describe("Error JSON shape parity", () => { - test.todo("HttpApi 404 body matches NamedError shape", async () => { + test("HttpApi 404 body matches Hono shape", async () => { await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + const headers = { "x-opencode-directory": tmp.path } - const response = await app(true).request("/session/ses_does_not_exist", { - headers: { "x-opencode-directory": tmp.path }, - }) + const hono = await app(false).request("/session/ses_does_not_exist", { headers }) + const httpapi = await app(true).request("/session/ses_does_not_exist", { headers }) - expect(response.status).toBe(404) - const body = (await response.json()) as { name?: string; data?: { message?: string } } + expect(httpapi.status).toBe(hono.status) + const body = (await httpapi.json()) as { name?: string; data?: { message?: string } } + expect(body).toEqual(await hono.json()) expect(body.name).toBe("NotFoundError") expect(typeof body.data?.message).toBe("string") }) diff --git a/packages/opencode/test/server/httpapi-pty.test.ts b/packages/opencode/test/server/httpapi-pty.test.ts index 2b6284a310..5e63eae61c 100644 --- a/packages/opencode/test/server/httpapi-pty.test.ts +++ b/packages/opencode/test/server/httpapi-pty.test.ts @@ -50,9 +50,9 @@ const effectIt = testEffect( ), ) -function app() { - Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true - return Server.Default().app +function app(experimental = true) { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental + return experimental ? Server.Default().app : Server.Legacy().app } function serverUrl() { @@ -121,6 +121,18 @@ describe("pty HttpApi bridge", () => { expect(missing.status).toBe(404) }) + test("matches Hono missing PTY error body", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const headers = { "x-opencode-directory": tmp.path } + const path = PtyPaths.get.replace(":ptyID", PtyID.ascending()) + + const hono = await app(false).request(path, { headers }) + const httpapi = await app().request(path, { headers }) + + expect(httpapi.status).toBe(hono.status) + expect(await httpapi.json()).toEqual(await hono.json()) + }) + test("returns 404 for missing PTY websocket before upgrade", async () => { await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) const response = await app().request(PtyPaths.connect.replace(":ptyID", PtyID.ascending()), { diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index ce774ccfd0..6d2df45078 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/packages/opencode/test/server/httpapi-sdk.test.ts @@ -4,6 +4,7 @@ import type * as Scope from "effect/Scope" import { HttpRouter } from "effect/unstable/http" import { Flag } from "@opencode-ai/core/flag/flag" import { createOpencodeClient } from "@opencode-ai/sdk/v2" +import { validateSession } from "../../src/cli/cmd/tui/validate-session" import { Instance } from "../../src/project/instance" import { WithInstance } from "../../src/project/with-instance" import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" @@ -13,6 +14,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { ModelID, ProviderID } from "../../src/provider/schema" import type { Config } from "@/config/config" import { Session as SessionNs } from "@/session/session" +import { errorMessage } from "../../src/util/error" import { TestLLMServer } from "../lib/llm-server" import path from "path" import { resetDatabase } from "../fixture/db" @@ -64,20 +66,23 @@ function client( directory?: string, input?: { password?: string; username?: string; headers?: Record }, ) { - const serverApp = app(backend, input) - const fetch = Object.assign( - async (request: RequestInfo | URL, init?: RequestInit) => - await serverApp.fetch(request instanceof Request ? request : new Request(request, init)), - { preconnect: globalThis.fetch.preconnect }, - ) satisfies typeof globalThis.fetch return createOpencodeClient({ baseUrl: "http://localhost", directory, headers: input?.headers, - fetch, + fetch: serverFetch(backend, input), }) } +function serverFetch(backend: Backend, input?: { password?: string; username?: string }) { + const serverApp = app(backend, input) + return Object.assign( + async (request: RequestInfo | URL, init?: RequestInit) => + await serverApp.fetch(request instanceof Request ? request : new Request(request, init)), + { preconnect: globalThis.fetch.preconnect }, + ) satisfies typeof globalThis.fetch +} + function authorization(username: string, password: string) { return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` } @@ -129,6 +134,16 @@ function capture(request: () => Promise) { ) } +function captureThrown(request: () => Promise) { + return call(async () => { + try { + await request() + } catch (error) { + return error + } + }) +} + function expectStatus(request: () => Promise<{ response: Response }>, status: number) { return call(request).pipe( Effect.tap((result) => Effect.sync(() => expect(result.response.status).toBe(status))), @@ -338,6 +353,46 @@ describe("HttpApi SDK", () => { ), ) + parity("matches generated SDK missing session errors across backends", (backend) => + withStandardProject(backend, ({ sdk }) => + Effect.gen(function* () { + const sessionID = "ses_missing" + const expected = { + name: "NotFoundError", + data: { message: `Session not found: ${sessionID}` }, + } + const missing = yield* capture(() => sdk.session.get({ sessionID })) + const thrown = yield* captureThrown(() => sdk.session.get({ sessionID }, { throwOnError: true })) + + expect(missing.error).toEqual(expected) + expect(thrown).toEqual(expected) + return { + status: missing.status, + error: missing.error, + thrown, + } + }), + ), + ) + + parity("formats missing session validation errors for -s", (backend) => + withStandardProject(backend, ({ directory }) => + Effect.gen(function* () { + const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T" + const thrown = yield* captureThrown(() => + validateSession({ + url: "http://localhost", + directory, + sessionID, + fetch: serverFetch(backend), + }), + ) + expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`) + return errorMessage(thrown) + }), + ), + ) + parity("matches generated SDK basic auth behavior across backends", (backend) => withStandardProject(backend, ({ directory }) => Effect.gen(function* () { diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 34cecd80d0..c45aacce75 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -8,13 +8,12 @@ import type { WorkspaceAdapter } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { PermissionID } from "../../src/permission/schema" import { ModelID, ProviderID } from "../../src/provider/schema" -import { Instance } from "../../src/project/instance" import { WithInstance } from "../../src/project/with-instance" import { Project } from "../../src/project/project" import { Server } from "../../src/server/server" import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" import { Session } from "@/session/session" -import { MessageID, PartID, type SessionID } from "../../src/session/schema" +import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema" import { MessageV2 } from "../../src/session/message-v2" import { Database } from "@/storage/db" import { SessionMessageTable, SessionTable } from "@/session/session.sql" @@ -55,7 +54,7 @@ function createSession(directory: string, input?: Session.CreateInput) { ) } -function createTextMessage(directory: string, sessionID: SessionID, text: string) { +function createTextMessage(directory: string, sessionID: SessionIDType, text: string) { return Effect.promise( async () => await WithInstance.provide({ @@ -125,6 +124,10 @@ function json(response: Response) { }) } +function responseJson(response: Response) { + return Effect.promise(() => response.json()) +} + function requestJson(path: string, init?: RequestInit) { return request(path, init).pipe(Effect.flatMap(json)) } @@ -147,6 +150,47 @@ afterEach(async () => { }) describe("session HttpApi", () => { + it.live( + "returns declared not found errors for read routes", + withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) => + Effect.gen(function* () { + const headers = { "x-opencode-directory": tmp.path } + const missingSession = SessionID.descending() + const missingSessionBody = { + name: "NotFoundError", + data: { message: `Session not found: ${missingSession}` }, + } + + const get = yield* request(pathFor(SessionPaths.get, { sessionID: missingSession }), { headers }) + expect(get.status).toBe(404) + expect(yield* responseJson(get)).toEqual(missingSessionBody) + + const messages = yield* request(pathFor(SessionPaths.messages, { sessionID: missingSession }), { headers }) + expect(messages.status).toBe(404) + expect(yield* responseJson(messages)).toEqual(missingSessionBody) + + const remove = yield* request(pathFor(SessionPaths.remove, { sessionID: missingSession }), { + headers, + method: "DELETE", + }) + expect(remove.status).toBe(404) + expect(yield* responseJson(remove)).toEqual(missingSessionBody) + + const session = yield* createSession(tmp.path, { title: "missing message" }) + const missingMessage = MessageID.ascending() + const message = yield* request( + pathFor(SessionPaths.message, { sessionID: session.id, messageID: missingMessage }), + { headers }, + ) + expect(message.status).toBe(404) + expect(yield* responseJson(message)).toEqual({ + name: "NotFoundError", + data: { message: `Message not found: ${missingMessage}` }, + }) + }), + ), + ) + it.live( "serves read routes through Hono bridge", withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) => diff --git a/packages/opencode/test/server/httpapi-tui.test.ts b/packages/opencode/test/server/httpapi-tui.test.ts index 8d2670c492..91cad362a9 100644 --- a/packages/opencode/test/server/httpapi-tui.test.ts +++ b/packages/opencode/test/server/httpapi-tui.test.ts @@ -72,14 +72,27 @@ describe("tui HttpApi bridge", () => { properties: { text: "from publish" }, }) + const missingSessionID = SessionID.descending() const missing = await app().request(TuiPaths.selectSession, { method: "POST", headers: { ...headers, "content-type": "application/json" }, - body: JSON.stringify({ sessionID: SessionID.descending() }), + body: JSON.stringify({ sessionID: missingSessionID }), }) expect(missing.status).toBe(404) }) + test("matches Hono missing selected session error body", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" } + const body = JSON.stringify({ sessionID: SessionID.descending() }) + + const hono = await app(false).request(TuiPaths.selectSession, { method: "POST", headers, body }) + const httpapi = await app().request(TuiPaths.selectSession, { method: "POST", headers, body }) + + expect(httpapi.status).toBe(hono.status) + expect(await httpapi.json()).toEqual(await hono.json()) + }) + test("matches legacy unknown execute command behavior", async () => { await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" } From 6e7c9eb820a2dd85724bcc8b4ad521776f937bca Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 6 May 2026 01:35:13 +0000 Subject: [PATCH 16/70] chore: generate --- packages/opencode/src/cli/cmd/session.ts | 6 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 3 +- packages/sdk/js/src/v2/gen/types.gen.ts | 47 +++++++++------- packages/sdk/openapi.json | 72 ++++++++++++++---------- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 33f1e78ac0..1240fa92ce 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -60,9 +60,9 @@ export const SessionDeleteCommand = effectCmd({ handler: Effect.fn("Cli.session.delete")(function* (args) { const svc = yield* Session.Service const sessionID = SessionID.make(args.sessionID) - yield* svc.remove(sessionID).pipe( - Effect.catchIf(NotFoundError.isInstance, () => fail(`Session not found: ${args.sessionID}`)), - ) + yield* svc + .remove(sessionID) + .pipe(Effect.catchIf(NotFoundError.isInstance, () => fail(`Session not found: ${args.sessionID}`))) UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL) }), }) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index fba70b5bf6..803d9ed16e 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -131,6 +131,7 @@ import type { SessionDeleteResponses, SessionDelivery, SessionDiffResponses, + SessionForkErrors, SessionForkResponses, SessionGetErrors, SessionGetResponses, @@ -3320,7 +3321,7 @@ export class Session2 extends HeyApiClient { }, ], ) - return (options?.client ?? this.client).post({ + return (options?.client ?? this.client).post({ url: "/session/{sessionID}/fork", ...options, ...params, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d8ea6d94e5..b58f6cfc2b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1561,6 +1561,13 @@ export type McpUnsupportedOAuthError = { error: string } +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + export type EffectHttpApiErrorForbidden = { _tag: "Forbidden" } @@ -3224,13 +3231,6 @@ export type BadRequestError = { success: false } -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - export type AuthRemoveData = { body?: never path: { @@ -4571,7 +4571,7 @@ export type PtyRemoveData = { export type PtyRemoveErrors = { /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -4601,7 +4601,7 @@ export type PtyGetData = { export type PtyGetErrors = { /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -4671,7 +4671,7 @@ export type PtyConnectTokenErrors = { */ 403: EffectHttpApiErrorForbidden /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5070,7 +5070,7 @@ export type SessionDeleteErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5104,7 +5104,7 @@ export type SessionGetErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5144,7 +5144,7 @@ export type SessionUpdateErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5270,7 +5270,7 @@ export type SessionMessagesErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5395,7 +5395,7 @@ export type SessionMessageErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5428,6 +5428,15 @@ export type SessionForkData = { url: "/session/{sessionID}/fork" } +export type SessionForkErrors = { + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionForkError = SessionForkErrors[keyof SessionForkErrors] + export type SessionForkResponses = { /** * 200 @@ -5527,7 +5536,7 @@ export type SessionUnshareErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5561,7 +5570,7 @@ export type SessionShareErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -5599,7 +5608,7 @@ export type SessionSummarizeErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } @@ -6463,7 +6472,7 @@ export type TuiSelectSessionErrors = { */ 400: BadRequestError /** - * Not found + * NotFoundError */ 404: NotFoundError } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 007da60269..477145f017 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3241,7 +3241,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -3394,7 +3394,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -3479,7 +3479,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -4454,7 +4454,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -4526,7 +4526,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -4597,7 +4597,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -4952,7 +4952,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -5200,7 +5200,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -5342,6 +5342,16 @@ } } } + }, + "404": { + "description": "NotFoundError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } } }, "description": "Create a new session by forking an existing session at a specific message point.", @@ -5592,7 +5602,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -5663,7 +5673,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -5737,7 +5747,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -7897,7 +7907,7 @@ } }, "404": { - "description": "Not found", + "description": "NotFoundError", "content": { "application/json": { "schema": { @@ -12904,6 +12914,25 @@ "required": ["error"], "additionalProperties": false }, + "NotFoundError": { + "type": "object", + "required": ["name", "data"], + "properties": { + "name": { + "type": "string", + "enum": ["NotFoundError"] + }, + "data": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } + } + } + }, "effect_HttpApiError_Forbidden": { "type": "object", "properties": { @@ -18026,25 +18055,6 @@ "enum": [false] } } - }, - "NotFoundError": { - "type": "object", - "required": ["name", "data"], - "properties": { - "name": { - "type": "string", - "enum": ["NotFoundError"] - }, - "data": { - "type": "object", - "required": ["message"], - "properties": { - "message": { - "type": "string" - } - } - } - } } } }, From 5013e8a8ecc7912044f29918d5567c1b37942350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?imduchuyyy=20=F0=9F=90=AC?= Date: Wed, 6 May 2026 11:15:59 +0700 Subject: [PATCH 17/70] docs: update desktop app references from Tauri to Electron (#25965) --- CONTRIBUTING.md | 24 ++++++------------------ README.ar.md | 4 ++-- README.bn.md | 6 +++--- README.br.md | 4 ++-- README.bs.md | 4 ++-- README.da.md | 4 ++-- README.de.md | 4 ++-- README.es.md | 4 ++-- README.fr.md | 4 ++-- README.gr.md | 4 ++-- README.it.md | 4 ++-- README.ja.md | 4 ++-- README.ko.md | 4 ++-- README.md | 12 ++++++------ README.no.md | 4 ++-- README.pl.md | 4 ++-- README.ru.md | 4 ++-- README.th.md | 4 ++-- README.tr.md | 4 ++-- README.uk.md | 4 ++-- README.vi.md | 4 ++-- README.zh.md | 4 ++-- README.zht.md | 4 ++-- 23 files changed, 55 insertions(+), 67 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ae3fc6f2f..e1a62ae9ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,7 +73,7 @@ Replace `` with your platform (e.g., `darwin-arm64`, `linux-x64`). - `packages/opencode`: OpenCode core business logic & server. - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui) - `packages/app`: The shared web UI components, written in SolidJS - - `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`) + - `packages/desktop`: The native desktop app, built with Electron (wraps `packages/app`) - `packages/plugin`: Source for `@opencode-ai/plugin` ### Understanding bun dev vs opencode @@ -123,33 +123,21 @@ This starts a local dev server at http://localhost:5173 (or similar port shown i ### Running the Desktop App -The desktop app is a native Tauri application that wraps the web UI. +The desktop app is an Electron application that wraps the web UI. -To run the native desktop app: - -```bash -bun run --cwd packages/desktop tauri dev -``` - -This starts the web dev server on http://localhost:1420 and opens the native window. - -If you only want the web dev server (no native shell): +To run the desktop app in development: ```bash bun run --cwd packages/desktop dev ``` -To create a production `dist/` and build the native app bundle: +To create a production build and package the app: ```bash -bun run --cwd packages/desktop tauri build +bun run --cwd packages/desktop build +bun run --cwd packages/desktop package ``` -This runs `bun run --cwd packages/desktop build` automatically via Tauri’s `beforeBuildCommand`. - -> [!NOTE] -> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. - > [!NOTE] > If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files. diff --git a/README.ar.md b/README.ar.md index beb44589e6..e6781325f2 100644 --- a/README.ar.md +++ b/README.ar.md @@ -70,8 +70,8 @@ nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث | المنصة | التنزيل | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb` او `.rpm` او AppImage | diff --git a/README.bn.md b/README.bn.md index c7abc7346a..b6d981e383 100644 --- a/README.bn.md +++ b/README.bn.md @@ -70,10 +70,10 @@ OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসে | প্ল্যাটফর্ম | ডাউনলোড | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, or AppImage | +| Linux | `.deb`, `.rpm`, or `.AppImage` | ```bash # macOS (Homebrew) diff --git a/README.br.md b/README.br.md index 6d1de21562..ae01949394 100644 --- a/README.br.md +++ b/README.br.md @@ -70,8 +70,8 @@ O OpenCode também está disponível como aplicativo desktop. Baixe diretamente | Plataforma | Download | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` ou AppImage | diff --git a/README.bs.md b/README.bs.md index 2cff8e0279..c2035bb1b3 100644 --- a/README.bs.md +++ b/README.bs.md @@ -70,8 +70,8 @@ OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice | Platforma | Preuzimanje | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, ili AppImage | diff --git a/README.da.md b/README.da.md index ac522f29c4..a89cb21193 100644 --- a/README.da.md +++ b/README.da.md @@ -70,8 +70,8 @@ OpenCode findes også som desktop-app. Download direkte fra [releases-siden](htt | Platform | Download | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, eller AppImage | diff --git a/README.de.md b/README.de.md index 87a670f3fc..41f651849b 100644 --- a/README.de.md +++ b/README.de.md @@ -70,8 +70,8 @@ OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Rel | Plattform | Download | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` oder AppImage | diff --git a/README.es.md b/README.es.md index 9e456af1c0..20f749fbb6 100644 --- a/README.es.md +++ b/README.es.md @@ -70,8 +70,8 @@ OpenCode también está disponible como aplicación de escritorio. Descárgala d | Plataforma | Descarga | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, o AppImage | diff --git a/README.fr.md b/README.fr.md index c1fca23376..30e089cd6f 100644 --- a/README.fr.md +++ b/README.fr.md @@ -70,8 +70,8 @@ OpenCode est aussi disponible en application de bureau. Téléchargez-la directe | Plateforme | Téléchargement | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, ou AppImage | diff --git a/README.gr.md b/README.gr.md index 2b2c2679d8..d7e9885a2d 100644 --- a/README.gr.md +++ b/README.gr.md @@ -70,8 +70,8 @@ nix run nixpkgs#opencode # ή github:anomalyco/opencode με βάση | Πλατφόρμα | Λήψη | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, ή AppImage | diff --git a/README.it.md b/README.it.md index 3e516a9027..03b6f2427d 100644 --- a/README.it.md +++ b/README.it.md @@ -70,8 +70,8 @@ OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla diretta | Piattaforma | Download | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, oppure AppImage | diff --git a/README.ja.md b/README.ja.md index 144dc7b6f8..d5c68d8c3f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -70,8 +70,8 @@ OpenCode はデスクトップアプリとしても利用できます。[release | プラットフォーム | ダウンロード | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`、`.rpm`、または AppImage | diff --git a/README.ko.md b/README.ko.md index 32defc0a5e..b8b4b5164a 100644 --- a/README.ko.md +++ b/README.ko.md @@ -70,8 +70,8 @@ OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https:// | 플랫폼 | 다운로드 | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, 또는 AppImage | diff --git a/README.md b/README.md index 3ebfb1627c..ccce3e97bb 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download). -| Platform | Download | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, or AppImage | +| Platform | Download | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, or `.AppImage` | ```bash # macOS (Homebrew) diff --git a/README.no.md b/README.no.md index c3348286b2..866de55d4f 100644 --- a/README.no.md +++ b/README.no.md @@ -70,8 +70,8 @@ OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [release | Plattform | Nedlasting | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` eller AppImage | diff --git a/README.pl.md b/README.pl.md index 4c5a076656..468a5a5edb 100644 --- a/README.pl.md +++ b/README.pl.md @@ -70,8 +70,8 @@ OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośred | Platforma | Pobieranie | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` lub AppImage | diff --git a/README.ru.md b/README.ru.md index e507be70e6..c19175cca6 100644 --- a/README.ru.md +++ b/README.ru.md @@ -70,8 +70,8 @@ OpenCode также доступен как десктопное приложе | Платформа | Загрузка | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` или AppImage | diff --git a/README.th.md b/README.th.md index 4a4ea62c95..b68a7cd6f3 100644 --- a/README.th.md +++ b/README.th.md @@ -70,8 +70,8 @@ OpenCode มีให้ใช้งานเป็นแอปพลิเค | แพลตฟอร์ม | ดาวน์โหลด | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, หรือ AppImage | diff --git a/README.tr.md b/README.tr.md index e88b40f875..7657a846c9 100644 --- a/README.tr.md +++ b/README.tr.md @@ -70,8 +70,8 @@ OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm | Platform | İndirme | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` veya AppImage | diff --git a/README.uk.md b/README.uk.md index a1a0259b6d..331637862c 100644 --- a/README.uk.md +++ b/README.uk.md @@ -70,8 +70,8 @@ OpenCode також доступний як десктопний застосу | Платформа | Завантаження | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm` або AppImage | diff --git a/README.vi.md b/README.vi.md index 0932c50f78..166daa25e0 100644 --- a/README.vi.md +++ b/README.vi.md @@ -70,8 +70,8 @@ OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiế | Nền tảng | Tải xuống | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, hoặc AppImage | diff --git a/README.zh.md b/README.zh.md index 46d9f761cb..0366a0868b 100644 --- a/README.zh.md +++ b/README.zh.md @@ -70,8 +70,8 @@ OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](htt | 平台 | 下载文件 | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`、`.rpm` 或 AppImage | diff --git a/README.zht.md b/README.zht.md index 7ef51d8fdd..721623e72c 100644 --- a/README.zht.md +++ b/README.zht.md @@ -70,8 +70,8 @@ OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (rele | 平台 | 下載連結 | | --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` | -| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | | Windows | `opencode-desktop-windows-x64.exe` | | Linux | `.deb`, `.rpm`, 或 AppImage | From 2f05676e0470292633cbfb1feb96a1d6bf555245 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 6 May 2026 04:17:05 +0000 Subject: [PATCH 18/70] chore: generate --- README.ar.md | 10 +++++----- README.bn.md | 10 +++++----- README.br.md | 10 +++++----- README.bs.md | 10 +++++----- README.da.md | 10 +++++----- README.de.md | 10 +++++----- README.es.md | 10 +++++----- README.fr.md | 10 +++++----- README.gr.md | 10 +++++----- README.it.md | 10 +++++----- README.ja.md | 10 +++++----- README.ko.md | 10 +++++----- README.no.md | 10 +++++----- README.pl.md | 10 +++++----- README.ru.md | 10 +++++----- README.th.md | 10 +++++----- README.tr.md | 10 +++++----- README.uk.md | 10 +++++----- README.vi.md | 10 +++++----- README.zh.md | 10 +++++----- README.zht.md | 10 +++++----- 21 files changed, 105 insertions(+), 105 deletions(-) diff --git a/README.ar.md b/README.ar.md index e6781325f2..a590f1ca58 100644 --- a/README.ar.md +++ b/README.ar.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # او github:anomalyco/opencode لاحدث يتوفر OpenCode ايضا كتطبيق سطح مكتب. قم بالتنزيل مباشرة من [صفحة الاصدارات](https://github.com/anomalyco/opencode/releases) او من [opencode.ai/download](https://opencode.ai/download). -| المنصة | التنزيل | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| المنصة | التنزيل | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb` او `.rpm` او AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb` او `.rpm` او AppImage | ```bash # macOS (Homebrew) diff --git a/README.bn.md b/README.bn.md index b6d981e383..b80b1e202c 100644 --- a/README.bn.md +++ b/README.bn.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev OpenCode ডেস্কটপ অ্যাপ্লিকেশন হিসেবেও উপলব্ধ। সরাসরি [রিলিজ পেজ](https://github.com/anomalyco/opencode/releases) অথবা [opencode.ai/download](https://opencode.ai/download) থেকে ডাউনলোড করুন। -| প্ল্যাটফর্ম | ডাউনলোড | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| প্ল্যাটফর্ম | ডাউনলোড | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, or `.AppImage` | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, or `.AppImage` | ```bash # macOS (Homebrew) diff --git a/README.br.md b/README.br.md index ae01949394..60a9e72f70 100644 --- a/README.br.md +++ b/README.br.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode para a branch O OpenCode também está disponível como aplicativo desktop. Baixe diretamente pela [página de releases](https://github.com/anomalyco/opencode/releases) ou em [opencode.ai/download](https://opencode.ai/download). -| Plataforma | Download | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Plataforma | Download | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` ou AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` ou AppImage | ```bash # macOS (Homebrew) diff --git a/README.bs.md b/README.bs.md index c2035bb1b3..4c3083c4c0 100644 --- a/README.bs.md +++ b/README.bs.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ili github:anomalyco/opencode za najnoviji OpenCode je dostupan i kao desktop aplikacija. Preuzmi je direktno sa [stranice izdanja](https://github.com/anomalyco/opencode/releases) ili sa [opencode.ai/download](https://opencode.ai/download). -| Platforma | Preuzimanje | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Platforma | Preuzimanje | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, ili AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ili AppImage | ```bash # macOS (Homebrew) diff --git a/README.da.md b/README.da.md index a89cb21193..c7a99f7d89 100644 --- a/README.da.md +++ b/README.da.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste OpenCode findes også som desktop-app. Download direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). -| Platform | Download | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Platform | Download | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, eller AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, eller AppImage | ```bash # macOS (Homebrew) diff --git a/README.de.md b/README.de.md index 41f651849b..340cbe5bd3 100644 --- a/README.de.md +++ b/README.de.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oder github:anomalyco/opencode für den neu OpenCode ist auch als Desktop-Anwendung verfügbar. Lade sie direkt von der [Releases-Seite](https://github.com/anomalyco/opencode/releases) oder [opencode.ai/download](https://opencode.ai/download) herunter. -| Plattform | Download | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Plattform | Download | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` oder AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` oder AppImage | ```bash # macOS (Homebrew) diff --git a/README.es.md b/README.es.md index 20f749fbb6..9180e689fc 100644 --- a/README.es.md +++ b/README.es.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # o github:anomalyco/opencode para la rama de OpenCode también está disponible como aplicación de escritorio. Descárgala directamente desde la [página de releases](https://github.com/anomalyco/opencode/releases) o desde [opencode.ai/download](https://opencode.ai/download). -| Plataforma | Descarga | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Plataforma | Descarga | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, o AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, o AppImage | ```bash # macOS (Homebrew) diff --git a/README.fr.md b/README.fr.md index 30e089cd6f..8ca10b080d 100644 --- a/README.fr.md +++ b/README.fr.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ou github:anomalyco/opencode pour la branch OpenCode est aussi disponible en application de bureau. Téléchargez-la directement depuis la [page des releases](https://github.com/anomalyco/opencode/releases) ou [opencode.ai/download](https://opencode.ai/download). -| Plateforme | Téléchargement | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Plateforme | Téléchargement | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, ou AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ou AppImage | ```bash # macOS (Homebrew) diff --git a/README.gr.md b/README.gr.md index d7e9885a2d..6f7c67b30e 100644 --- a/README.gr.md +++ b/README.gr.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # ή github:anomalyco/opencode με βάση Το OpenCode είναι επίσης διαθέσιμο ως εφαρμογή. Κατέβασε το απευθείας από τη [σελίδα εκδόσεων](https://github.com/anomalyco/opencode/releases) ή το [opencode.ai/download](https://opencode.ai/download). -| Πλατφόρμα | Λήψη | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Πλατφόρμα | Λήψη | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, ή AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, ή AppImage | ```bash # macOS (Homebrew) diff --git a/README.it.md b/README.it.md index 03b6f2427d..d17de67987 100644 --- a/README.it.md +++ b/README.it.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # oppure github:anomalyco/opencode per l’ul OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download). -| Piattaforma | Download | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Piattaforma | Download | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, oppure AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, oppure AppImage | ```bash # macOS (Homebrew) diff --git a/README.ja.md b/README.ja.md index d5c68d8c3f..4002433824 100644 --- a/README.ja.md +++ b/README.ja.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # または github:anomalyco/opencode で最 OpenCode はデスクトップアプリとしても利用できます。[releases page](https://github.com/anomalyco/opencode/releases) から直接ダウンロードするか、[opencode.ai/download](https://opencode.ai/download) を利用してください。 -| プラットフォーム | ダウンロード | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| プラットフォーム | ダウンロード | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`、`.rpm`、または AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`、`.rpm`、または AppImage | ```bash # macOS (Homebrew) diff --git a/README.ko.md b/README.ko.md index b8b4b5164a..5b7329db05 100644 --- a/README.ko.md +++ b/README.ko.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 또는 github:anomalyco/opencode 로 최신 OpenCode 는 데스크톱 앱으로도 제공됩니다. [releases page](https://github.com/anomalyco/opencode/releases) 에서 직접 다운로드하거나 [opencode.ai/download](https://opencode.ai/download) 를 이용하세요. -| 플랫폼 | 다운로드 | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| 플랫폼 | 다운로드 | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, 또는 AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 또는 AppImage | ```bash # macOS (Homebrew) diff --git a/README.no.md b/README.no.md index 866de55d4f..6abd214d64 100644 --- a/README.no.md +++ b/README.no.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # eller github:anomalyco/opencode for nyeste OpenCode er også tilgjengelig som en desktop-app. Last ned direkte fra [releases-siden](https://github.com/anomalyco/opencode/releases) eller [opencode.ai/download](https://opencode.ai/download). -| Plattform | Nedlasting | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Plattform | Nedlasting | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` eller AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` eller AppImage | ```bash # macOS (Homebrew) diff --git a/README.pl.md b/README.pl.md index 468a5a5edb..0beb6d996b 100644 --- a/README.pl.md +++ b/README.pl.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # lub github:anomalyco/opencode dla najnowsze OpenCode jest także dostępny jako aplikacja desktopowa. Pobierz ją bezpośrednio ze strony [releases](https://github.com/anomalyco/opencode/releases) lub z [opencode.ai/download](https://opencode.ai/download). -| Platforma | Pobieranie | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Platforma | Pobieranie | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` lub AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` lub AppImage | ```bash # macOS (Homebrew) diff --git a/README.ru.md b/README.ru.md index c19175cca6..c5f9eceda5 100644 --- a/README.ru.md +++ b/README.ru.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # или github:anomalyco/opencode для с OpenCode также доступен как десктопное приложение. Скачайте его со [страницы релизов](https://github.com/anomalyco/opencode/releases) или с [opencode.ai/download](https://opencode.ai/download). -| Платформа | Загрузка | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Платформа | Загрузка | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` или AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` или AppImage | ```bash # macOS (Homebrew) diff --git a/README.th.md b/README.th.md index b68a7cd6f3..3781b028f8 100644 --- a/README.th.md +++ b/README.th.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # หรือ github:anomalyco/opencode ส OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download) -| แพลตฟอร์ม | ดาวน์โหลด | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| แพลตฟอร์ม | ดาวน์โหลด | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, หรือ AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, หรือ AppImage | ```bash # macOS (Homebrew) diff --git a/README.tr.md b/README.tr.md index 7657a846c9..15fc79233d 100644 --- a/README.tr.md +++ b/README.tr.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # veya en güncel geliştirme dalı için git OpenCode ayrıca masaüstü uygulaması olarak da mevcuttur. Doğrudan [sürüm sayfasından](https://github.com/anomalyco/opencode/releases) veya [opencode.ai/download](https://opencode.ai/download) adresinden indirebilirsiniz. -| Platform | İndirme | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Platform | İndirme | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` veya AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` veya AppImage | ```bash # macOS (Homebrew) diff --git a/README.uk.md b/README.uk.md index 331637862c..987dd784ee 100644 --- a/README.uk.md +++ b/README.uk.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # або github:anomalyco/opencode для н OpenCode також доступний як десктопний застосунок. Завантажуйте напряму зі [сторінки релізів](https://github.com/anomalyco/opencode/releases) або [opencode.ai/download](https://opencode.ai/download). -| Платформа | Завантаження | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Платформа | Завантаження | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm` або AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm` або AppImage | ```bash # macOS (Homebrew) diff --git a/README.vi.md b/README.vi.md index 166daa25e0..a2f9c3708c 100644 --- a/README.vi.md +++ b/README.vi.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # hoặc github:anomalyco/opencode cho nhánh OpenCode cũng có sẵn dưới dạng ứng dụng desktop. Tải trực tiếp từ [trang releases](https://github.com/anomalyco/opencode/releases) hoặc [opencode.ai/download](https://opencode.ai/download). -| Nền tảng | Tải xuống | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| Nền tảng | Tải xuống | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, hoặc AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, hoặc AppImage | ```bash # macOS (Homebrew) diff --git a/README.zh.md b/README.zh.md index 0366a0868b..99b701b896 100644 --- a/README.zh.md +++ b/README.zh.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或用 github:anomalyco/opencode 获取最 OpenCode 也提供桌面版应用。可直接从 [发布页 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下载。 -| 平台 | 下载文件 | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| 平台 | 下载文件 | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`、`.rpm` 或 AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`、`.rpm` 或 AppImage | ```bash # macOS (Homebrew Cask) diff --git a/README.zht.md b/README.zht.md index 721623e72c..1d31e1a591 100644 --- a/README.zht.md +++ b/README.zht.md @@ -68,12 +68,12 @@ nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取 OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。 -| 平台 | 下載連結 | -| --------------------- | ------------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | +| 平台 | 下載連結 | +| --------------------- | ---------------------------------- | +| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | | macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, 或 AppImage | +| Windows | `opencode-desktop-windows-x64.exe` | +| Linux | `.deb`, `.rpm`, 或 AppImage | ```bash # macOS (Homebrew Cask) From efd8024430f8ec6d90086688bf6bb259a1b5af4c Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 12:30:20 +0800 Subject: [PATCH 19/70] feat(desktop): add OPENCODE_TEST_ONBOARDING env (#25968) --- packages/desktop/src/main/index.ts | 35 ++++++++++++++++++++++++----- packages/desktop/src/main/server.ts | 2 +- packages/desktop/src/main/store.ts | 8 ++++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index a1eba8b98d..cbac5aa449 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -1,9 +1,9 @@ import { randomUUID } from "node:crypto" import { EventEmitter } from "node:events" -import { existsSync } from "node:fs" +import { existsSync, mkdirSync, rmSync } from "node:fs" import * as http from "node:http" import { createServer } from "node:net" -import { homedir } from "node:os" +import { homedir, tmpdir } from "node:os" import { join } from "node:path" import { getCACertificates, setDefaultCACertificates } from "node:tls" import type { Event } from "electron" @@ -30,10 +30,17 @@ const APP_IDS: Record = { beta: "ai.opencode.desktop.beta", prod: "ai.opencode.desktop", } +const TEST_ONBOARDING = process.env.OPENCODE_TEST_ONBOARDING === "1" const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" +const onboardingTestRoot = setupOnboardingTestEnv() app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev") app.setAppUserModelId(appId) -app.setPath("userData", join(app.getPath("appData"), appId)) +app.setPath( + "userData", + onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId), +) +if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session")) +const logger = initLogging() const { autoUpdater } = pkg import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types" @@ -65,13 +72,29 @@ const loadingComplete = defer() const pendingDeepLinks: string[] = [] const serverReady = defer() -const logger = initLogging() useSystemCertificates() +function setupOnboardingTestEnv() { + if (!TEST_ONBOARDING) return + + const root = join(tmpdir(), `opencode-onboarding-${randomUUID()}`) + rmSync(root, { recursive: true, force: true }) + ;["data", "config", "cache", "state", "desktop", "session"].forEach((dir) => + mkdirSync(join(root, dir), { recursive: true }), + ) + process.env.OPENCODE_DB = ":memory:" + process.env.XDG_DATA_HOME = join(root, "data") + process.env.XDG_CONFIG_HOME = join(root, "config") + process.env.XDG_CACHE_HOME = join(root, "cache") + process.env.XDG_STATE_HOME = join(root, "state") + return root +} + logger.log("app starting", { version: app.getVersion(), packaged: app.isPackaged, + onboardingTest: Boolean(onboardingTestRoot), }) setupApp() @@ -118,7 +141,7 @@ function setupApp() { } void app.whenReady().then(async () => { - migrate() + if (!TEST_ONBOARDING) migrate() app.setAsDefaultProtocolClient("opencode") registerRendererProtocol() setDockIcon() @@ -344,6 +367,8 @@ async function getSidecarPort() { } function sqliteFileExists() { + if (process.env.OPENCODE_DB === ":memory:") return true + const xdg = process.env.XDG_DATA_HOME const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".local", "share") return existsSync(join(base, "opencode", "opencode.db")) diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index fab09eb1b1..4b8cb04943 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -70,7 +70,7 @@ function prepareServerEnv(password: string) { OPENCODE_CLIENT: "desktop", OPENCODE_SERVER_USERNAME: "opencode", OPENCODE_SERVER_PASSWORD: password, - XDG_STATE_HOME: app.getPath("userData"), + XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? app.getPath("userData"), } Object.assign(process.env, env) } diff --git a/packages/desktop/src/main/store.ts b/packages/desktop/src/main/store.ts index 7b3bd7c660..a591f878de 100644 --- a/packages/desktop/src/main/store.ts +++ b/packages/desktop/src/main/store.ts @@ -1,4 +1,5 @@ import Store from "electron-store" +import { app } from "electron" import { SETTINGS_STORE } from "./constants" @@ -11,7 +12,12 @@ const cache = new Map() export function getStore(name = SETTINGS_STORE) { const cached = cache.get(name) if (cached) return cached - const next = new Store({ name, fileExtension: "", accessPropertiesByDotNotation: false }) + const next = new Store({ + name, + cwd: app.getPath("userData"), + fileExtension: "", + accessPropertiesByDotNotation: false, + }) cache.set(name, next) return next } From b4c60e1b213c899fce258e8fdc9978924ee01740 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 6 May 2026 04:32:49 +0000 Subject: [PATCH 20/70] chore: generate --- packages/desktop/src/main/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index cbac5aa449..1360c29523 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -35,10 +35,7 @@ const appId = app.isPackaged ? APP_IDS[CHANNEL] : "ai.opencode.desktop.dev" const onboardingTestRoot = setupOnboardingTestEnv() app.setName(app.isPackaged ? APP_NAMES[CHANNEL] : "OpenCode Dev") app.setAppUserModelId(appId) -app.setPath( - "userData", - onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId), -) +app.setPath("userData", onboardingTestRoot ? join(onboardingTestRoot, "desktop") : join(app.getPath("appData"), appId)) if (onboardingTestRoot) app.setPath("sessionData", join(onboardingTestRoot, "session")) const logger = initLogging() const { autoUpdater } = pkg From 89afac3d9d8cf57a47facfb619a8262877c1474d Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 6 May 2026 12:39:52 +0800 Subject: [PATCH 21/70] go: restore Kimi K2.6 limits (#25969) --- packages/console/app/src/i18n/ar.ts | 2 -- packages/console/app/src/i18n/br.ts | 2 -- packages/console/app/src/i18n/da.ts | 2 -- packages/console/app/src/i18n/de.ts | 2 -- packages/console/app/src/i18n/en.ts | 2 -- packages/console/app/src/i18n/es.ts | 2 -- packages/console/app/src/i18n/fr.ts | 2 -- packages/console/app/src/i18n/it.ts | 2 -- packages/console/app/src/i18n/ja.ts | 2 -- packages/console/app/src/i18n/ko.ts | 2 -- packages/console/app/src/i18n/no.ts | 2 -- packages/console/app/src/i18n/pl.ts | 2 -- packages/console/app/src/i18n/ru.ts | 2 -- packages/console/app/src/i18n/th.ts | 2 -- packages/console/app/src/i18n/tr.ts | 2 -- packages/console/app/src/i18n/zh.ts | 2 -- packages/console/app/src/i18n/zht.ts | 2 -- packages/console/app/src/routes/go/index.css | 35 -------------------- packages/console/app/src/routes/go/index.tsx | 22 ++---------- 19 files changed, 2 insertions(+), 89 deletions(-) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 5c0919e8e2..42258db866 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -261,8 +261,6 @@ export const dict = { "go.cta.promo": "$5 للشهر الأول", "go.pricing.body": "استخدمه مع أي وكيل. $5 للشهر الأول، ثم $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: حد الاستخدام 3 أضعاف حتى 27 أبريل", "go.graph.free": "مجاني", "go.graph.freePill": "Big Pickle ونماذج مجانية", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index 76e6987d3e..a848ba38da 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -265,8 +265,6 @@ export const dict = { "go.cta.promo": "$5 no primeiro mês", "go.pricing.body": "Use com qualquer agente. $5 no primeiro mês, depois $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: limite de uso 3x maior até 27 de abril", "go.graph.free": "Grátis", "go.graph.freePill": "Big Pickle e modelos gratuitos", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index b97ee2cc0a..c54aca32e1 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -263,8 +263,6 @@ export const dict = { "go.cta.promo": "$5 første måned", "go.pricing.body": "Brug med enhver agent. $5 første måned, derefter $10/måned. Tank op med kredit efter behov. Afmeld når som helst.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: brugsgrænsen tredoblet til 27. april", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 33b6e1b3de..6e14778de8 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -265,8 +265,6 @@ export const dict = { "go.cta.promo": "$5 im ersten Monat", "go.pricing.body": "Mit jedem Agenten nutzbar. $5 im ersten Monat, danach $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: Nutzungslimit bis zum 27. April verdreifacht", "go.graph.free": "Kostenlos", "go.graph.freePill": "Big Pickle und kostenlose Modelle", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index b6934b94de..0d0869da53 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -249,8 +249,6 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.meta.description": "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6 gets 3× usage limits through April 27", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index c5cc71ae1e..fd13a54de6 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -266,8 +266,6 @@ export const dict = { "go.cta.promo": "$5 el primer mes", "go.pricing.body": "Úsalo con cualquier agente. $5 el primer mes, luego 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: límite de uso triplicado hasta el 27 de abril", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle y modelos gratuitos", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 04e6e3bc62..3762915abf 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -267,8 +267,6 @@ export const dict = { "go.cta.promo": "$5 le premier mois", "go.pricing.body": "Utilisez-le avec n'importe quel agent. $5 le premier mois, puis 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6 : limites d’utilisation triplées jusqu’au 27 avril", "go.graph.free": "Gratuit", "go.graph.freePill": "Big Pickle et modèles gratuits", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 13f33bfc39..04d0e2451c 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -263,8 +263,6 @@ export const dict = { "go.cta.promo": "$5 il primo mese", "go.pricing.body": "Usalo con qualsiasi agente. $5 il primo mese, poi $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: limite d'uso triplicato fino al 27 aprile", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle e modelli gratuiti", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 845faebf61..71404c91eb 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -262,8 +262,6 @@ export const dict = { "go.cta.promo": "初月 $5", "go.pricing.body": "どのエージェントでも使えます。最初の月$5、その後$10/月。必要に応じてクレジットを追加。いつでもキャンセルできます。", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6、4月27日まで利用上限が3倍に", "go.graph.free": "無料", "go.graph.freePill": "Big Pickleと無料モデル", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 7efe563a07..6a7d52bbd5 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -259,8 +259,6 @@ export const dict = { "go.cta.promo": "첫 달 $5", "go.pricing.body": "어떤 에이전트와도 사용할 수 있습니다. 첫 달 $5, 이후 $10/월. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6, 4월 27일까지 사용 한도 3배 확대", "go.graph.free": "무료", "go.graph.freePill": "Big Pickle 및 무료 모델", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 8948e158b0..629e690c64 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -263,8 +263,6 @@ export const dict = { "go.cta.promo": "$5 første måned", "go.pricing.body": "Bruk med hvilken som helst agent. $5 første måned, deretter $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: bruksgrensen er tredoblet til 27. april", "go.graph.free": "Gratis", "go.graph.freePill": "Big Pickle og gratis modeller", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index f879ed7057..0f465df9d9 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -264,8 +264,6 @@ export const dict = { "go.cta.promo": "$5 pierwszy miesiąc", "go.pricing.body": "Używaj z dowolnym agentem. $5 za pierwszy miesiąc, potem $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: limit użycia zwiększony 3× do 27 kwietnia", "go.graph.free": "Darmowe", "go.graph.freePill": "Big Pickle i darmowe modele", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 9ba36d2208..90019dbe54 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -267,8 +267,6 @@ export const dict = { "go.cta.promo": "$5 первый месяц", "go.pricing.body": "Используйте с любым агентом. $5 за первый месяц, затем $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: лимит использования увеличен в 3 раза до 27 апреля", "go.graph.free": "Бесплатно", "go.graph.freePill": "Big Pickle и бесплатные модели", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 01b2b19c39..9f210ada49 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -261,8 +261,6 @@ export const dict = { "go.cta.price": "$10/เดือน", "go.cta.promo": "$5 เดือนแรก", "go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $5 ในเดือนแรก จากนั้น $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6 โควตาการใช้งานเพิ่มเป็น 3 เท่า ถึง 27 เม.ย.", "go.graph.free": "ฟรี", "go.graph.freePill": "Big Pickle และโมเดลฟรี", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 0345277b87..3d2f8f39de 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -265,8 +265,6 @@ export const dict = { "go.cta.promo": "İlk ay $5", "go.pricing.body": "Herhangi bir ajanla kullanın. İlk ay $5, sonrasında ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6: kullanım limiti 27 Nisan'a kadar 3 katına çıktı", "go.graph.free": "Ücretsiz", "go.graph.freePill": "Big Pickle ve ücretsiz modeller", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index b9300cc87e..fdcb7d37a0 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -252,8 +252,6 @@ export const dict = { "go.cta.price": "$10/月", "go.cta.promo": "首月 $5", "go.pricing.body": "可配合任何代理使用。首月 $5,之后 $10/月。如有需要可充值。随时取消。", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6 使用额度提升至 3 倍,限时至 4 月 27 日", "go.graph.free": "免费", "go.graph.freePill": "Big Pickle 和免费模型", "go.graph.go": "Go", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index f129a99d02..bfbfcf7e81 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -252,8 +252,6 @@ export const dict = { "go.cta.price": "$10/月", "go.cta.promo": "首月 $5", "go.pricing.body": "可搭配任何代理使用。首月 $5,之後 $10/月。如有需要可儲值。隨時取消。", - "go.banner.badge": "3x", - "go.banner.text": "Kimi K2.6 使用額度提升至 3 倍,限時至 4 月 27 日", "go.graph.free": "免費", "go.graph.freePill": "Big Pickle 與免費模型", "go.graph.go": "Go", diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css index de8dce4724..25ae00e5f8 100644 --- a/packages/console/app/src/routes/go/index.css +++ b/packages/console/app/src/routes/go/index.css @@ -326,37 +326,6 @@ body { } } - [data-component="desktop-app-banner"] { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 32px; - - [data-slot="badge"] { - background: var(--color-background-strong); - color: var(--color-text-inverted); - font-weight: 500; - padding: 4px 8px; - line-height: 1; - flex-shrink: 0; - } - - [data-slot="content"] { - display: flex; - align-items: center; - gap: 1ch; - } - - [data-slot="text"] { - color: var(--color-text-strong); - line-height: 1.4; - - @media (max-width: 30.625rem) { - display: none; - } - } - } - [data-slot="hero-copy"] { img { margin-bottom: 24px; @@ -662,10 +631,6 @@ body { fill: var(--color-text-strong); } - [data-bar][data-kind="promo"] { - fill: color-mix(in srgb, var(--bar-go) 50%, transparent); - } - [data-val] { fill: var(--color-text-strong); font-size: 13px; diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 67ae58ae88..1ec83b25fe 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -63,7 +63,7 @@ function LimitsGraph(props: { href: string }) { const free = 200 const graph = [ { id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" }, - { id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" }, + { id: "kimi-k2.6", name: "Kimi K2.6", req: 1150, d: "150ms" }, { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" }, { id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" }, { id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" }, @@ -157,24 +157,12 @@ function LimitsGraph(props: { href: string }) { - {m.baseReq && ( - - )} )} @@ -264,12 +252,6 @@ export default function Home() {
-
- {i18n.t("home.banner.badge")} -
- {i18n.t("go.banner.text")} -
-
From 7c8cf6ca5be788b65598d565acb0c6511e6f60d9 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 12:44:40 +0800 Subject: [PATCH 22/70] fix(desktop): suppress browser API Sentry errors in prod (#25972) --- packages/desktop/src/renderer/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 97c7ed23a2..f9114c7550 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -43,7 +43,11 @@ if (import.meta.env.VITE_SENTRY_DSN) { integrations: (integrations) => { return integrations.filter( (i) => - i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"), + i.name !== "Breadcrumbs" && + !( + import.meta.env.OPENCODE_CHANNEL === "prod" && + (i.name === "GlobalHandlers" || i.name === "BrowserApiErrors") + ), ) }, }) From 9d178e094437dc8698e749e02b8dcd105c2824a3 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 May 2026 01:05:09 -0400 Subject: [PATCH 23/70] sync --- packages/console/app/src/routes/incident/webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/incident/webhook.ts b/packages/console/app/src/routes/incident/webhook.ts index 3f4aa5f7ce..62ee202743 100644 --- a/packages/console/app/src/routes/incident/webhook.ts +++ b/packages/console/app/src/routes/incident/webhook.ts @@ -37,7 +37,7 @@ const postDiscordMessage = async (incident: Incident) => { `**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`, incident.summary, "", - "@everyone", + "@inference", "", incident.permalink, ] From acca2e92dcd6cd2548a28929407e49a5ce596656 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 14:39:20 +0800 Subject: [PATCH 24/70] fix(desktop): disable auto install on app quit (#25976) --- packages/desktop/src/main/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 1360c29523..d3c8fcc04e 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -378,7 +378,7 @@ function setupAutoUpdater() { autoUpdater.allowPrerelease = false autoUpdater.allowDowngrade = true autoUpdater.autoDownload = false - autoUpdater.autoInstallOnAppQuit = true + autoUpdater.autoInstallOnAppQuit = false logger.log("auto updater configured", { channel: autoUpdater.channel, allowPrerelease: autoUpdater.allowPrerelease, From 754a1fb712e2b79c8786e27af58f7ae0c1e34d65 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 15:30:18 +0800 Subject: [PATCH 25/70] fix(desktop): suppress EPIPE errors in console transport (#25980) --- packages/desktop/src/main/logging.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/desktop/src/main/logging.ts b/packages/desktop/src/main/logging.ts index d315b2d344..1f1c5e54e3 100644 --- a/packages/desktop/src/main/logging.ts +++ b/packages/desktop/src/main/logging.ts @@ -7,6 +7,7 @@ const TAIL_LINES = 1000 export function initLogging() { log.transports.file.maxSize = 5 * 1024 * 1024 + initConsoleTransport() cleanup() return log } @@ -38,3 +39,19 @@ function cleanup() { } } } + +function initConsoleTransport() { + const write = log.transports.console.writeFn.bind(log.transports.console) + log.transports.console.writeFn = (options) => { + try { + write(options) + } catch (err) { + if (!isBrokenPipe(err)) throw err + log.transports.console.level = false + } + } +} + +function isBrokenPipe(err: unknown) { + return typeof err === "object" && err !== null && "code" in err && err.code === "EPIPE" +} From c235ba1bef3f06530176974d854401389255f4dd Mon Sep 17 00:00:00 2001 From: Guiii <68971828+kill74@users.noreply.github.com> Date: Wed, 6 May 2026 08:56:38 +0100 Subject: [PATCH 26/70] docs: fix CLI attach section order (#25749) --- packages/web/src/content/docs/cli.mdx | 62 +++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 8ecb6a6eb9..ac8a1a3044 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -61,37 +61,6 @@ opencode agent [command] --- -### attach - -Attach a terminal to an already running OpenCode backend server started via `serve` or `web` commands. - -```bash -opencode attach [url] -``` - -This allows using the TUI with a remote OpenCode backend. For example: - -```bash -# Start the backend server for web/mobile access -opencode web --port 4096 --hostname 0.0.0.0 - -# In another terminal, attach the TUI to the running backend -opencode attach http://10.20.30.40:4096 -``` - -#### Flags - -| Flag | Short | Description | -| ---------------------------------------- | ----- | -------------------------------------------------------------------------- | -| {"--dir"} | | Working directory to start TUI in | -| {"--continue"} | `-c` | Continue the last session | -| {"--session"} | `-s` | Session ID to continue | -| {"--fork"} | | Fork the session when continuing (use with `--continue` or `--session`) | -| {"--password"} | `-p` | Basic auth password (defaults to `OPENCODE_SERVER_PASSWORD`) | -| {"--username"} | `-u` | Basic auth username (defaults to `OPENCODE_SERVER_USERNAME` or `opencode`) | - ---- - #### create Create a new agent with custom configuration. @@ -126,6 +95,37 @@ opencode agent list --- +### attach + +Attach a terminal to an already running OpenCode backend server started via `serve` or `web` commands. + +```bash +opencode attach [url] +``` + +This allows using the TUI with a remote OpenCode backend. For example: + +```bash +# Start the backend server for web/mobile access +opencode web --port 4096 --hostname 0.0.0.0 + +# In another terminal, attach the TUI to the running backend +opencode attach http://10.20.30.40:4096 +``` + +#### Flags + +| Flag | Short | Description | +| ---------------------------------------- | ----- | -------------------------------------------------------------------------- | +| {"--dir"} | | Working directory to start TUI in | +| {"--continue"} | `-c` | Continue the last session | +| {"--session"} | `-s` | Session ID to continue | +| {"--fork"} | | Fork the session when continuing (use with `--continue` or `--session`) | +| {"--password"} | `-p` | Basic auth password (defaults to `OPENCODE_SERVER_PASSWORD`) | +| {"--username"} | `-u` | Basic auth username (defaults to `OPENCODE_SERVER_USERNAME` or `opencode`) | + +--- + ### auth Command to manage credentials and login for providers. From 518503b29ba9826af296e7c089c400ad99d581bf Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Wed, 6 May 2026 05:06:37 -0300 Subject: [PATCH 27/70] fix(ui): preserve SVG tags in DOMPurify config for KaTeX math rendering (#25866) --- packages/ui/src/components/markdown.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 56e2d9d709..7ee73af10f 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -33,6 +33,8 @@ const config = { SANITIZE_NAMED_PROPS: true, FORBID_TAGS: ["style"], FORBID_CONTENTS: ["style", "script"], + ADD_TAGS: ["svg", "path"], + ADD_ATTR: ["d", "viewBox", "preserveAspectRatio", "xmlns"], } const iconPaths = { From 901d1171a6a3d987aa7e8afe1b8149dd60091e68 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 16:37:10 +0800 Subject: [PATCH 28/70] chore(desktop): add @parcel/watcher platform packages to optionalDependencies (#25996) --- bun.lock | 8 ++++++++ packages/desktop/package.json | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 35075c1441..77ad4d982f 100644 --- a/bun.lock +++ b/bun.lock @@ -271,6 +271,14 @@ "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", "@lydell/node-pty-win32-x64": "1.2.0-beta.10", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1", }, }, "packages/enterprise": { diff --git a/packages/desktop/package.json b/packages/desktop/package.json index cbc20b9061..60ccd6cfb6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -63,6 +63,14 @@ "@lydell/node-pty-linux-arm64": "1.2.0-beta.10", "@lydell/node-pty-linux-x64": "1.2.0-beta.10", "@lydell/node-pty-win32-arm64": "1.2.0-beta.10", - "@lydell/node-pty-win32-x64": "1.2.0-beta.10" + "@lydell/node-pty-win32-x64": "1.2.0-beta.10", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" } } From 043a5c7c0dc43219a0268969baac3deee43008ef Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 6 May 2026 16:40:45 +0800 Subject: [PATCH 29/70] feat(desktop): implement clipboard write permission handling (#25998) --- packages/desktop/src/main/windows.ts | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/desktop/src/main/windows.ts b/packages/desktop/src/main/windows.ts index 387e793b0e..41abfc784d 100644 --- a/packages/desktop/src/main/windows.ts +++ b/packages/desktop/src/main/windows.ts @@ -8,6 +8,7 @@ const root = dirname(fileURLToPath(import.meta.url)) const rendererRoot = join(root, "../renderer") const rendererProtocol = "oc" const rendererHost = "renderer" +const clipboardWritePermission = "clipboard-sanitized-write" protocol.registerSchemesAsPrivileged([ { @@ -107,6 +108,8 @@ export function createMainWindow() { }, }) + allowClipboardWrite(win) + win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { const { requestHeaders } = details upsertKeyValue(requestHeaders, "Access-Control-Allow-Origin", ["*"]) @@ -157,6 +160,8 @@ export function createLoadingWindow() { }, }) + allowClipboardWrite(win) + loadWindow(win, "loading.html") return win @@ -191,6 +196,31 @@ function loadWindow(win: BrowserWindow, html: string) { void win.loadURL(`${rendererProtocol}://${rendererHost}/${html}`) } + +function allowClipboardWrite(win: BrowserWindow) { + win.webContents.session.setPermissionRequestHandler((webContents, permission, callback, details) => { + callback( + permission === clipboardWritePermission && + isTrustedRendererUrl(details.requestingUrl) && + webContents.id === win.webContents.id, + ) + }) + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission !== clipboardWritePermission) return false + if (webContents && webContents.id !== win.webContents.id) return false + return isTrustedRendererUrl(details.requestingUrl) || isTrustedRendererUrl(requestingOrigin) + }) +} + +function isTrustedRendererUrl(value?: string) { + if (!value || !URL.canParse(value)) return false + const url = new URL(value) + if (url.protocol === `${rendererProtocol}:` && url.host === rendererHost) return true + const devUrl = process.env.ELECTRON_RENDERER_URL + if (!devUrl || !URL.canParse(devUrl)) return false + return url.origin === new URL(devUrl).origin +} + function wireZoom(win: BrowserWindow) { win.webContents.setZoomFactor(1) win.webContents.on("zoom-changed", () => { From d49d217e9d703c230e569e74c574d4d563aa5268 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 6 May 2026 14:14:31 +0530 Subject: [PATCH 30/70] fix(tui): preserve selected model on refresh (#25993) --- .../src/cli/cmd/tui/context/local.tsx | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 0b8c902c49..2958b573dd 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -397,23 +397,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, } - // Automatically update model when agent changes createEffect(() => { const value = agent.current() - if (!value) return - if (value.model) { - if (isModelValid(value.model)) - model.set({ - providerID: value.model.providerID, - modelID: value.model.modelID, - }) - else - toast.show({ - variant: "warning", - message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`, - duration: 3000, - }) - } + if (!value?.model) return + if (isModelValid(value.model)) return + toast.show({ + variant: "warning", + message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`, + duration: 3000, + }) }) const result = { From aa3c99a3c0a609ea4dd485355627e3161251584a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 6 May 2026 08:50:56 +0000 Subject: [PATCH 31/70] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index dc4ab9a32e..3792b80503 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Oo27Xkoo5HOzLaRs7FmSobzb1SNyidKIqk1+/BWtcqg=", - "aarch64-linux": "sha256-/d3ukZERWvV7egmc2Rtxg5vroZaXkCs7yVcIjIa4CUE=", - "aarch64-darwin": "sha256-1CX6n+9Wo2vAuPLekGsdjByReHQBbpKHwuK3L7Pfous=", - "x86_64-darwin": "sha256-Jqx3LDSoLSy8em7c/455xLEy9Pn4DmoYLHDemA1i+9w=" + "x86_64-linux": "sha256-ynZFX8eCamzBuVpauYLbju/Cqbt2260JNumMUj79PKA=", + "aarch64-linux": "sha256-JCu7JZkdAAHTufWEJRV1gJErKvHFirq+qmVNIRPZ/0w=", + "aarch64-darwin": "sha256-9Dkt/poYBpLdtqA6L9pLe6GS435zFGb5rOYWE5rEnjA=", + "x86_64-darwin": "sha256-Nd5j28gAcM7+0ETBchjk9VojViHy3N/z2MkdU42YuCg=" } } From 2abc4507b23834986f08c22b87cf15ff91417782 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 6 May 2026 10:25:42 -0400 Subject: [PATCH 32/70] fix(tui): filter only connected workspaces in dialog; add warp synthetic message (#25915) --- .../tui/component/dialog-workspace-create.tsx | 78 ++++++++++++++----- .../cmd/tui/dialog-workspace-create.test.ts | 38 +++++++++ 2 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index e372c59b99..157ca20582 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -33,6 +33,28 @@ export type WorkspaceSelection = type WorkspaceSelectValue = WorkspaceSelection | { type: "existing-list" } type ExistingWorkspaceSelectValue = { workspace: Workspace } +export function recentConnectedWorkspaces(input: { + sessions: readonly { workspaceID?: string; time: { updated: number } }[] + get: (workspaceID: string) => WorkspaceInfo | undefined + status: (workspaceID: string) => string | undefined + limit?: number +}) { + const workspaces = input.sessions + .toSorted((a, b) => b.time.updated - a.time.updated) + .flatMap((session) => { + const workspace = session.workspaceID ? input.get(session.workspaceID) : undefined + return workspace && input.status(workspace.id) === "connected" ? [workspace] : [] + }) + .filter((workspace, index, list) => list.findIndex((item) => item.id === workspace.id) === index) + const recent = workspaces.slice(0, input.limit ?? 3) + + return { recent, hasMore: recent.length < workspaces.length } +} + +export function warpReminderText(dir: string) { + return `The user has changed the current working directory to "${dir}". This is still the same project but at a possibly new location; take this into account when working with any files from now on.` +} + async function loadWorkspaceAdapters(input: { sdk: ReturnType sync: ReturnType @@ -77,7 +99,7 @@ export async function warpWorkspaceSession(input: { }): Promise { const result = await input.sdk.client.experimental.workspace .warp({ - id: input.workspaceID ?? undefined, + id: input.workspaceID, sessionID: input.sessionID, }) .catch(() => undefined) @@ -93,10 +115,30 @@ export async function warpWorkspaceSession(input: { await input.sync.bootstrap({ fatal: false }).catch(() => undefined) + const dir = input.project.instance.directory() || input.sync.path.directory + if (dir) { + await input.sdk.client.session + .promptAsync({ + sessionID: input.sessionID, + workspace: input.workspaceID ?? undefined, + noReply: true, + parts: [ + { + type: "text", + text: warpReminderText(dir), + synthetic: true, + }, + ], + }) + .catch(() => undefined) + } + await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]) - input.done?.() - if (input.done) return true + if (input.done) { + input.done() + return true + } input.dialog.clear() return true } @@ -125,15 +167,11 @@ export function DialogWorkspaceSelect(props: { const options = createMemo[]>(() => { const list = adapters() if (!list) return [] - const recent = sync.data.session - .toSorted((a, b) => b.time.updated - a.time.updated) - .flatMap((session) => (session.workspaceID ? [session.workspaceID] : [])) - .filter((workspaceID, index, list) => list.indexOf(workspaceID) === index) - .flatMap((workspaceID) => { - const workspace = project.workspace.get(workspaceID) - return workspace && project.workspace.status(workspace.id) === "connected" ? [workspace] : [] - }) - .slice(0, 3) + const { recent, hasMore } = recentConnectedWorkspaces({ + sessions: sync.data.session, + get: project.workspace.get, + status: project.workspace.status, + }) return [ ...list.map((adapter) => ({ title: adapter.name, @@ -158,12 +196,16 @@ export function DialogWorkspaceSelect(props: { }, category: "Choose workspace", })), - { - title: "View all workspaces", - value: { type: "existing-list" as const }, - description: "Choose from all workspaces", - category: "Choose workspace", - }, + ...(hasMore + ? [ + { + title: "View all workspaces", + value: { type: "existing-list" as const }, + description: "Choose from all workspaces", + category: "Choose workspace", + }, + ] + : []), ] }) diff --git a/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts new file mode 100644 index 0000000000..7d051923f6 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test" +import { recentConnectedWorkspaces } from "../../../../src/cli/cmd/tui/component/dialog-workspace-create" + +describe("recentConnectedWorkspaces", () => { + test("returns unique connected workspaces after filtering missing and inactive entries", () => { + const workspaces = [ + { id: "wrk_a", name: "alpha" }, + { id: "wrk_b", name: "beta" }, + { id: "wrk_c", name: "gamma" }, + { id: "wrk_d", name: "delta" }, + { id: "wrk_e", name: "epsilon" }, + ] + const status = { + wrk_a: "connected", + wrk_b: "disconnected", + wrk_c: "error", + wrk_d: "connected", + wrk_e: "connected", + } as const + + const { recent } = recentConnectedWorkspaces({ + sessions: [ + { time: { updated: 900 } }, + { workspaceID: "wrk_b", time: { updated: 800 } }, + { workspaceID: "wrk_a", time: { updated: 700 } }, + { workspaceID: "wrk_a", time: { updated: 600 } }, + { workspaceID: "wrk_missing", time: { updated: 500 } }, + { workspaceID: "wrk_c", time: { updated: 400 } }, + { workspaceID: "wrk_d", time: { updated: 300 } }, + { workspaceID: "wrk_e", time: { updated: 200 } }, + ], + get: (workspaceID) => workspaces.find((workspace) => workspace.id === workspaceID), + status: (workspaceID) => status[workspaceID as keyof typeof status], + }) + + expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_a", "wrk_d", "wrk_e"]) + }) +}) From 889f979c0ba547842ee6716cc7f38329bff729b7 Mon Sep 17 00:00:00 2001 From: Victor Navarro Date: Wed, 6 May 2026 16:57:34 +0200 Subject: [PATCH 33/70] chore: fix model alerts (#25990) --- infra/monitoring.ts | 18 ++++++++---------- .../console/app/src/routes/incident/webhook.ts | 6 ++++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index f500b099a0..85d68a7c5f 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -178,7 +178,7 @@ new incident.AlertRoute("HoneycombAlertRoute", { reference: $interpolate`alert.attributes.${fields.product.id}`, }, ], - groupingWindowSeconds: 900, + groupingWindowSeconds: 3600, }, incidentTemplate: { name: { @@ -215,7 +215,6 @@ type Trigger = (opts: { model: string; product: Product }) => { description: string json: honeycomb.GetQuerySpecificationOutputArgs threshold: { op: ">=" | "<="; value: number } - baseline: 3600 | 86400 } type Model = { id: string; products: Product[]; triggers: Trigger[] } @@ -232,6 +231,8 @@ const httpErrors: Trigger = ({ model, product }) => ({ filterCombination: "AND", filters: [ { column: "model", op: "=", value: model }, + { column: "event_type", op: "=", value: "completions" }, + { column: "user_agent", op: "contains", value: "opencode" }, { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, ], }, @@ -241,6 +242,8 @@ const httpErrors: Trigger = ({ model, product }) => ({ filterCombination: "AND", filters: [ { column: "model", op: "=", value: model }, + { column: "event_type", op: "=", value: "completions" }, + { column: "user_agent", op: "contains", value: "opencode" }, { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, { column: "status", op: ">=", value: "400" }, { column: "status", op: "!=", value: "401" }, @@ -250,10 +253,7 @@ const httpErrors: Trigger = ({ model, product }) => ({ formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }], timeRange: 900, }, - // Alert when errors surge 50% compared to the previous period - threshold: { op: ">=", value: 50 }, - // What previous time period to evaluate against - baseline: 3600, + threshold: { op: ">=", value: 0.8 }, }) const models: Model[] = [ @@ -296,10 +296,8 @@ for (const model of models) { name: spec.title, description: spec.description, queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json, - alertType: "on_change", - // This is the minimum when using % change detection - frequency: 900, - baselineDetails: [{ type: "percentage", offsetMinutes: spec.baseline / 60 }], + alertType: "on_true", + frequency: 300, thresholds: [{ ...spec.threshold, exceededLimit: 1 }], recipients: [ { diff --git a/packages/console/app/src/routes/incident/webhook.ts b/packages/console/app/src/routes/incident/webhook.ts index 62ee202743..ce7b0a0d9f 100644 --- a/packages/console/app/src/routes/incident/webhook.ts +++ b/packages/console/app/src/routes/incident/webhook.ts @@ -2,6 +2,8 @@ import type { APIEvent } from "@solidjs/start/server" import { Resource } from "@opencode-ai/console-resource" import { Webhook } from "svix" +const DISCORD_INCIDENT_ROLE_ID = "1501447160175136838" + type Incident = { mode?: "test" | "standard" name?: string @@ -37,14 +39,14 @@ const postDiscordMessage = async (incident: Incident) => { `**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`, incident.summary, "", - "@inference", + `<@&${DISCORD_INCIDENT_ROLE_ID}>`, "", incident.permalink, ] .filter((line) => line !== undefined) .join("\n"), allowed_mentions: { - parse: ["everyone"], + roles: [DISCORD_INCIDENT_ROLE_ID], }, flags: 4, }), From 63a175b50de63c52f34cd3fb662f528ceea74b01 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 6 May 2026 11:02:08 -0400 Subject: [PATCH 34/70] fix(cli): avoid AppRuntime re-entry for network options (#26052) --- packages/opencode/src/cli/cmd/acp.ts | 2 +- packages/opencode/src/cli/cmd/serve.ts | 2 +- packages/opencode/src/cli/cmd/web.ts | 4 ++-- packages/opencode/src/cli/network.ts | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts index e24262307c..b3b7df486b 100644 --- a/packages/opencode/src/cli/cmd/acp.ts +++ b/packages/opencode/src/cli/cmd/acp.ts @@ -22,7 +22,7 @@ export const AcpCommand = effectCmd({ }, handler: Effect.fn("Cli.acp")(function* (args) { process.env.OPENCODE_CLIENT = "acp" - const opts = yield* Effect.promise(() => resolveNetworkOptions(args)) + const opts = yield* resolveNetworkOptions(args) const server = yield* Effect.promise(() => Server.listen(opts)) const sdk = createOpencodeClient({ diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index a8a7234d9a..76f6276af5 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -15,7 +15,7 @@ export const ServeCommand = effectCmd({ if (!Flag.OPENCODE_SERVER_PASSWORD) { console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } - const opts = yield* Effect.promise(() => resolveNetworkOptions(args)) + const opts = yield* resolveNetworkOptions(args) const server = yield* Effect.promise(() => Server.listen(opts)) console.log(`opencode server listening on http://${server.hostname}:${server.port}`) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index f20381a014..384290c6ac 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -40,7 +40,7 @@ export const WebCommand = effectCmd({ if (!Flag.OPENCODE_SERVER_PASSWORD) { UI.println(UI.Style.TEXT_WARNING_BOLD + "! OPENCODE_SERVER_PASSWORD is not set; server is unsecured.") } - const opts = yield* Effect.promise(() => resolveNetworkOptions(args)) + const opts = yield* resolveNetworkOptions(args) const server = yield* Effect.promise(() => Server.listen(opts)) UI.empty() UI.println(UI.logo(" ")) @@ -72,7 +72,7 @@ export const WebCommand = effectCmd({ } // Open localhost in browser - open(localhostUrl.toString()).catch(() => {}) + open(localhostUrl).catch(() => {}) } else { const displayUrl = server.url.toString() UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl) diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index a6cecdfacd..41f8184ef5 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -1,6 +1,6 @@ import type { Argv, InferredOptionTypes } from "yargs" import { Config } from "@/config/config" -import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" const options = { port: { @@ -36,10 +36,10 @@ export type NetworkOptions = InferredOptionTypes export function withNetworkOptions(yargs: Argv) { return yargs.options(options) } -export async function resolveNetworkOptions(args: NetworkOptions) { - const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) +export const resolveNetworkOptions = Effect.fn("Cli.resolveNetworkOptions")(function* (args: NetworkOptions) { + const config = yield* Config.Service.use((cfg) => cfg.getGlobal()) return resolveNetworkOptionsNoConfig(args, config) -} +}) export function resolveNetworkOptionsNoConfig(args: NetworkOptions, config?: Config.Info) { const portExplicitlySet = process.argv.includes("--port") From d9c18381a67da445189d74e230728e737ef161c5 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 6 May 2026 11:12:23 -0400 Subject: [PATCH 35/70] feat(config): support well-known remote_config (#26054) --- packages/opencode/src/config/config.ts | 57 ++++++++++++++- packages/opencode/test/config/config.test.ts | 77 ++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3a933f81e9..6b43b18968 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -70,6 +70,40 @@ function normalizeLoadedConfig(data: unknown, source: string) { return copy } +async function substituteWellKnownRemoteConfig(input: { + value: unknown + dir: string + source: string +}) { + if (!isRecord(input.value) || typeof input.value.url !== "string") return + + const url = await ConfigVariable.substitute({ + text: input.value.url, + type: "virtual", + dir: input.dir, + source: input.source, + }) + const headers = isRecord(input.value.headers) + ? Object.fromEntries( + await Promise.all( + Object.entries(input.value.headers) + .filter((entry): entry is [string, string] => typeof entry[1] === "string") + .map(async ([key, value]) => [ + key, + await ConfigVariable.substitute({ + text: value, + type: "virtual", + dir: input.dir, + source: input.source, + }), + ]), + ), + ) + : undefined + + return { url, headers } +} + async function resolveLoadedPlugins(config: T, filepath: string) { if (!config.plugin) return config for (let i = 0; i < config.plugin.length; i++) { @@ -494,8 +528,27 @@ export const layer = Layer.effect( if (!response.ok) { throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) } - const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record } - const remoteConfig = wellknown.config ?? {} + const wellknown = (yield* Effect.promise(() => response.json())) as { + config?: Record + remote_config?: unknown + } + const remote = yield* Effect.promise(() => + substituteWellKnownRemoteConfig({ + value: wellknown.remote_config, + dir: url, + source: `${url}/.well-known/opencode`, + }), + ) + const fetchedConfig = remote + ? ((yield* Effect.promise(async () => { + log.debug("fetching remote config", { url: remote.url }) + const response = await fetch(remote.url, { headers: remote.headers }) + if (!response.ok) throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`) + const data = await response.json() + return isRecord(data) && isRecord(data.config) ? data.config : data + })) as Record) + : {} + const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info) if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" const source = `${url}/.well-known/opencode` const next = yield* loadConfig(JSON.stringify(remoteConfig), { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 0a522b0850..bbe585237b 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1972,6 +1972,83 @@ test("wellknown URL with trailing slash is normalized", async () => { } }) +test("wellknown remote_config supports templated env vars in headers", async () => { + const originalFetch = globalThis.fetch + const originalToken = process.env.TEST_TOKEN + let wellknownFetchedUrl: string | undefined + let remoteFetchedUrl: string | undefined + let remoteHeaders: HeadersInit | undefined + globalThis.fetch = mock((url: string | URL | Request, init?: RequestInit) => { + const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url + if (urlStr.includes(".well-known/opencode")) { + wellknownFetchedUrl = urlStr + return Promise.resolve( + new Response( + JSON.stringify({ + remote_config: { + url: "https://config.example.com/opencode.json", + headers: { + Authorization: "Bearer {env:TEST_TOKEN}", + }, + }, + }), + { status: 200 }, + ), + ) + } + if (urlStr.includes("config.example.com")) { + remoteFetchedUrl = urlStr + remoteHeaders = init?.headers + return Promise.resolve( + new Response( + JSON.stringify({ + mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } }, + }), + { status: 200 }, + ), + ) + } + return originalFetch(url, init) + }) as unknown as typeof fetch + + const fakeAuth = Layer.mock(Auth.Service)({ + all: () => + Effect.succeed({ + "https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }), + }), + }) + + const layer = Config.layer.pipe( + Layer.provide(testFlock), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(fakeAuth), + Layer.provide(emptyAccount), + Layer.provideMerge(infra), + Layer.provide(noopNpm), + ) + + try { + await provideTmpdirInstance( + () => + Config.Service.use((svc) => + Effect.gen(function* () { + const config = yield* svc.get() + expect(wellknownFetchedUrl).toBe("https://example.com/.well-known/opencode") + expect(remoteFetchedUrl).toBe("https://config.example.com/opencode.json") + expect(remoteHeaders).toEqual({ Authorization: "Bearer test-token" }) + expect(config.mcp?.confluence?.enabled).toBe(true) + }), + ), + { git: true }, + ).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise) + } finally { + globalThis.fetch = originalFetch + if (originalToken === undefined) delete process.env.TEST_TOKEN + else process.env.TEST_TOKEN = originalToken + } +}) + describe("resolvePluginSpec", () => { test("keeps package specs unchanged", async () => { await using tmp = await tmpdir() From b9b854bf9f206e5c1c85cfd15d128bb3d0966e58 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 6 May 2026 15:13:34 +0000 Subject: [PATCH 36/70] chore: generate --- packages/opencode/src/config/config.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 6b43b18968..fcdb4e7b1c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -70,11 +70,7 @@ function normalizeLoadedConfig(data: unknown, source: string) { return copy } -async function substituteWellKnownRemoteConfig(input: { - value: unknown - dir: string - source: string -}) { +async function substituteWellKnownRemoteConfig(input: { value: unknown; dir: string; source: string }) { if (!isRecord(input.value) || typeof input.value.url !== "string") return const url = await ConfigVariable.substitute({ @@ -543,7 +539,8 @@ export const layer = Layer.effect( ? ((yield* Effect.promise(async () => { log.debug("fetching remote config", { url: remote.url }) const response = await fetch(remote.url, { headers: remote.headers }) - if (!response.ok) throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`) + if (!response.ok) + throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`) const data = await response.json() return isRecord(data) && isRecord(data.config) ? data.config : data })) as Record) From 344ccc647b93a71af7a2486f94a6458112e9250f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 6 May 2026 11:45:11 -0500 Subject: [PATCH 37/70] ignore: vimtor to team members list --- .github/TEAM_MEMBERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/TEAM_MEMBERS b/.github/TEAM_MEMBERS index e5f8f000e0..a662c7c063 100644 --- a/.github/TEAM_MEMBERS +++ b/.github/TEAM_MEMBERS @@ -13,3 +13,4 @@ R44VC0RP rekram1-node thdxr simonklee +vimtor From 38b0cdc1493930082b9fcc8e855e2985f58bf26e Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 May 2026 13:10:46 -0400 Subject: [PATCH 38/70] go: deprecate old models --- packages/console/app/src/i18n/ar.ts | 8 ++++---- packages/console/app/src/i18n/br.ts | 8 ++++---- packages/console/app/src/i18n/da.ts | 8 ++++---- packages/console/app/src/i18n/de.ts | 8 ++++---- packages/console/app/src/i18n/en.ts | 8 ++++---- packages/console/app/src/i18n/es.ts | 8 ++++---- packages/console/app/src/i18n/fr.ts | 8 ++++---- packages/console/app/src/i18n/it.ts | 8 ++++---- packages/console/app/src/i18n/ja.ts | 8 ++++---- packages/console/app/src/i18n/ko.ts | 8 ++++---- packages/console/app/src/i18n/no.ts | 8 ++++---- packages/console/app/src/i18n/pl.ts | 8 ++++---- packages/console/app/src/i18n/ru.ts | 8 ++++---- packages/console/app/src/i18n/th.ts | 8 ++++---- packages/console/app/src/i18n/tr.ts | 8 ++++---- packages/console/app/src/i18n/zh.ts | 8 ++++---- packages/console/app/src/i18n/zht.ts | 8 ++++---- packages/console/app/src/routes/go/index.tsx | 2 -- .../routes/workspace/[id]/go/lite-section.tsx | 2 -- .../app/src/routes/zen/util/keyRateLimiter.ts | 2 +- packages/web/src/content/docs/ar/go.mdx | 16 ++++------------ packages/web/src/content/docs/bs/go.mdx | 16 ++++------------ packages/web/src/content/docs/da/go.mdx | 16 ++++------------ packages/web/src/content/docs/de/go.mdx | 16 ++++------------ packages/web/src/content/docs/es/go.mdx | 16 ++++------------ packages/web/src/content/docs/fr/go.mdx | 16 ++++------------ packages/web/src/content/docs/go.mdx | 16 ++++------------ packages/web/src/content/docs/it/go.mdx | 16 ++++------------ packages/web/src/content/docs/ja/go.mdx | 16 ++++------------ packages/web/src/content/docs/ko/go.mdx | 16 ++++------------ packages/web/src/content/docs/nb/go.mdx | 16 ++++------------ packages/web/src/content/docs/pl/go.mdx | 16 ++++------------ packages/web/src/content/docs/pt-br/go.mdx | 16 ++++------------ packages/web/src/content/docs/ru/go.mdx | 16 ++++------------ packages/web/src/content/docs/th/go.mdx | 16 ++++------------ packages/web/src/content/docs/tr/go.mdx | 16 ++++------------ packages/web/src/content/docs/zh-cn/go.mdx | 16 ++++------------ packages/web/src/content/docs/zh-tw/go.mdx | 16 ++++------------ 38 files changed, 141 insertions(+), 289 deletions(-) diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index 42258db866..12ec7f1fbd 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -249,7 +249,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -298,7 +298,7 @@ export const dict = { "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": - "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash", + "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -322,7 +322,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -345,7 +345,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index a848ba38da..0a6d8f153e 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": - "Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index c54aca32e1..15e7151b67 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 6e14778de8..0efcce78bf 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": - "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash", + "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -328,7 +328,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -352,7 +352,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 0d0869da53..f2cf3c14a4 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -248,7 +248,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -296,7 +296,7 @@ export const dict = { "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": - "Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash", + "Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -321,7 +321,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -345,7 +345,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} not supported", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index fd13a54de6..5614a8c7ad 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": - "Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash", + "Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 3762915abf..390025d275 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": - "Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash", + "Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -353,7 +353,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 04d0e2451c..3737186996 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": - "Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", + "Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index 71404c91eb..66f3c4a89d 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.meta.description": - "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": - "GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", + "GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -349,7 +349,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index 6a7d52bbd5..04482d35f6 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -247,7 +247,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -297,7 +297,7 @@ export const dict = { "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -321,7 +321,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -344,7 +344,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 629e690c64..31200d3edd 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -326,7 +326,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -350,7 +350,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 0f465df9d9..50d904bc56 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -301,7 +301,7 @@ export const dict = { "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": - "Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash", + "Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 90019dbe54..651309fc95 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": - "Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash", + "Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 9f210ada49..42c9e455fd 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -298,7 +298,7 @@ export const dict = { "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": - "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", + "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -323,7 +323,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -346,7 +346,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 3d2f8f39de..64380db375 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index fdcb7d37a0..3b104cca6d 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", + "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -289,7 +289,7 @@ export const dict = { "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": - "包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash", + "包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -311,7 +311,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -333,7 +333,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index bfbfcf7e81..a4d5512da4 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 低成本全民編碼模型", "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", + "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -289,7 +289,7 @@ export const dict = { "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": - "包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash", + "包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -311,7 +311,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -333,7 +333,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 1ec83b25fe..71102c7227 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -27,8 +27,6 @@ const models = [ { name: "GLM-5", provider: "DeepInfra, Fireworks AI, Z.ai" }, { name: "Kimi K2.5", provider: "Moonshot AI" }, { name: "Kimi K2.6", provider: "Moonshot AI" }, - { name: "MiMo-V2-Pro", provider: "Xiaomi MiMo" }, - { name: "MiMo-V2-Omni", provider: "Xiaomi MiMo" }, { name: "MiMo-V2.5-Pro", provider: "Xiaomi MiMo" }, { name: "MiMo-V2.5", provider: "Xiaomi MiMo" }, { name: "Qwen3.5 Plus", provider: "Alibaba Cloud Model Studio" }, diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 0df181ae16..eba52b0e17 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -289,8 +289,6 @@ export function LiteSection() {
  • Kimi K2.6
  • GLM-5
  • GLM-5.1
  • -
  • MiMo-V2-Pro
  • -
  • MiMo-V2-Omni
  • MiMo-V2.5-Pro
  • MiMo-V2.5
  • MiniMax M2.5
  • diff --git a/packages/console/app/src/routes/zen/util/keyRateLimiter.ts b/packages/console/app/src/routes/zen/util/keyRateLimiter.ts index e3e0fb18f2..2472776caa 100644 --- a/packages/console/app/src/routes/zen/util/keyRateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/keyRateLimiter.ts @@ -8,7 +8,7 @@ export function createRateLimiter(modelId: string, zenApiKey: string | undefined if (!zenApiKey) return const dict = i18n(localeFromRequest(request)) - const LIMIT = 100 + const LIMIT = 300 const yyyyMMddHHmm = new Date(Date.now()) .toISOString() .replace(/[^0-9]/g, "") diff --git a/packages/web/src/content/docs/ar/go.mdx b/packages/web/src/content/docs/ar/go.mdx index 35c52d9695..81f885335c 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -57,10 +57,8 @@ OpenCode Go حاليًا في المرحلة التجريبية. - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Go حاليًا في المرحلة التجريبية. | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | @@ -110,10 +106,8 @@ OpenCode Go حاليًا في المرحلة التجريبية. - MiniMax M2.7/M2.5 — ‏300 input، و55,000 cached، و125 output tokens لكل طلب - Qwen3.5 Plus — ‏410 input، و47,000 cached، و140 output tokens لكل طلب - Qwen3.6 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب -- MiMo-V2-Pro — ‏350 input، و41,000 cached، و250 output tokens لكل طلب -- MiMo-V2-Omni — ‏1000 input، و60,000 cached، و140 output tokens لكل طلب -- MiMo-V2.5-Pro — ‏350 input، و41,000 cached، و250 output tokens لكل طلب - MiMo-V2.5 — ‏1000 input، و60,000 cached، و140 output tokens لكل طلب +- MiMo-V2.5-Pro — ‏350 input، و41,000 cached، و250 output tokens لكل طلب يمكنك تتبّع استخدامك الحالي في **console**. @@ -143,10 +137,8 @@ OpenCode Go حاليًا في المرحلة التجريبية. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index a895a20941..d2df6aaad8 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -67,10 +67,8 @@ Trenutna lista modela uključuje: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ Procjene se zasnivaju na zapaženim prosječnim obrascima zahtjeva: - MiniMax M2.7/M2.5 — 300 ulaznih, 55,000 keširanih, 125 izlaznih tokena po zahtjevu - Qwen3.5 Plus — 410 ulaznih, 47,000 keširanih, 140 izlaznih tokena po zahtjevu - Qwen3.6 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu -- MiMo-V2-Pro — 350 ulaznih, 41,000 keširanih, 250 izlaznih tokena po zahtjevu -- MiMo-V2-Omni — 1000 ulaznih, 60,000 keširanih, 140 izlaznih tokena po zahtjevu -- MiMo-V2.5-Pro — 350 ulaznih, 41,000 keširanih, 250 izlaznih tokena po zahtjevu - MiMo-V2.5 — 1000 ulaznih, 60,000 keširanih, 140 izlaznih tokena po zahtjevu +- MiMo-V2.5-Pro — 350 ulaznih, 41,000 keširanih, 250 izlaznih tokena po zahtjevu Svoju trenutnu potrošnju možete pratiti u **konzoli**. @@ -155,10 +149,8 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index db61689a28..6891e6d579 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -67,10 +67,8 @@ Den nuværende liste over modeller inkluderer: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ Estimaterne er baseret på observerede gennemsnitlige anmodningsmønstre: - MiniMax M2.7/M2.5 — 300 input, 55.000 cachelagrede, 125 output-tokens pr. anmodning - Qwen3.5 Plus — 410 input, 47.000 cachelagrede, 140 output-tokens pr. anmodning - Qwen3.6 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning -- MiMo-V2-Pro — 350 input, 41.000 cachelagrede, 250 output-tokens pr. anmodning -- MiMo-V2-Omni — 1000 input, 60.000 cachelagrede, 140 output-tokens pr. anmodning -- MiMo-V2.5-Pro — 350 input, 41.000 cachelagrede, 250 output-tokens pr. anmodning - MiMo-V2.5 — 1000 input, 60.000 cachelagrede, 140 output-tokens pr. anmodning +- MiMo-V2.5-Pro — 350 input, 41.000 cachelagrede, 250 output-tokens pr. anmodning Du kan spore dit nuværende forbrug i **konsollen**. @@ -155,10 +149,8 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index a8da54728d..917ea340ef 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -59,10 +59,8 @@ Die aktuelle Liste der Modelle umfasst: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -92,10 +90,8 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -112,10 +108,8 @@ Die Schätzungen basieren auf beobachteten durchschnittlichen Anfragemustern: - MiniMax M2.7/M2.5 — 300 Input-, 55.000 Cached-, 125 Output-Tokens pro Anfrage - Qwen3.5 Plus — 410 Input-, 47.000 Cached-, 140 Output-Tokens pro Anfrage - Qwen3.6 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage -- MiMo-V2-Pro — 350 Input-, 41.000 Cached-, 250 Output-Tokens pro Anfrage -- MiMo-V2-Omni — 1.000 Input-, 60.000 Cached-, 140 Output-Tokens pro Anfrage -- MiMo-V2.5-Pro — 350 Input-, 41.000 Cached-, 250 Output-Tokens pro Anfrage - MiMo-V2.5 — 1.000 Input-, 60.000 Cached-, 140 Output-Tokens pro Anfrage +- MiMo-V2.5-Pro — 350 Input-, 41.000 Cached-, 250 Output-Tokens pro Anfrage Du kannst deine aktuelle Nutzung in der **Console** verfolgen. @@ -145,10 +139,8 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index becff7ac04..0be23b3fa4 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -67,10 +67,8 @@ La lista actual de modelos incluye: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ Las estimaciones se basan en los patrones de peticiones promedio observados: - MiniMax M2.7/M2.5 — 300 tokens de entrada, 55,000 en caché, 125 tokens de salida por petición - Qwen3.5 Plus — 410 tokens de entrada, 47,000 en caché, 140 tokens de salida por petición - Qwen3.6 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición -- MiMo-V2-Pro — 350 tokens de entrada, 41,000 en caché, 250 tokens de salida por petición -- MiMo-V2-Omni — 1000 tokens de entrada, 60,000 en caché, 140 tokens de salida por petición -- MiMo-V2.5-Pro — 350 tokens de entrada, 41,000 en caché, 250 tokens de salida por petición - MiMo-V2.5 — 1000 tokens de entrada, 60,000 en caché, 140 tokens de salida por petición +- MiMo-V2.5-Pro — 350 tokens de entrada, 41,000 en caché, 250 tokens de salida por petición Puedes realizar un seguimiento de tu uso actual en la **consola**. @@ -155,10 +149,8 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index 97280fa372..3dd9c25f32 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -57,10 +57,8 @@ La liste actuelle des modèles comprend : - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -110,10 +106,8 @@ Les estimations sont basées sur les modèles de requêtes moyens observés : - MiniMax M2.7/M2.5 — 300 tokens en entrée, 55,000 en cache, 125 tokens en sortie par requête - Qwen3.5 Plus — 410 tokens en entrée, 47,000 en cache, 140 tokens en sortie par requête - Qwen3.6 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête -- MiMo-V2-Pro — 350 tokens en entrée, 41,000 en cache, 250 tokens en sortie par requête -- MiMo-V2-Omni — 1000 tokens en entrée, 60,000 en cache, 140 tokens en sortie par requête -- MiMo-V2.5-Pro — 350 tokens en entrée, 41,000 en cache, 250 tokens en sortie par requête - MiMo-V2.5 — 1000 tokens en entrée, 60,000 en cache, 140 tokens en sortie par requête +- MiMo-V2.5-Pro — 350 tokens en entrée, 41,000 en cache, 250 tokens en sortie par requête Vous pouvez suivre votre utilisation actuelle dans la **console**. @@ -143,10 +137,8 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d' | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx index cddb6d491b..237d1c4b84 100644 --- a/packages/web/src/content/docs/go.mdx +++ b/packages/web/src/content/docs/go.mdx @@ -67,10 +67,8 @@ The current list of models includes: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **MiniMax M2.7** - **Qwen3.5 Plus** @@ -100,10 +98,8 @@ The table below provides an estimated request count based on typical Go usage pa | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -118,10 +114,8 @@ Estimates are based on observed average request patterns: - DeepSeek V4 Pro — 750 input, 82,000 cached, 290 output tokens per request - DeepSeek V4 Flash — 790 input, 68,000 cached, 280 output tokens per request - MiniMax M2.7/M2.5 — 300 input, 55,000 cached, 125 output tokens per request -- MiMo-V2-Pro — 350 input, 41,000 cached, 250 output tokens per request -- MiMo-V2-Omni — 1000 input, 60,000 cached, 140 output tokens per request -- MiMo-V2.5-Pro — 350 input, 41,000 cached, 250 output tokens per request - MiMo-V2.5 — 1000 input, 60,000 cached, 140 output tokens per request +- MiMo-V2.5-Pro — 350 input, 41,000 cached, 250 output tokens per request - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -155,10 +149,8 @@ You can also access Go models through the following API endpoints. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index 28f8c5fbf8..df4f6dd1ca 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -65,10 +65,8 @@ L'elenco attuale dei modelli include: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -98,10 +96,8 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -118,10 +114,8 @@ Le stime si basano sui pattern medi di richieste osservati: - MiniMax M2.7/M2.5 — 300 di input, 55.000 in cache, 125 token di output per richiesta - Qwen3.5 Plus — 410 di input, 47.000 in cache, 140 token di output per richiesta - Qwen3.6 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta -- MiMo-V2-Pro — 350 di input, 41.000 in cache, 250 token di output per richiesta -- MiMo-V2-Omni — 1000 di input, 60.000 in cache, 140 token di output per richiesta -- MiMo-V2.5-Pro — 350 di input, 41.000 in cache, 250 token di output per richiesta - MiMo-V2.5 — 1000 di input, 60.000 in cache, 140 token di output per richiesta +- MiMo-V2.5-Pro — 350 di input, 41.000 in cache, 250 token di output per richiesta Puoi monitorare il tuo utilizzo attuale nella **console**. @@ -153,10 +147,8 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index 5f4fcfbc39..0cb294754f 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -57,10 +57,8 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Goには以下の制限が含まれています: | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -110,10 +106,8 @@ OpenCode Goには以下の制限が含まれています: - MiniMax M2.7/M2.5 — リクエストあたり 入力 300トークン、キャッシュ 55,000トークン、出力 125トークン - Qwen3.5 Plus — リクエストあたり 入力 410トークン、キャッシュ 47,000トークン、出力 140トークン - Qwen3.6 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン -- MiMo-V2-Pro — リクエストあたり 入力 350トークン、キャッシュ 41,000トークン、出力 250トークン -- MiMo-V2-Omni — リクエストあたり 入力 1000トークン、キャッシュ 60,000トークン、出力 140トークン -- MiMo-V2.5-Pro — リクエストあたり 入力 350トークン、キャッシュ 41,000トークン、出力 250トークン - MiMo-V2.5 — リクエストあたり 入力 1000トークン、キャッシュ 60,000トークン、出力 140トークン +- MiMo-V2.5-Pro — リクエストあたり 入力 350トークン、キャッシュ 41,000トークン、出力 250トークン 現在の利用状況は**コンソール**で追跡できます。 @@ -143,10 +137,8 @@ Zen残高にクレジットがある場合は、コンソールで**Use balance* | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index ef05b01c49..d0a3b9d0d1 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -57,10 +57,8 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -110,10 +106,8 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. - MiniMax M2.7/M2.5 — 요청당 입력 300, 캐시 55,000, 출력 토큰 125 - Qwen3.5 Plus — 요청당 입력 410, 캐시 47,000, 출력 토큰 140 - Qwen3.6 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 -- MiMo-V2-Pro — 요청당 입력 350, 캐시 41,000, 출력 토큰 250 -- MiMo-V2-Omni — 요청당 입력 1000, 캐시 60,000, 출력 토큰 140 -- MiMo-V2.5-Pro — 요청당 입력 350, 캐시 41,000, 출력 토큰 250 - MiMo-V2.5 — 요청당 입력 1000, 캐시 60,000, 출력 토큰 140 +- MiMo-V2.5-Pro — 요청당 입력 350, 캐시 41,000, 출력 토큰 250 현재 사용량은 **console**에서 확인할 수 있습니다. @@ -143,10 +137,8 @@ Zen 잔액에 크레딧도 있다면, console에서 **Use balance** 옵션을 | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index 02a2ba9e0b..e19b6ccce1 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -67,10 +67,8 @@ Den nåværende listen over modeller inkluderer: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ Estimatene er basert på observerte gjennomsnittlige forespørselsmønstre: - MiniMax M2.7/M2.5 — 300 input, 55 000 bufret, 125 output-tokens per forespørsel - Qwen3.5 Plus — 410 input, 47 000 bufret, 140 output-tokens per forespørsel - Qwen3.6 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel -- MiMo-V2-Pro — 350 input, 41 000 bufret, 250 output-tokens per forespørsel -- MiMo-V2-Omni — 1000 input, 60 000 bufret, 140 output-tokens per forespørsel -- MiMo-V2.5-Pro — 350 input, 41 000 bufret, 250 output-tokens per forespørsel - MiMo-V2.5 — 1000 input, 60 000 bufret, 140 output-tokens per forespørsel +- MiMo-V2.5-Pro — 350 input, 41 000 bufret, 250 output-tokens per forespørsel Du kan spore din nåværende bruk i **konsollen**. @@ -155,10 +149,8 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index 224671a19a..00f76a103f 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -61,10 +61,8 @@ Obecna lista modeli obejmuje: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -94,10 +92,8 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -114,10 +110,8 @@ Szacunki opierają się na zaobserwowanych średnich wzorcach żądań: - MiniMax M2.7/M2.5 — 300 tokenów wejściowych, 55 000 w pamięci podręcznej, 125 tokenów wyjściowych na żądanie - Qwen3.5 Plus — 410 tokenów wejściowych, 47 000 w pamięci podręcznej, 140 tokenów wyjściowych na żądanie - Qwen3.6 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie -- MiMo-V2-Pro — 350 tokenów wejściowych, 41 000 w pamięci podręcznej, 250 tokenów wyjściowych na żądanie -- MiMo-V2-Omni — 1000 tokenów wejściowych, 60 000 w pamięci podręcznej, 140 tokenów wyjściowych na żądanie -- MiMo-V2.5-Pro — 350 tokenów wejściowych, 41 000 w pamięci podręcznej, 250 tokenów wyjściowych na żądanie - MiMo-V2.5 — 1000 tokenów wejściowych, 60 000 w pamięci podręcznej, 140 tokenów wyjściowych na żądanie +- MiMo-V2.5-Pro — 350 tokenów wejściowych, 41 000 w pamięci podręcznej, 250 tokenów wyjściowych na żądanie Możesz śledzić swoje bieżące zużycie w **konsoli**. @@ -147,10 +141,8 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index e50f7d3962..44c5092a00 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -67,10 +67,8 @@ A lista atual de modelos inclui: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ As estimativas baseiam-se nos padrões médios de requisições observados: - MiniMax M2.7/M2.5 — 300 tokens de entrada, 55.000 em cache, 125 tokens de saída por requisição - Qwen3.5 Plus — 410 tokens de entrada, 47.000 em cache, 140 tokens de saída por requisição - Qwen3.6 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição -- MiMo-V2-Pro — 350 tokens de entrada, 41.000 em cache, 250 tokens de saída por requisição -- MiMo-V2-Omni — 1000 tokens de entrada, 60.000 em cache, 140 tokens de saída por requisição -- MiMo-V2.5-Pro — 350 tokens de entrada, 41.000 em cache, 250 tokens de saída por requisição - MiMo-V2.5 — 1000 tokens de entrada, 60.000 em cache, 140 tokens de saída por requisição +- MiMo-V2.5-Pro — 350 tokens de entrada, 41.000 em cache, 250 tokens de saída por requisição Você pode acompanhar o seu uso atual no **console**. @@ -155,10 +149,8 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index 4f11204e1a..66e929c5f4 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -67,10 +67,8 @@ OpenCode Go работает так же, как и любой другой пр - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -100,10 +98,8 @@ OpenCode Go включает следующие лимиты: | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -120,10 +116,8 @@ OpenCode Go включает следующие лимиты: - MiniMax M2.7/M2.5 — 300 входных, 55,000 кешированных, 125 выходных токенов на запрос - Qwen3.5 Plus — 410 входных, 47,000 кешированных, 140 выходных токенов на запрос - Qwen3.6 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос -- MiMo-V2-Pro — 350 входных, 41,000 кешированных, 250 выходных токенов на запрос -- MiMo-V2-Omni — 1000 входных, 60,000 кешированных, 140 выходных токенов на запрос -- MiMo-V2.5-Pro — 350 входных, 41,000 кешированных, 250 выходных токенов на запрос - MiMo-V2.5 — 1000 входных, 60,000 кешированных, 140 выходных токенов на запрос +- MiMo-V2.5-Pro — 350 входных, 41,000 кешированных, 250 выходных токенов на запрос Вы можете отслеживать текущее использование в **консоли**. @@ -155,10 +149,8 @@ OpenCode Go включает следующие лимиты: | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index 2a4c90a840..1fa0f8cc2a 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -57,10 +57,8 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -110,10 +106,8 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: - MiniMax M2.7/M2.5 — 300 input, 55,000 cached, 125 output tokens ต่อ request - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens ต่อ request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request -- MiMo-V2-Pro — 350 input, 41,000 cached, 250 output tokens ต่อ request -- MiMo-V2-Omni — 1000 input, 60,000 cached, 140 output tokens ต่อ request -- MiMo-V2.5-Pro — 350 input, 41,000 cached, 250 output tokens ต่อ request - MiMo-V2.5 — 1000 input, 60,000 cached, 140 output tokens ต่อ request +- MiMo-V2.5-Pro — 350 input, 41,000 cached, 250 output tokens ต่อ request คุณสามารถติดตามการใช้งานปัจจุบันของคุณได้ใน **console** @@ -143,10 +137,8 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index b3995e8a57..367be5a750 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -57,10 +57,8 @@ Mevcut model listesi şunları içerir: - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | @@ -110,10 +106,8 @@ Tahminler, gözlemlenen ortalama istek modellerine dayanmaktadır: - MiniMax M2.7/M2.5 — İstek başına 300 girdi, 55.000 önbelleğe alınmış, 125 çıktı token'ı - Qwen3.5 Plus — İstek başına 410 girdi, 47.000 önbelleğe alınmış, 140 çıktı token'ı - Qwen3.6 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı -- MiMo-V2-Pro — İstek başına 350 girdi, 41.000 önbelleğe alınmış, 250 çıktı token'ı -- MiMo-V2-Omni — İstek başına 1000 girdi, 60.000 önbelleğe alınmış, 140 çıktı token'ı -- MiMo-V2.5-Pro — İstek başına 350 girdi, 41.000 önbelleğe alınmış, 250 çıktı token'ı - MiMo-V2.5 — İstek başına 1000 girdi, 60.000 önbelleğe alınmış, 140 çıktı token'ı +- MiMo-V2.5-Pro — İstek başına 350 girdi, 41.000 önbelleğe alınmış, 250 çıktı token'ı Mevcut kullanımınızı **konsoldan** takip edebilirsiniz. @@ -143,10 +137,8 @@ Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsi | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index 8bd90d5fbf..17934ee2a0 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -57,10 +57,8 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Go 包含以下限制: | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | @@ -107,10 +103,8 @@ OpenCode Go 包含以下限制: - Kimi K2.5/K2.6 — 每次请求 870 个输入 token,55,000 个缓存 token,200 个输出 token - DeepSeek V4 Pro — 每次请求 750 个输入 token,82,000 个缓存 token,290 个输出 token - DeepSeek V4 Flash — 每次请求 790 个输入 token,68,000 个缓存 token,280 个输出 token -- MiMo-V2-Pro — 每次请求 350 个输入 token,41,000 个缓存 token,250 个输出 token -- MiMo-V2-Omni — 每次请求 1000 个输入 token,60,000 个缓存 token,140 个输出 token -- MiMo-V2.5-Pro — 每次请求 350 个输入 token,41,000 个缓存 token,250 个输出 token - MiMo-V2.5 — 每次请求 1000 个输入 token,60,000 个缓存 token,140 个输出 token +- MiMo-V2.5-Pro — 每次请求 350 个输入 token,41,000 个缓存 token,250 个输出 token - MiniMax M2.7/M2.5 — 每次请求 300 个输入 token,55,000 个缓存 token,125 个输出 token - Qwen3.5 Plus — 每次请求 410 个输入 token,47,000 个缓存 token,140 个输出 token - Qwen3.6 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token @@ -143,10 +137,8 @@ OpenCode Go 包含以下限制: | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index 3bf4618bc5..c4589716f2 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -57,10 +57,8 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **GLM-5.1** - **Kimi K2.5** - **Kimi K2.6** -- **MiMo-V2-Pro** -- **MiMo-V2-Omni** -- **MiMo-V2.5-Pro** - **MiMo-V2.5** +- **MiMo-V2.5-Pro** - **MiniMax M2.5** - **Qwen3.5 Plus** - **Qwen3.6 Plus** @@ -90,10 +88,8 @@ OpenCode Go 包含以下限制: | GLM-5 | 1,150 | 2,880 | 5,750 | | Kimi K2.5 | 1,850 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | MiMo-V2.5 (≤ 256K) | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | | MiniMax M2.5 | 6,300 | 15,900 | 31,800 | @@ -110,10 +106,8 @@ OpenCode Go 包含以下限制: - MiniMax M2.7/M2.5 — 每次請求 300 個輸入 token、55,000 個快取 token、125 個輸出 token - Qwen3.5 Plus — 每次請求 410 個輸入 token、47,000 個快取 token、140 個輸出 token - Qwen3.6 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token -- MiMo-V2-Pro — 每次請求 350 個輸入 token、41,000 個快取 token、250 個輸出 token -- MiMo-V2-Omni — 每次請求 1000 個輸入 token、60,000 個快取 token、140 個輸出 token -- MiMo-V2.5-Pro — 每次請求 350 個輸入 token、41,000 個快取 token、250 個輸出 token - MiMo-V2.5 — 每次請求 1000 個輸入 token、60,000 個快取 token、140 個輸出 token +- MiMo-V2.5-Pro — 每次請求 350 個輸入 token、41,000 個快取 token、250 個輸出 token 您可以在 **console** 中追蹤您目前的使用量。 @@ -143,10 +137,8 @@ OpenCode Go 包含以下限制: | Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `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` | | MiMo-V2.5 | mimo-v2.5 | `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.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` | From bf979413f9f5f0f420ab43ac6c55341236438285 Mon Sep 17 00:00:00 2001 From: vimtor Date: Wed, 6 May 2026 19:26:53 +0200 Subject: [PATCH 39/70] chore: change alert type for honeycomb triggers --- infra/monitoring.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 85d68a7c5f..4fb7183a2f 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -296,7 +296,7 @@ for (const model of models) { name: spec.title, description: spec.description, queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json, - alertType: "on_true", + alertType: "on_change", frequency: 300, thresholds: [{ ...spec.threshold, exceededLimit: 1 }], recipients: [ From e41843eaf7772985ebbf37c4c1419ed30f4c16c6 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 May 2026 15:20:36 -0400 Subject: [PATCH 40/70] sync --- .../console/app/src/routes/workspace/[id]/model-section.tsx | 1 + packages/console/app/src/routes/zen/v1/models.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/workspace/[id]/model-section.tsx b/packages/console/app/src/routes/workspace/[id]/model-section.tsx index b9cdf3bc3a..35ea2cf878 100644 --- a/packages/console/app/src/routes/workspace/[id]/model-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/model-section.tsx @@ -45,6 +45,7 @@ const getModelsInfo = query(async (workspaceID: string) => { all: Object.entries(ZenData.list("full").models) .filter(([id, _model]) => !["claude-3-5-haiku"].includes(id)) .filter(([id, _model]) => !id.startsWith("alpha-")) + .filter(([id, _model]) => !id.endsWith(":global")) .sort(([idA, modelA], [idB, modelB]) => { const priority = ["big-pickle", "minimax", "grok", "claude", "gpt", "gemini"] const getPriority = (id: string) => { diff --git a/packages/console/app/src/routes/zen/v1/models.ts b/packages/console/app/src/routes/zen/v1/models.ts index 794f85029a..68c3cac694 100644 --- a/packages/console/app/src/routes/zen/v1/models.ts +++ b/packages/console/app/src/routes/zen/v1/models.ts @@ -28,7 +28,9 @@ export async function GET(input: APIEvent) { ) })() - const models = Object.keys(ZenData.list("full").models).filter((id) => !disabledModels.includes(id)) + const models = Object.keys(ZenData.list("full").models) + .filter((id) => !id.endsWith(":global")) + .filter((id) => !disabledModels.includes(id)) return buildModelsResponse(models) } From a4ab1408ebee6f4fc1e7d5439fd41e1719c33265 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 May 2026 15:32:08 -0400 Subject: [PATCH 41/70] zen: update rate limiter --- packages/console/app/src/routes/zen/util/handler.ts | 2 +- .../console/app/src/routes/zen/util/keyRateLimiter.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 7f36246ee5..16f9174325 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -116,7 +116,7 @@ export async function handler( const trialProviders = await trialLimiter?.check() const rateLimiter = modelInfo.allowAnonymous ? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request) - : createKeyRateLimiter(modelInfo.id, zenApiKey, input.request) + : createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request) await rateLimiter?.check() const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId) const stickyProvider = await stickyTracker?.get() diff --git a/packages/console/app/src/routes/zen/util/keyRateLimiter.ts b/packages/console/app/src/routes/zen/util/keyRateLimiter.ts index 2472776caa..0bf495f7db 100644 --- a/packages/console/app/src/routes/zen/util/keyRateLimiter.ts +++ b/packages/console/app/src/routes/zen/util/keyRateLimiter.ts @@ -4,11 +4,16 @@ import { RateLimitError } from "./error" import { i18n } from "~/i18n" import { localeFromRequest } from "~/lib/language" -export function createRateLimiter(modelId: string, zenApiKey: string | undefined, request: Request) { +export function createRateLimiter( + modelId: string, + rateLimit: number | undefined, + zenApiKey: string | undefined, + request: Request, +) { if (!zenApiKey) return const dict = i18n(localeFromRequest(request)) - const LIMIT = 300 + const LIMIT = rateLimit ?? 300 const yyyyMMddHHmm = new Date(Date.now()) .toISOString() .replace(/[^0-9]/g, "") From 2dffdfff4aa02d5c4df128035d0bfce2fd309ebd Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Thu, 7 May 2026 08:55:09 +1000 Subject: [PATCH 42/70] fix(server): apply cors before legacy auth (#26092) --- packages/opencode/src/server/server.ts | 4 ++-- packages/opencode/test/server/httpapi-cors.test.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index ca86599955..bc09667c29 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -107,10 +107,10 @@ function createHono(opts: CorsOptions, selection: ServerBackend.Selection = Serv const backendAttributes = ServerBackend.attributes(selection) const app = new Hono() .onError(ErrorMiddleware) - .use(AuthMiddleware) + .use(CorsMiddleware(opts)) .use(LoggerMiddleware(backendAttributes)) + .use(AuthMiddleware) .use(CompressionMiddleware) - .use(CorsMiddleware(opts)) .route("/global", GlobalRoutes()) const runtime = adapter.create(app) diff --git a/packages/opencode/test/server/httpapi-cors.test.ts b/packages/opencode/test/server/httpapi-cors.test.ts index 72265ad9bd..8d7e95dfbf 100644 --- a/packages/opencode/test/server/httpapi-cors.test.ts +++ b/packages/opencode/test/server/httpapi-cors.test.ts @@ -63,6 +63,19 @@ describe("HttpApi CORS", () => { }), ) + it.live("adds CORS headers to legacy unauthorized responses", () => + Effect.gen(function* () { + const response = yield* Effect.promise(async () => + Server.Legacy().app.request("/global/config", { + headers: { origin: "https://app.opencode.ai" }, + }), + ) + + expect(response.status).toBe(401) + expect(response.headers.get("access-control-allow-origin")).toBe("https://app.opencode.ai") + }), + ) + it.live("uses custom CORS origins passed to the server", () => Effect.gen(function* () { const listener = yield* Effect.acquireRelease( From 233fc5b91017b119cca046b892d6dc39c233c0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Thu, 7 May 2026 00:57:56 +0100 Subject: [PATCH 43/70] fix(provider): preserve assistant message content when reasoning blocks present (#21370) Co-authored-by: Omer Koren <54630488+omer-koren@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/opencode/src/session/message-v2.ts | 22 +++- .../opencode/test/session/message-v2.test.ts | 102 ++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 237fb527c0..ed09262d0e 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -854,13 +854,31 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* ( role: "assistant", parts: [], } + // Anthropic adaptive thinking can persist assistant turns like: + // step-start, reasoning(signature), text(""), step-start, + // reasoning(signature). The empty text part is a structural separator, + // but it does not carry the signature metadata itself. Dropping it shifts + // signed thinking positions after step-start splitting/provider regrouping; + // keeping it as "" is filtered by the AI SDK and rejected by Anthropic. + // It is unclear whether this shape originates in our stream processing, + // a proxy, or a lower-level library, but preserving a non-empty separator + // here is the only safe replay point we have. + // Use a single space so the separator survives replay without changing + // the neighboring signed reasoning blocks. Bedrock-hosted Claude stores + // the same signature under the bedrock metadata namespace. + const hasSignedReasoning = msg.parts.some((part) => { + if (part.type !== "reasoning") return false + return part.metadata?.anthropic?.signature != null || part.metadata?.bedrock?.signature != null + }) for (const part of msg.parts) { - if (part.type === "text") + if (part.type === "text") { + const text = part.text === "" && hasSignedReasoning ? " " : part.text assistantMessage.parts.push({ type: "text", - text: part.text, + text, ...(differentModel ? {} : { providerMetadata: part.metadata }), }) + } if (part.type === "step-start") assistantMessage.parts.push({ type: "step-start", diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index a7853be0b8..999b61b48e 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -1098,6 +1098,108 @@ describe("session.message-v2.toModelMessage", () => { }, ]) }) + + test("substitutes space for empty text between signed reasoning blocks", async () => { + // Reproduces the bug pattern: [reasoning(sig), text(""), reasoning(sig), text(full)] + const assistantID = "m-assistant" + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent"), + parts: [ + { ...basePart(assistantID, "p1"), type: "step-start" }, + { + ...basePart(assistantID, "p2"), + type: "reasoning", + text: "thinking-one", + metadata: { anthropic: { signature: "sig1" } }, + }, + { ...basePart(assistantID, "p3"), type: "text", text: "" }, + { ...basePart(assistantID, "p4"), type: "step-start" }, + { + ...basePart(assistantID, "p5"), + type: "reasoning", + text: "thinking-two", + metadata: { anthropic: { signature: "sig2" } }, + }, + { ...basePart(assistantID, "p6"), type: "text", text: "the answer" }, + ] as MessageV2.Part[], + }, + ] + + const result = await MessageV2.toModelMessages(input, model) + + // step-start splits into two assistant messages; SDK's groupIntoBlocks merges them later + expect(result).toHaveLength(2) + expect((result[0].content as any[]).find((p) => p.type === "text").text).toBe(" ") + expect((result[1].content as any[]).find((p) => p.type === "text").text).toBe("the answer") + }) + + test("substitutes space for empty text when reasoning signature is under 'bedrock' namespace", async () => { + // AWS Bedrock hosts Anthropic Claude but stores signatures under metadata.bedrock + const assistantID = "m-assistant-bedrock" + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent"), + parts: [ + { + ...basePart(assistantID, "p1"), + type: "reasoning", + text: "thinking-bedrock", + metadata: { bedrock: { signature: "bedrock-sig" } }, + }, + { ...basePart(assistantID, "p2"), type: "text", text: "" }, + { ...basePart(assistantID, "p3"), type: "text", text: "answer" }, + ] as MessageV2.Part[], + }, + ] + + const result = await MessageV2.toModelMessages(input, model) + + expect(result).toHaveLength(1) + const texts = (result[0].content as any[]).filter((p) => p.type === "text") + expect(texts.map((t) => t.text)).toStrictEqual([" ", "answer"]) + }) + + test("leaves empty text alone when reasoning has no Anthropic signature", async () => { + // Non-Anthropic providers' reasoning doesn't position-validate, so empty text + // should be filtered normally rather than substituted. + const assistantID = "m-assistant-unsigned" + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent"), + parts: [ + { ...basePart(assistantID, "p1"), type: "reasoning", text: "thinking" }, + { ...basePart(assistantID, "p2"), type: "text", text: "" }, + { ...basePart(assistantID, "p3"), type: "text", text: "answer" }, + ] as MessageV2.Part[], + }, + ] + + const result = await MessageV2.toModelMessages(input, model) + + expect(result).toHaveLength(1) + const texts = (result[0].content as any[]).filter((p) => p.type === "text") + expect(texts.map((t) => t.text)).toStrictEqual(["", "answer"]) + }) + + test("leaves empty text alone in assistant messages without reasoning", async () => { + const assistantID = "m-assistant-no-reasoning" + const input: MessageV2.WithParts[] = [ + { + info: assistantInfo(assistantID, "m-parent"), + parts: [ + { ...basePart(assistantID, "p1"), type: "text", text: "" }, + { ...basePart(assistantID, "p2"), type: "text", text: "hello" }, + ] as MessageV2.Part[], + }, + ] + + const result = await MessageV2.toModelMessages(input, model) + + expect(result).toHaveLength(1) + const texts = (result[0].content as any[]).filter((p) => p.type === "text") + expect(texts.map((t) => t.text)).toStrictEqual(["", "hello"]) + }) }) describe("session.message-v2.fromError", () => { From b2e3dc87ead239049b190973f7de05d0262e3eed Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 6 May 2026 19:33:52 -0500 Subject: [PATCH 44/70] feat: Update ACP support, modernize and fix misc issues (#25663) --- bun.lock | 4 +- packages/opencode/package.json | 2 +- packages/opencode/src/acp/agent.ts | 255 +++++++++++++----- packages/opencode/src/acp/session.ts | 6 + .../opencode/test/acp/agent-interface.test.ts | 5 +- 5 files changed, 197 insertions(+), 75 deletions(-) diff --git a/bun.lock b/bun.lock index 77ad4d982f..bcf1405a9f 100644 --- a/bun.lock +++ b/bun.lock @@ -335,7 +335,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.16.1", + "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/anthropic": "3.0.71", @@ -728,7 +728,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], "@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=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index db42557616..3126804ae0 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -80,7 +80,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.16.1", + "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/anthropic": "3.0.71", diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index d66c1b2583..ad930680d1 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -5,6 +5,8 @@ import { type AuthenticateRequest, type AuthMethod, type CancelNotification, + type CloseSessionRequest, + type CloseSessionResponse, type ForkSessionRequest, type ForkSessionResponse, type InitializeRequest, @@ -565,6 +567,7 @@ export class Agent implements ACPAgent { image: true, }, sessionCapabilities: { + close: {}, fork: {}, list: {}, resume: {}, @@ -627,6 +630,9 @@ export class Agent implements ACPAgent { // Store ACP session state await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model) + const messages = await this.loadSessionMessages(directory, sessionId) + this.restoreSessionStateFromMessages(sessionId, messages) + log.info("load_session", { sessionId, mcpServers: params.mcpServers.length }) const result = await this.loadSessionMode({ @@ -635,39 +641,6 @@ export class Agent implements ACPAgent { sessionId, }) - // Replay session history - const messages = await this.sdk.session - .messages( - { - sessionID: sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined - }) - - const lastUser = messages?.findLast((m) => m.info.role === "user")?.info - if (lastUser?.role === "user") { - result.models.currentModelId = `${lastUser.model.providerID}/${lastUser.model.modelID}` - this.sessionManager.setModel(sessionId, { - providerID: ProviderID.make(lastUser.model.providerID), - modelID: ModelID.make(lastUser.model.modelID), - }) - if (result.modes?.availableModes.some((m) => m.id === lastUser.agent)) { - result.modes.currentModeId = lastUser.agent - this.sessionManager.setMode(sessionId, lastUser.agent) - } - result.configOptions = buildConfigOptions({ - currentModelId: result.models.currentModelId, - availableModels: result.models.availableModels, - modes: result.modes, - }) - } - for (const msg of messages ?? []) { log.debug("replay message", msg) await this.processMessage(msg) @@ -756,6 +729,9 @@ export class Agent implements ACPAgent { const sessionId = forked.id await this.sessionManager.load(sessionId, directory, mcpServers, model) + const messages = await this.loadSessionMessages(directory, sessionId) + this.restoreSessionStateFromMessages(sessionId, messages) + log.info("fork_session", { sessionId, mcpServers: mcpServers.length }) const mode = await this.loadSessionMode({ @@ -764,20 +740,6 @@ export class Agent implements ACPAgent { sessionId, }) - const messages = await this.sdk.session - .messages( - { - sessionID: sessionId, - directory, - }, - { throwOnError: true }, - ) - .then((x) => x.data) - .catch((err) => { - log.error("unexpected error when fetching message", { error: err }) - return undefined - }) - for (const msg of messages ?? []) { log.debug("replay message", msg) await this.processMessage(msg) @@ -797,7 +759,7 @@ export class Agent implements ACPAgent { } } - async unstable_resumeSession(params: ResumeSessionRequest): Promise { + async resumeSession(params: ResumeSessionRequest): Promise { const directory = params.cwd const sessionId = params.sessionId const mcpServers = params.mcpServers ?? [] @@ -806,6 +768,9 @@ export class Agent implements ACPAgent { const model = await defaultModel(this.config, directory) await this.sessionManager.load(sessionId, directory, mcpServers, model) + const messages = await this.loadSessionMessages(directory, sessionId, 20) + this.restoreSessionStateFromMessages(sessionId, messages) + log.info("resume_session", { sessionId, mcpServers: mcpServers.length }) const result = await this.loadSessionMode({ @@ -828,6 +793,27 @@ export class Agent implements ACPAgent { } } + async closeSession(params: CloseSessionRequest): Promise { + const session = this.sessionManager.remove(params.sessionId) + if (!session) return {} + + await this.sdk.session + .abort( + { + sessionID: params.sessionId, + directory: session.cwd, + }, + { throwOnError: true }, + ) + .catch((error) => { + log.error("failed to abort session while closing ACP session", { error, sessionID: params.sessionId }) + }) + + this.permissionQueues.delete(params.sessionId) + log.info("close_session", { sessionId: params.sessionId }) + return {} + } + private async processMessage(message: SessionMessageResponse) { log.debug("process message", message) if (message.info.role !== "assistant" && message.info.role !== "user") return @@ -1159,23 +1145,26 @@ export class Agent implements ACPAgent { sessionId: string, ): Promise<{ availableModes: ModeOption[]; currentModeId?: string }> { const availableModes = await this.loadAvailableModes(directory) - const currentModeId = - this.sessionManager.get(sessionId).modeId || - (await (async () => { - if (!availableModes.length) return undefined - const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())) - const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id - this.sessionManager.setMode(sessionId, resolvedModeId) - return resolvedModeId - })()) + const storedModeId = this.sessionManager.get(sessionId).modeId + if (storedModeId && availableModes.some((mode) => mode.id === storedModeId)) { + return { availableModes, currentModeId: storedModeId } + } + + const currentModeId = await (async () => { + if (!availableModes.length) return undefined + const defaultAgentName = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultAgent())) + const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgentName)?.id ?? availableModes[0].id + this.sessionManager.setMode(sessionId, resolvedModeId) + return resolvedModeId + })() return { availableModes, currentModeId } } private async loadSessionMode(params: LoadSessionRequest) { const directory = params.cwd - const model = await defaultModel(this.config, directory) const sessionId = params.sessionId + const model = this.sessionManager.get(sessionId).model ?? (await defaultModel(this.config, directory)) const providers = await this.sdk.config.providers({ directory }).then((x) => x.data!.providers) const entries = sortProvidersByName(providers) @@ -1184,7 +1173,7 @@ export class Agent implements ACPAgent { if (currentVariant && !availableVariants.includes(currentVariant)) { this.sessionManager.setVariant(sessionId, undefined) } - const availableModels = buildAvailableModels(entries, { includeVariants: true }) + const availableModels = buildAvailableModels(entries) const modeState = await this.resolveModeState(directory, sessionId) const currentModeId = modeState.currentModeId const modes = currentModeId @@ -1267,13 +1256,15 @@ export class Agent implements ACPAgent { return { sessionId, models: { - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false), availableModels, }, modes, configOptions: buildConfigOptions({ - currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, true), + currentModelId: formatModelIdWithVariant(model, currentVariant, availableVariants, false), availableModels, + currentVariant, + availableVariants, modes, }), _meta: buildVariantMeta({ @@ -1296,6 +1287,24 @@ export class Agent implements ACPAgent { const entries = sortProvidersByName(providers) const availableVariants = modelVariantsFromProviders(entries, selection.model) + const modeState = await this.resolveModeState(session.cwd, session.id) + const modes = modeState.currentModeId + ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } + : undefined + + await this.connection.sessionUpdate({ + sessionId: session.id, + update: { + sessionUpdate: "config_option_update", + configOptions: buildConfigOptions({ + currentModelId: formatModelIdWithVariant(selection.model, selection.variant, availableVariants, false), + availableModels: buildAvailableModels(entries), + currentVariant: selection.variant, + availableVariants, + modes, + }), + }, + }) return { _meta: buildVariantMeta({ @@ -1327,6 +1336,14 @@ export class Agent implements ACPAgent { const selection = parseModelSelection(params.value, providers) this.sessionManager.setModel(session.id, selection.model) this.sessionManager.setVariant(session.id, selection.variant) + } else if (params.configId === "effort") { + if (typeof params.value !== "string") throw RequestError.invalidParams("effort value must be a string") + const current = session.model ?? (await defaultModel(this.config, session.cwd)) + const availableVariants = modelVariantsFromProviders(entries, current) + if (!availableVariants.includes(params.value)) { + throw RequestError.invalidParams(JSON.stringify({ error: `Effort not found: ${params.value}` })) + } + this.sessionManager.setVariant(session.id, params.value) } else if (params.configId === "mode") { if (typeof params.value !== "string") throw RequestError.invalidParams("mode value must be a string") const availableModes = await this.loadAvailableModes(session.cwd) @@ -1341,15 +1358,21 @@ export class Agent implements ACPAgent { const updatedSession = this.sessionManager.get(session.id) const model = updatedSession.model ?? (await defaultModel(this.config, session.cwd)) const availableVariants = modelVariantsFromProviders(entries, model) - const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, true) - const availableModels = buildAvailableModels(entries, { includeVariants: true }) + const currentModelId = formatModelIdWithVariant(model, updatedSession.variant, availableVariants, false) + const availableModels = buildAvailableModels(entries) const modeState = await this.resolveModeState(session.cwd, session.id) const modes = modeState.currentModeId ? { availableModes: modeState.availableModes, currentModeId: modeState.currentModeId } : undefined return { - configOptions: buildConfigOptions({ currentModelId, availableModels, modes }), + configOptions: buildConfigOptions({ + currentModelId, + availableModels, + currentVariant: updatedSession.variant, + availableVariants, + modes, + }), } } @@ -1546,6 +1569,37 @@ export class Agent implements ACPAgent { { throwOnError: true }, ) } + + private async loadSessionMessages(directory: string, sessionId: string, limit?: number) { + return this.sdk.session + .messages( + { + sessionID: sessionId, + directory, + limit, + }, + { throwOnError: true }, + ) + .then((x) => x.data) + .catch((error) => { + log.error("unexpected error when fetching message", { error }) + return undefined + }) + } + + private restoreSessionStateFromMessages(sessionId: string, messages: SessionMessageResponse[] | undefined) { + const lastUser = messages?.findLast((message) => message.info.role === "user")?.info + if (lastUser?.role !== "user") return + + this.sessionManager.setModel(sessionId, { + providerID: ProviderID.make(lastUser.model.providerID), + modelID: ModelID.make(lastUser.model.modelID), + }) + this.sessionManager.setVariant(sessionId, lastUser.model.variant) + if (lastUser.agent) { + this.sessionManager.setMode(sessionId, lastUser.agent) + } + } } function toToolKind(toolName: string): ToolKind { @@ -1629,11 +1683,11 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider if (specified && !providers.length) return specified + const lastUsed = await lastUsedModel(sdk, directory, providers) + if (lastUsed) return lastUsed + const opencodeProvider = providers.find((p) => p.id === "opencode") if (opencodeProvider) { - if (opencodeProvider.models["big-pickle"]) { - return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } - } const [best] = Provider.sort(Object.values(opencodeProvider.models)) if (best) { return { @@ -1653,8 +1707,38 @@ async function defaultModel(config: ACPConfig, cwd?: string): Promise<{ provider } if (specified) return specified + throw new Error("No models available") +} - return { providerID: ProviderID.opencode, modelID: ModelID.make("big-pickle") } +async function lastUsedModel( + sdk: OpencodeClient, + directory: string, + providers: Array<{ id: string; models: Record }>, +): Promise<{ providerID: ProviderID; modelID: ModelID } | undefined> { + const session = await sdk.session + .list({ directory, roots: true, limit: 1 }, { throwOnError: true }) + .then((x) => x.data?.[0]) + .catch((error) => { + log.error("failed to list sessions for default model", { error }) + return undefined + }) + if (!session) return + + const lastUser = await sdk.session + .messages({ sessionID: session.id, directory, limit: 20 }, { throwOnError: true }) + .then((x) => x.data?.findLast((message) => message.info.role === "user")?.info) + .catch((error) => { + log.error("failed to load session messages for default model", { error, sessionID: session.id }) + return undefined + }) + if (lastUser?.role !== "user") return + + const provider = providers.find((entry) => entry.id === lastUser.model.providerID) + if (!provider?.models[lastUser.model.modelID]) return + return { + providerID: ProviderID.make(lastUser.model.providerID), + modelID: ModelID.make(lastUser.model.modelID), + } } function parseUri( @@ -1757,8 +1841,14 @@ function formatModelIdWithVariant( includeVariant: boolean, ) { const base = `${model.providerID}/${model.modelID}` - if (!includeVariant || !variant || !availableVariants.includes(variant)) return base - return `${base}/${variant}` + if (!includeVariant || availableVariants.length === 0) return base + const selectedVariant = + variant && availableVariants.includes(variant) + ? variant + : availableVariants.includes(DEFAULT_VARIANT_VALUE) + ? DEFAULT_VARIANT_VALUE + : availableVariants[0] + return `${base}/${selectedVariant}` } function buildVariantMeta(input: { @@ -1810,6 +1900,8 @@ function parseModelSelection( function buildConfigOptions(input: { currentModelId: string availableModels: ModelOption[] + currentVariant?: string + availableVariants?: string[] modes?: { availableModes: ModeOption[]; currentModeId: string } | undefined }): SessionConfigOption[] { const options: SessionConfigOption[] = [ @@ -1822,6 +1914,22 @@ function buildConfigOptions(input: { options: input.availableModels.map((m) => ({ value: m.modelId, name: m.name })), }, ] + if (input.availableVariants?.length) { + options.push({ + id: "effort", + name: "Effort", + description: "Available effort levels for this model", + category: "thought_level", + type: "select", + currentValue: + input.currentVariant && input.availableVariants.includes(input.currentVariant) + ? input.currentVariant + : input.availableVariants.includes(DEFAULT_VARIANT_VALUE) + ? DEFAULT_VARIANT_VALUE + : input.availableVariants[0], + options: input.availableVariants.map((variant) => ({ value: variant, name: formatVariantName(variant) })), + }) + } if (input.modes) { options.push({ id: "mode", @@ -1839,4 +1947,11 @@ function buildConfigOptions(input: { return options } +function formatVariantName(variant: string) { + return variant + .split(/[_-]/) + .map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part)) + .join(" ") +} + export * as ACP from "./agent" diff --git a/packages/opencode/src/acp/session.ts b/packages/opencode/src/acp/session.ts index d932b65701..cc1ed0be30 100644 --- a/packages/opencode/src/acp/session.ts +++ b/packages/opencode/src/acp/session.ts @@ -113,4 +113,10 @@ export class ACPSessionManager { this.sessions.set(sessionId, session) return session } + + remove(sessionId: string): ACPSessionState | undefined { + const session = this.sessions.get(sessionId) + this.sessions.delete(sessionId) + return session + } } diff --git a/packages/opencode/test/acp/agent-interface.test.ts b/packages/opencode/test/acp/agent-interface.test.ts index 9fa67de829..7c4633d7d8 100644 --- a/packages/opencode/test/acp/agent-interface.test.ts +++ b/packages/opencode/test/acp/agent-interface.test.ts @@ -34,10 +34,11 @@ describe("acp.agent interface compliance", () => { "loadSession", "setSessionMode", "authenticate", - // Unstable - SDK checks these with unstable_ prefix + // Capability-gated methods checked by the SDK router "listSessions", + "resumeSession", + "closeSession", "unstable_forkSession", - "unstable_resumeSession", "unstable_setSessionModel", ] From dcfe4b0d5184cb93dd2232f1461641d6530e1abb Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 7 May 2026 00:34:09 +0000 Subject: [PATCH 45/70] sync release versions for v1.14.40 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index bcf1405a9f..d481de8e83 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -120,7 +120,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -147,7 +147,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -171,7 +171,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -195,7 +195,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.14.39", + "version": "1.14.40", "bin": { "opencode": "./bin/opencode", }, @@ -229,7 +229,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -283,7 +283,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -312,7 +312,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -328,7 +328,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.39", + "version": "1.14.40", "bin": { "opencode": "./bin/opencode", }, @@ -470,7 +470,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -505,7 +505,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "cross-spawn": "catalog:", }, @@ -520,7 +520,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -555,7 +555,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -604,7 +604,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index def3f65fc2..45908e45b8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.39", + "version": "1.14.40", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 78a4a1fd44..71d37d1553 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index bdfc576fb9..c1acfab6e0 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.39", + "version": "1.14.40", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index dc56d8bc29..9c0ce79d74 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.39", + "version": "1.14.40", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 1600bb877d..d9648b3243 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.39", + "version": "1.14.40", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/core/package.json b/packages/core/package.json index 88136cb51a..9d92e96e1d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.39", + "version": "1.14.40", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 60ccd6cfb6..431de79bc5 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 49509aa075..867d2155da 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.39", + "version": "1.14.40", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 8102023128..666198d55e 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.39" +version = "1.14.40" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.39/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.39/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.39/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.39/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.39/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 84219c5510..f5bd20d0be 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.39", + "version": "1.14.40", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 3126804ae0..245bb86621 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.39", + "version": "1.14.40", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 9bcf2a6f1f..fa9e4214e8 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 4ec95155c3..8029d2c9ae 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index f70692d76f..6d2cd71e30 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index f16dfdf134..3e875f7524 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.39", + "version": "1.14.40", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 295ac2ad10..59390274d5 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.39", + "version": "1.14.40", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index c78e2a1486..4052393c0d 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.39", + "version": "1.14.40", "publisher": "sst-dev", "repository": { "type": "git", From 3480cef52e4bb8fd5d155069786d5207f967ad3f Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 7 May 2026 00:46:33 +0000 Subject: [PATCH 46/70] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 3792b80503..a765e803d2 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-ynZFX8eCamzBuVpauYLbju/Cqbt2260JNumMUj79PKA=", - "aarch64-linux": "sha256-JCu7JZkdAAHTufWEJRV1gJErKvHFirq+qmVNIRPZ/0w=", - "aarch64-darwin": "sha256-9Dkt/poYBpLdtqA6L9pLe6GS435zFGb5rOYWE5rEnjA=", - "x86_64-darwin": "sha256-Nd5j28gAcM7+0ETBchjk9VojViHy3N/z2MkdU42YuCg=" + "x86_64-linux": "sha256-cgqwEUyOYcOnh07Wz20qkPIrDeBaCBmKiis6HO1EAIU=", + "aarch64-linux": "sha256-AVy0RQuuXiseIwJV9f9API8OEo1jcy84dVidkEXgnX8=", + "aarch64-darwin": "sha256-RIS3/SuSXaMV9WcTXxOJWGDw96LcCFT6E8Ktc28/544=", + "x86_64-darwin": "sha256-GBwXIZQxy5F7tH9TJyWAonX5aETbZ/veAjeznDtsYmk=" } } From 0b702704ae199fc7952f6a81d3816b09a0ff4645 Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 6 May 2026 23:01:14 -0400 Subject: [PATCH 47/70] zen: nano not used for title gen --- packages/web/src/content/docs/ar/zen.mdx | 2 +- packages/web/src/content/docs/bs/zen.mdx | 2 +- packages/web/src/content/docs/da/zen.mdx | 2 +- packages/web/src/content/docs/de/zen.mdx | 2 +- packages/web/src/content/docs/es/zen.mdx | 2 +- packages/web/src/content/docs/fr/zen.mdx | 2 +- packages/web/src/content/docs/it/zen.mdx | 2 +- packages/web/src/content/docs/ja/zen.mdx | 2 +- packages/web/src/content/docs/ko/zen.mdx | 2 +- packages/web/src/content/docs/nb/zen.mdx | 2 +- packages/web/src/content/docs/pl/zen.mdx | 2 +- packages/web/src/content/docs/pt-br/zen.mdx | 2 +- packages/web/src/content/docs/ru/zen.mdx | 2 +- packages/web/src/content/docs/th/zen.mdx | 2 +- packages/web/src/content/docs/tr/zen.mdx | 2 +- packages/web/src/content/docs/zen.mdx | 2 +- packages/web/src/content/docs/zh-cn/zen.mdx | 2 +- packages/web/src/content/docs/zh-tw/zen.mdx | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index a2e2aacfe9..33fd9493ba 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -165,7 +165,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | قد تلاحظ _Claude Haiku 3.5_ في سجل الاستخدام. هذا [نموذج منخفض التكلفة](/docs/config/#models) يُستخدم لتوليد عناوين جلساتك. diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index 89527763ca..3723cbaa3c 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -172,7 +172,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Možda ćete primijetiti _Claude Haiku 3.5_ u historiji korištenja. To je [low cost model](/docs/config/#models) koji se koristi za generisanje naslova vaših sesija. diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index 009ad42023..d45f785a59 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -172,7 +172,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Du vil måske bemærke _Claude Haiku 3.5_ i din brugshistorik. Det er en [lavprismodel](/docs/config/#models), som bruges til at generere titlerne på dine sessioner. diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 11550f61c3..5e6c8eee80 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -161,7 +161,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Möglicherweise siehst du _Claude Haiku 3.5_ in deinem Nutzungsverlauf. Das ist ein [kostengünstiges Modell](/docs/config/#models), das verwendet wird, um die Titel deiner Sessions zu generieren. diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index f1a08c7ba5..15436226a5 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -172,7 +172,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Puede que notes _Claude Haiku 3.5_ en tu historial de uso. Este es un [modelo de bajo costo](/docs/config/#models) que se usa para generar los títulos de tus sesiones. diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index 7710da2259..fdf14e8fb0 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -161,7 +161,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Vous remarquerez peut-être _Claude Haiku 3.5_ dans votre historique d'utilisation. Il s'agit d'un [modèle à faible coût](/docs/config/#models) utilisé pour générer les titres de vos sessions. diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index a3b8725535..a53d6a2ba1 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -172,7 +172,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Potresti notare _Claude Haiku 3.5_ nella cronologia di utilizzo. È un [modello a basso costo](/docs/config/#models) usato per generare i titoli delle tue sessioni. diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index 8fcdc6d46b..64427a72ec 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -161,7 +161,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | 使用履歴に _Claude Haiku 3.5_ が表示されることがあります。これはセッションのタイトル生成に使われる [low cost model](/docs/config/#models) です。 diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index eb99c29fe6..e80a5e8710 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -161,7 +161,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | 사용 기록에서 *Claude Haiku 3.5*를 볼 수 있습니다. 이는 세션 제목을 생성할 때 사용되는 [저비용 모델](/docs/config/#models)입니다. diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 8ab1762e1f..4bd1e6115e 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -172,7 +172,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Du vil kanskje legge merke til _Claude Haiku 3.5_ i brukshistorikken din. Dette er en [lavprismodell](/docs/config/#models) som brukes til å generere titlene på øktene dine. diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index 52906036c0..ebd16d7856 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -172,7 +172,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Możesz zauważyć _Claude Haiku 3.5_ w historii użycia. To [niedrogi model](/docs/config/#models), który służy do generowania tytułów Twoich sesji. diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index b35cfdbde5..1dcc98c5d5 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -161,7 +161,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Você pode notar _Claude Haiku 3.5_ no seu histórico de uso. Este é um [low cost model](/docs/config/#models) usado para gerar os títulos das suas sessões. diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index 919026447f..10c55fc4dd 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -172,7 +172,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Вы можете заметить _Claude Haiku 3.5_ в истории использования. Это [недорогая модель](/docs/config/#models), которая используется для генерации заголовков ваших сессий. diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index b3914b73c2..cb2556ef63 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -163,7 +163,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | คุณอาจสังเกตเห็น _Claude Haiku 3.5_ ในประวัติการใช้งานของคุณ นี่คือ [low cost model](/docs/config/#models) ที่ใช้สร้างชื่อ session ของคุณ diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index 3e53ba40d4..36c1bfc66e 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -161,7 +161,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | Kullanım geçmişinizde _Claude Haiku 3.5_ görebilirsiniz. Bu, oturum başlıklarınızı oluşturmak için kullanılan [düşük maliyetli bir modeldir](/docs/config/#models). diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 58baceb258..333e74434b 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -172,7 +172,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions. diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 03124a34f4..9ad7e6b53d 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -161,7 +161,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | 你可能会在使用记录中看到 _Claude Haiku 3.5_。这是一个[低成本模型](/docs/config/#models),用于生成会话标题。 diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index ebd48dea8f..9511bd9e24 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -166,7 +166,7 @@ https://opencode.ai/zen/v1/models | GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - | | GPT 5 | $1.07 | $8.50 | $0.107 | - | | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | -| GPT 5 Nano | Free | Free | Free | - | +| GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | 你可能會在使用紀錄中看到 _Claude Haiku 3.5_。這是一個[低成本模型](/docs/config/#models), 會用來產生工作階段的標題。 From 72ec05d0be201514ca506741567e57ecec0e72ee Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 7 May 2026 00:32:33 -0400 Subject: [PATCH 48/70] go: rate limit metadata --- .../console/app/src/routes/zen/util/error.ts | 11 ++++++- .../app/src/routes/zen/util/handler.ts | 30 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/error.ts b/packages/console/app/src/routes/zen/util/error.ts index b2a1d30d03..216b6564e7 100644 --- a/packages/console/app/src/routes/zen/util/error.ts +++ b/packages/console/app/src/routes/zen/util/error.ts @@ -13,4 +13,13 @@ class LimitError extends Error { } export class RateLimitError extends LimitError {} export class FreeUsageLimitError extends LimitError {} -export class SubscriptionUsageLimitError extends LimitError {} + +class SubscriptionUsageLimitError extends LimitError { + workspace: string + constructor(message: string, workspace: string, retryAfter?: number) { + super(message, retryAfter) + this.workspace = workspace + } +} +export class GoUsageLimitError extends SubscriptionUsageLimitError {} +export class BlackUsageLimitError extends SubscriptionUsageLimitError {} diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 16f9174325..c12129ff1d 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -23,7 +23,8 @@ import { ModelError, RateLimitError, FreeUsageLimitError, - SubscriptionUsageLimitError, + GoUsageLimitError, + BlackUsageLimitError, } from "./error" import { buildCostChunk, @@ -395,7 +396,8 @@ export async function handler( if ( error instanceof RateLimitError || error instanceof FreeUsageLimitError || - error instanceof SubscriptionUsageLimitError + error instanceof GoUsageLimitError || + error instanceof BlackUsageLimitError ) { const headers = new Headers() if (error.retryAfter) { @@ -404,7 +406,14 @@ export async function handler( return new Response( JSON.stringify({ type: "error", - error: { type: error.constructor.name, message: error.message }, + error: { + type: error.constructor.name, + message: error.message, + }, + metadata: + error instanceof GoUsageLimitError || error instanceof BlackUsageLimitError + ? { workspace: error.workspace } + : {}, }), { status: 429, headers }, ) @@ -693,10 +702,11 @@ export async function handler( timeUpdated: sub.timeFixedUpdated, }) if (result.status === "rate-limited") - throw new SubscriptionUsageLimitError( + throw new BlackUsageLimitError( t("zen.api.error.subscriptionQuotaExceeded", { retryIn: formatRetryTime(result.resetInSec), }), + authInfo.workspaceID, result.resetInSec, ) } @@ -711,10 +721,11 @@ export async function handler( timeUpdated: sub.timeRollingUpdated, }) if (result.status === "rate-limited") - throw new SubscriptionUsageLimitError( + throw new BlackUsageLimitError( t("zen.api.error.subscriptionQuotaExceeded", { retryIn: formatRetryTime(result.resetInSec), }), + authInfo.workspaceID, result.resetInSec, ) } @@ -739,8 +750,9 @@ export async function handler( timeUpdated: sub.timeWeeklyUpdated, }) if (result.status === "rate-limited") - throw new SubscriptionUsageLimitError( + throw new GoUsageLimitError( t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), + authInfo.workspaceID, result.resetInSec, ) } @@ -754,8 +766,9 @@ export async function handler( timeSubscribed: sub.timeCreated, }) if (result.status === "rate-limited") - throw new SubscriptionUsageLimitError( + throw new GoUsageLimitError( t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), + authInfo.workspaceID, result.resetInSec, ) } @@ -769,8 +782,9 @@ export async function handler( timeUpdated: sub.timeRollingUpdated, }) if (result.status === "rate-limited") - throw new SubscriptionUsageLimitError( + throw new GoUsageLimitError( t("zen.api.error.subscriptionQuotaExceededUseFreeModels"), + authInfo.workspaceID, result.resetInSec, ) } From ba1ec62caf7c114ffe3d422a51c90c1e572f15e4 Mon Sep 17 00:00:00 2001 From: carmit hershman <78722358+carmithersh@users.noreply.github.com> Date: Thu, 7 May 2026 08:37:14 +0300 Subject: [PATCH 49/70] docs: add opencode-jfrog-plugin to ecosystem list for JFrog integration (#26019) --- packages/web/src/content/docs/ecosystem.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index 055daf1419..55f0bcdaac 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -52,6 +52,7 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [opencode-worktree](https://github.com/kdcokenny/opencode-worktree) | Zero-friction git worktrees for OpenCode | | [opencode-sentry-monitor](https://github.com/stolinski/opencode-sentry-monitor) | Trace and debug your AI agents with Sentry AI Monitoring | | [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI | +| [opencode-jfrog-plugin](https://github.com/jfrog/opencode-jfrog-plugin) | JFrog Plugin for seamless integration of Opencode users to JFrog platform | --- From 9b30ee2db217925b31064e71a65a2ee57c130611 Mon Sep 17 00:00:00 2001 From: Jesse <82005785+jessedi0n@users.noreply.github.com> Date: Thu, 7 May 2026 07:39:14 +0200 Subject: [PATCH 50/70] fix(desktop): add macOS settings menu entry (#26081) Co-authored-by: jesse.mahnken --- packages/desktop/src/main/menu.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/desktop/src/main/menu.ts b/packages/desktop/src/main/menu.ts index 0d9a697fa9..2d5a900f39 100644 --- a/packages/desktop/src/main/menu.ts +++ b/packages/desktop/src/main/menu.ts @@ -23,6 +23,11 @@ export function createMenu(deps: Deps) { enabled: UPDATER_ENABLED, click: () => deps.checkForUpdates(), }, + { + label: "Settings", + accelerator: "Cmd+,", + click: () => deps.trigger("settings.open"), + }, { label: "Reload Webview", click: () => deps.reload(), From 54a78c92246de620234200af3649f8392b3f6761 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Thu, 7 May 2026 13:48:56 +0800 Subject: [PATCH 51/70] feat(desktop): move server to utilityProcess (#25962) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/desktop/electron.vite.config.ts | 2 +- packages/desktop/src/main/apps.ts | 55 +++--- packages/desktop/src/main/env.d.ts | 1 + packages/desktop/src/main/index.ts | 79 +++++---- packages/desktop/src/main/ipc.ts | 2 +- packages/desktop/src/main/server.ts | 217 ++++++++++++++++++++--- packages/desktop/src/main/sidecar.ts | 178 +++++++++++++++++++ 7 files changed, 441 insertions(+), 93 deletions(-) create mode 100644 packages/desktop/src/main/sidecar.ts diff --git a/packages/desktop/electron.vite.config.ts b/packages/desktop/electron.vite.config.ts index a352e03fdd..52aa699ff6 100644 --- a/packages/desktop/electron.vite.config.ts +++ b/packages/desktop/electron.vite.config.ts @@ -37,7 +37,7 @@ export default defineConfig({ }, build: { rollupOptions: { - input: { index: "src/main/index.ts" }, + input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" }, }, externalizeDeps: { include: [nodePtyPkg] }, }, diff --git a/packages/desktop/src/main/apps.ts b/packages/desktop/src/main/apps.ts index 174da94a5d..bf25417b83 100644 --- a/packages/desktop/src/main/apps.ts +++ b/packages/desktop/src/main/apps.ts @@ -1,14 +1,22 @@ -import { execFileSync } from "node:child_process" -import { existsSync, readFileSync, readdirSync } from "node:fs" +import { execFile, execFileSync } from "node:child_process" +import { access, readFile, readdir } from "node:fs/promises" import { dirname, extname, join } from "node:path" +import util from "node:util" -export function checkAppExists(appName: string): boolean { +const execFilePromise = util.promisify(execFile) + +const exists = (path: string) => + access(path) + .then(() => true) + .catch(() => false) + +export function checkAppExists(appName: string) { if (process.platform === "win32") return true if (process.platform === "linux") return true return checkMacosApp(appName) } -export function resolveAppPath(appName: string): string | null { +export function resolveAppPath(appName: string) { if (process.platform !== "win32") return appName return resolveWindowsAppPath(appName) } @@ -32,26 +40,25 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string } } -function checkMacosApp(appName: string) { +async function checkMacosApp(appName: string) { const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`] const home = process.env.HOME if (home) locations.push(`${home}/Applications/${appName}.app`) - if (locations.some((location) => existsSync(location))) return true - - try { - execFileSync("which", [appName]) - return true - } catch { - return false + for (const location of locations) { + if (await exists(location)) return true } + + return execFilePromise("which", [appName]) + .then(() => true) + .catch(() => false) } -function resolveWindowsAppPath(appName: string): string | null { +async function resolveWindowsAppPath(appName: string): Promise { let output: string try { - output = execFileSync("where", [appName]).toString() + output = execFilePromise("where", [appName]).toString() } catch { return null } @@ -66,8 +73,8 @@ function resolveWindowsAppPath(appName: string): string | null { const exe = paths.find((path) => hasExt(path, "exe")) if (exe) return exe - const resolveCmd = (path: string) => { - const content = readFileSync(path, "utf8") + const resolveCmd = async (path: string) => { + const content = await readFile(path, "utf8") for (const token of content.split('"').map((value: string) => value.trim())) { const lower = token.toLowerCase() if (!lower.includes(".exe")) continue @@ -85,10 +92,10 @@ function resolveWindowsAppPath(appName: string): string | null { return join(current, part) }, base) - if (existsSync(resolved)) return resolved + if (await exists(resolved)) return resolved } - if (existsSync(token)) return token + if (await exists(token)) return token } return null @@ -96,20 +103,20 @@ function resolveWindowsAppPath(appName: string): string | null { for (const path of paths) { if (hasExt(path, "cmd") || hasExt(path, "bat")) { - const resolved = resolveCmd(path) + const resolved = await resolveCmd(path) if (resolved) return resolved } if (!extname(path)) { const cmd = `${path}.cmd` - if (existsSync(cmd)) { - const resolved = resolveCmd(cmd) + if (await exists(cmd)) { + const resolved = await resolveCmd(cmd) if (resolved) return resolved } const bat = `${path}.bat` - if (existsSync(bat)) { - const resolved = resolveCmd(bat) + if (await exists(bat)) { + const resolved = await resolveCmd(bat) if (resolved) return resolved } } @@ -126,7 +133,7 @@ function resolveWindowsAppPath(appName: string): string | null { const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))] for (const dir of dirs) { try { - for (const entry of readdirSync(dir)) { + for (const entry of await readdir(dir)) { const candidate = join(dir, entry) if (!hasExt(candidate, "exe")) continue const stem = entry.replace(/\.exe$/i, "") diff --git a/packages/desktop/src/main/env.d.ts b/packages/desktop/src/main/env.d.ts index 1de56e1c90..eee21e48cb 100644 --- a/packages/desktop/src/main/env.d.ts +++ b/packages/desktop/src/main/env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv } + declare module "virtual:opencode-server" { export namespace Server { export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index d3c8fcc04e..f75cd719a2 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -47,7 +47,15 @@ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigratio import { initLogging } from "./logging" import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" -import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" +import { + getDefaultServerUrl, + getWslConfig, + preferAppEnv, + setDefaultServerUrl, + setWslConfig, + spawnLocalServer, + type SidecarListener, +} from "./server" import { createLoadingWindow, createMainWindow, @@ -55,15 +63,13 @@ import { setBackgroundColor, setDockIcon, } from "./windows" -import { drizzle } from "drizzle-orm/node-sqlite/driver" -import type { Server } from "virtual:opencode-server" import { migrate } from "./migrate" const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } let mainWindow: BrowserWindow | null = null -let server: Server.Listener | null = null +let server: SidecarListener | null = null const loadingComplete = defer() const pendingDeepLinks: string[] = [] @@ -107,6 +113,8 @@ function setupApp() { return } + preferAppEnv(app.getPath("userData")) + app.on("second-instance", (_event: Event, argv: string[]) => { const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) if (urls.length) { @@ -123,17 +131,16 @@ function setupApp() { }) app.on("before-quit", () => { - killSidecar() + void killSidecar() }) app.on("will-quit", () => { - killSidecar() + void killSidecar() }) for (const signal of ["SIGINT", "SIGTERM"] as const) { process.on(signal, () => { - killSidecar() - app.exit(0) + void killSidecar().finally(() => app.exit(0)) }) } @@ -184,7 +191,6 @@ function setInitStep(step: InitStep) { async function initialize() { const needsMigration = !sqliteFileExists() - const sqliteDone = needsMigration ? defer() : undefined let overlay: BrowserWindow | null = null const port = await getSidecarPort() @@ -199,31 +205,26 @@ async function initialize() { setInitStep({ phase: "sqlite_waiting" }) if (overlay) sendSqliteMigrationProgress(overlay, progress) if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) - if (progress.type === "Done") sqliteDone?.resolve() }) - if (needsMigration) { - const { Database, JsonMigration } = await import("virtual:opencode-server") - await JsonMigration.run(drizzle({ client: Database.Client().$client }), { - progress: (event: { current: number; total: number }) => { - const percent = Math.round(event.current / event.total) * 100 - initEmitter.emit("sqlite", { type: "InProgress", value: percent }) - }, - }) - initEmitter.emit("sqlite", { type: "Done" }) - - sqliteDone?.resolve() - } - - if (needsMigration) { - await sqliteDone?.promise - } - logger.log("spawning sidecar", { url }) - const { listener, health } = await spawnLocalServer(hostname, port, password, () => { - ensureLoopbackNoProxy() - useEnvProxy() - }) + const { listener, health } = await spawnLocalServer( + hostname, + port, + password, + () => { + ensureLoopbackNoProxy() + useEnvProxy() + }, + { + needsMigration, + userDataPath: app.getPath("userData"), + onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress), + onStdout: (message) => logger.log("sidecar stdout", { message }), + onStderr: (message) => logger.warn("sidecar stderr", { message }), + onExit: (code) => logger.warn("sidecar exited", { code }), + }, + ) server = listener serverReady.resolve({ url, @@ -273,9 +274,10 @@ function wireMenu() { }, reload: () => mainWindow?.reload(), relaunch: () => { - killSidecar() - app.relaunch() - app.exit(0) + void killSidecar().finally(() => { + app.relaunch() + app.exit(0) + }) }, }) } @@ -304,7 +306,7 @@ registerIpcHandlers({ getDisplayBackend: async () => null, setDisplayBackend: async () => undefined, parseMarkdown: async (markdown) => parseMarkdown(markdown), - checkAppExists: async (appName) => checkAppExists(appName), + checkAppExists: (appName) => checkAppExists(appName), wslPath: async (path, mode) => wslPath(path, mode), resolveAppPath: async (appName) => resolveAppPath(appName), loadingWindowComplete: () => loadingComplete.resolve(), @@ -314,10 +316,11 @@ registerIpcHandlers({ setBackgroundColor: (color) => setBackgroundColor(color), }) -function killSidecar() { +async function killSidecar() { if (!server) return - server.stop() + const current = server server = null + await current.stop() } function ensureLoopbackNoProxy() { @@ -440,7 +443,7 @@ async function installUpdate() { logger.log("installing downloaded update", { version: downloadedUpdateVersion, }) - killSidecar() + await killSidecar() autoUpdater.quitAndInstall() } diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 1c4af0eb60..dbcd4239dc 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -19,7 +19,7 @@ const pickerFilters = (ext?: string[]) => { } type Deps = { - killSidecar: () => void + killSidecar: () => Promise | void awaitInitialization: (sendStep: (step: InitStep) => void) => Promise getWindowConfig: () => Promise | WindowConfig consumeInitialDeepLinks: () => Promise | string[] diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index 4b8cb04943..635a93578a 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -1,12 +1,37 @@ -import { app } from "electron" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { app, utilityProcess } from "electron" +import type { Details } from "electron" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" -import { getUserShell, loadShellEnv } from "./shell-env" +import { getUserShell, loadShellEnv, mergeShellEnv } from "./shell-env" import { getStore } from "./store" +import type { SqliteMigrationProgress } from "../preload/types" export type WslConfig = { enabled: boolean } export type HealthCheck = { wait: Promise } +type SidecarMessage = + | { type: "sqlite"; progress: SqliteMigrationProgress } + | { type: "ready" } + | { type: "stopped" } + | { type: "error"; error: { message: string; stack?: string } } + +export type SidecarListener = { stop: () => Promise } + +const SIDECAR_SERVICE_NAME = "opencode server" +const SIDECAR_START_STALL_TIMEOUT = 60_000 +const SIDECAR_STOP_TIMEOUT = 6_000 + +type SpawnLocalServerOptions = { + needsMigration: boolean + userDataPath: string + onSqliteProgress?: (progress: SqliteMigrationProgress) => void + onStdout?: (message: string) => void + onStderr?: (message: string) => void + onExit?: (code: number) => void +} + export function getDefaultServerUrl(): string | null { const value = getStore().get(DEFAULT_SERVER_URL_KEY) return typeof value === "string" ? value : null @@ -30,49 +55,155 @@ export function setWslConfig(config: WslConfig) { getStore().set(WSL_ENABLED_KEY, config.enabled) } -export async function spawnLocalServer(hostname: string, port: number, password: string, configureEnv?: () => void) { - prepareServerEnv(password) +export function preferAppEnv(userDataPath: string) { + const shell = process.platform === "win32" ? null : getUserShell() + Object.assign( + process.env, + mergeShellEnv(shell ? loadShellEnv(shell) : null, { + ...process.env, + OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", + OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", + OPENCODE_CLIENT: "desktop", + XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath, + }), + ) +} + +export async function spawnLocalServer( + hostname: string, + port: number, + password: string, + configureEnv: () => void, + options: SpawnLocalServerOptions, +) { configureEnv?.() - const { Log, Server } = await import("virtual:opencode-server") - await Log.init({ level: "WARN" }) - const listener = await Server.listen({ - port, - hostname, - username: "opencode", - password, - cors: ["oc://renderer"], + const sidecar = join(dirname(fileURLToPath(import.meta.url)), "sidecar.js") + const child = utilityProcess.fork(sidecar, [], { + cwd: process.cwd(), + env: createSidecarEnv(), + serviceName: SIDECAR_SERVICE_NAME, + stdio: "pipe", + }) + let exited = false + const exit = defer() + + const onProcessGone = (_event: unknown, details: Details) => { + if (details.type !== "Utility" || details.name !== SIDECAR_SERVICE_NAME) return + options.onStderr?.(`utility process gone reason=${details.reason} exitCode=${details.exitCode}`) + } + + app.on("child-process-gone", onProcessGone) + child.once("exit", (code) => { + exited = true + app.off("child-process-gone", onProcessGone) + options.onExit?.(code) + exit.resolve(code) + }) + child.on("error", (error) => options.onStderr?.(`utility process error: ${serializeError(error).message}`)) + + child.stdout?.on("data", (chunk: Buffer) => options.onStdout?.(chunk.toString("utf8").trimEnd())) + child.stderr?.on("data", (chunk: Buffer) => options.onStderr?.(chunk.toString("utf8").trimEnd())) + + await new Promise((resolve, reject) => { + let done = false + let timeout: NodeJS.Timeout + + const fail = (error: Error) => { + if (done) return + done = true + cleanup() + reject(error) + } + + const refreshTimeout = () => { + clearTimeout(timeout) + timeout = setTimeout(() => { + fail(new Error(`Sidecar did not become ready within ${SIDECAR_START_STALL_TIMEOUT}ms: ${sidecar}`)) + }, SIDECAR_START_STALL_TIMEOUT) + } + + const onMessage = (message: SidecarMessage) => { + if (message.type === "sqlite") { + refreshTimeout() + options.onSqliteProgress?.(message.progress) + return + } + if (message.type === "ready") { + if (done) return + done = true + cleanup() + resolve() + return + } + if (message.type === "error") { + fail(Object.assign(new Error(message.error.message), { stack: message.error.stack })) + } + } + const onExit = (code: number) => { + fail(new Error(`Sidecar exited before ready with code ${code}`)) + } + const cleanup = () => { + clearTimeout(timeout) + child.off("message", onMessage) + child.off("exit", onExit) + } + + child.on("message", onMessage) + child.on("exit", onExit) + refreshTimeout() + child.postMessage({ + type: "start", + hostname, + port, + password, + userDataPath: options.userDataPath, + needsMigration: options.needsMigration, + }) + }).catch((error) => { + if (!exited) child.kill() + throw error }) const wait = (async () => { const url = `http://${hostname}:${port}` + let healthy = false + const gone = exit.promise.then((code) => { + if (healthy) return + throw new Error(`Sidecar exited before health check passed with code ${code}`) + }) const ready = async () => { while (true) { await new Promise((resolve) => setTimeout(resolve, 100)) - if (await checkHealth(url, password)) return + if (await checkHealth(url, password)) { + healthy = true + return + } } } - await ready() + await Promise.race([ready(), gone]) })() - return { listener, health: { wait } } -} + let stopping: Promise | undefined -function prepareServerEnv(password: string) { - const shell = process.platform === "win32" ? null : getUserShell() - const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} - const env = { - ...process.env, - ...shellEnv, - OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", - OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", - OPENCODE_CLIENT: "desktop", - OPENCODE_SERVER_USERNAME: "opencode", - OPENCODE_SERVER_PASSWORD: password, - XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? app.getPath("userData"), + return { + listener: { + stop: () => { + if (stopping) return stopping + if (exited) return Promise.resolve() + child.postMessage({ type: "stop" }) + stopping = Promise.race([ + exit.promise.then(() => undefined), + delay(SIDECAR_STOP_TIMEOUT).then(() => { + if (!exited) child.kill() + }), + ]) + return stopping + }, + }, + health: { wait }, } - Object.assign(process.env, env) } export async function checkHealth(url: string, password?: string | null): Promise { @@ -100,3 +231,31 @@ export async function checkHealth(url: string, password?: string | null): Promis return false } } + +function createSidecarEnv(): Record { + const env = Object.fromEntries( + Object.entries(process.env).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])), + ) + delete env.DEBUG + if (process.platform === "linux") delete env.LD_PRELOAD + return env +} + +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function serializeError(error: unknown) { + if (error instanceof Error) return { message: error.message, stack: error.stack } + return { message: String(error) } +} + +function defer() { + let resolve!: (value: T) => void + let reject!: (error: Error) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} diff --git a/packages/desktop/src/main/sidecar.ts b/packages/desktop/src/main/sidecar.ts new file mode 100644 index 0000000000..e7d652b6e1 --- /dev/null +++ b/packages/desktop/src/main/sidecar.ts @@ -0,0 +1,178 @@ +import { drizzle } from "drizzle-orm/node-sqlite/driver" +import * as http from "node:http" +import * as tls from "node:tls" + +type NodeHttpWithEnvProxy = typeof http & { + setGlobalProxyFromEnv: () => void +} + +type NodeTlsWithSystemCertificates = typeof tls & { + getCACertificates: (type: "default" | "system") => string[] + setDefaultCACertificates: (certificates: string[]) => void +} + +type StartCommand = { + type: "start" + hostname: string + port: number + password: string + userDataPath: string + needsMigration: boolean +} + +type StopCommand = { type: "stop" } +type SidecarCommand = StartCommand | StopCommand + +type SidecarMessage = + | { type: "sqlite"; progress: { type: "InProgress"; value: number } | { type: "Done" } } + | { type: "ready" } + | { type: "stopped" } + | { type: "error"; error: { message: string; stack?: string } } + +type ParentPort = { + postMessage(message: SidecarMessage): void + on(event: "message", listener: (event: { data: unknown }) => void): void +} + +type Listener = { + stop(close?: boolean): void | Promise +} + +const parentPort = getParentPort() +let listener: Listener | undefined + +parentPort.on("message", (event) => { + const command = parseCommand(event.data) + if (!command) return + if (command.type === "stop") { + void stop() + return + } + void start(command) +}) + +async function start(command: StartCommand) { + try { + prepareSidecarEnv(command.password, command.userDataPath) + ensureLoopbackNoProxy() + useSystemCertificates() + useEnvProxy() + const { Database, JsonMigration, Log, Server } = await import("virtual:opencode-server") + await Log.init({ level: "WARN" }) + + if (command.needsMigration) { + await JsonMigration.run(drizzle({ client: Database.Client().$client }), { + progress: (event: { current: number; total: number }) => { + parentPort.postMessage({ + type: "sqlite", + progress: { + type: "InProgress", + value: event.total === 0 ? 100 : Math.round((event.current / event.total) * 100), + }, + }) + }, + }) + parentPort.postMessage({ type: "sqlite", progress: { type: "Done" } }) + } + + listener = await Server.listen({ + port: command.port, + hostname: command.hostname, + username: "opencode", + password: command.password, + cors: ["oc://renderer"], + }) + parentPort.postMessage({ type: "ready" }) + } catch (error) { + parentPort.postMessage({ type: "error", error: serializeError(error) }) + setImmediate(() => process.exit(1)) + } +} + +async function stop() { + try { + await listener?.stop() + } finally { + listener = undefined + parentPort.postMessage({ type: "stopped" }) + setImmediate(() => process.exit(0)) + } +} + +function prepareSidecarEnv(password: string, userDataPath: string) { + Object.assign(process.env, { + OPENCODE_SERVER_USERNAME: "opencode", + OPENCODE_SERVER_PASSWORD: password, + XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath, + }) +} + +function ensureLoopbackNoProxy() { + const loopback = ["127.0.0.1", "localhost", "::1"] + const upsert = (key: string) => { + const items = (process.env[key] ?? "") + .split(",") + .map((value: string) => value.trim()) + .filter((value: string) => Boolean(value)) + + for (const host of loopback) { + if (items.some((value: string) => value.toLowerCase() === host)) continue + items.push(host) + } + + process.env[key] = items.join(",") + } + + upsert("NO_PROXY") + upsert("no_proxy") +} + +function useSystemCertificates() { + try { + const nodeTls = tls as NodeTlsWithSystemCertificates + nodeTls.setDefaultCACertificates([ + ...new Set([...nodeTls.getCACertificates("default"), ...nodeTls.getCACertificates("system")]), + ]) + } catch (error) { + console.warn("failed to load system certificates", error) + } +} + +function useEnvProxy() { + try { + ;(http as NodeHttpWithEnvProxy).setGlobalProxyFromEnv() + } catch (error) { + console.warn("failed to load proxy environment", error) + } +} + +function parseCommand(value: unknown): SidecarCommand | undefined { + if (!value || typeof value !== "object") return + const command = value as Partial + if (command.type === "stop") return { type: "stop" } + if (command.type !== "start") return + if (typeof command.hostname !== "string") return + if (typeof command.port !== "number") return + if (typeof command.password !== "string") return + if (typeof command.userDataPath !== "string") return + if (typeof command.needsMigration !== "boolean") return + return { + type: "start", + hostname: command.hostname, + port: command.port, + password: command.password, + userDataPath: command.userDataPath, + needsMigration: command.needsMigration, + } +} + +function serializeError(error: unknown) { + if (error instanceof Error) return { message: error.message, stack: error.stack } + return { message: String(error) } +} + +function getParentPort() { + const port = process.parentPort as ParentPort | undefined + if (!port) throw new Error("Sidecar parent port unavailable") + return port +} From 293bb422fa920a426a0bf98ef95a3f6d77f9c504 Mon Sep 17 00:00:00 2001 From: Bence Ferdinandy Date: Thu, 7 May 2026 07:52:07 +0200 Subject: [PATCH 52/70] fix(format): restore stdout/stderr ignore for formatter processes (#26037) Co-authored-by: Aiden Cline --- packages/opencode/src/format/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 7c122e3501..a61eb7be29 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -91,6 +91,9 @@ export const layer = Layer.effect( cwd: dir, env: item.environment, extendEnv: true, + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", }), ) .pipe( From f8aa4a3be0cf3fae670b69c8940f42a33963e7b5 Mon Sep 17 00:00:00 2001 From: Victor Navarro Date: Thu, 7 May 2026 09:56:10 +0200 Subject: [PATCH 53/70] chore: simplify honeycomb alerts (#26142) --- bun.lock | 9 - infra/console.ts | 6 +- infra/monitoring.ts | 367 ++++-------------- infra/secret.ts | 7 + packages/console/app/package.json | 1 - .../app/src/routes/honeycomb/webhook.ts | 81 ++++ .../app/src/routes/incident/webhook.ts | 77 ---- packages/console/core/src/util/crypto.ts | 8 + packages/console/core/sst-env.d.ts | 4 +- packages/console/function/sst-env.d.ts | 4 +- packages/console/resource/sst-env.d.ts | 4 +- packages/enterprise/sst-env.d.ts | 4 +- packages/function/sst-env.d.ts | 4 +- sst-env.d.ts | 4 +- sst.config.ts | 13 +- 15 files changed, 185 insertions(+), 408 deletions(-) create mode 100644 packages/console/app/src/routes/honeycomb/webhook.ts delete mode 100644 packages/console/app/src/routes/incident/webhook.ts create mode 100644 packages/console/core/src/util/crypto.ts diff --git a/bun.lock b/bun.lock index d481de8e83..8e3c9b7452 100644 --- a/bun.lock +++ b/bun.lock @@ -107,7 +107,6 @@ "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", - "svix": "1.92.2", "vite": "catalog:", "zod": "catalog:", }, @@ -2168,8 +2167,6 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], - "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], - "@standard-community/standard-json": ["@standard-community/standard-json@0.3.5", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "@types/json-schema": "^7.0.15", "@valibot/to-json-schema": "^1.3.0", "arktype": "^2.1.20", "effect": "^3.16.8", "quansync": "^0.2.11", "sury": "^10.0.0", "typebox": "^1.0.17", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.5" }, "optionalPeers": ["@valibot/to-json-schema", "arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-to-json-schema"] }, "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA=="], "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], @@ -3180,8 +3177,6 @@ "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="], @@ -4656,8 +4651,6 @@ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], - "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -4726,8 +4719,6 @@ "sury": ["sury@11.0.0-alpha.4", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="], - "svix": ["svix@1.92.2", "", { "dependencies": { "standardwebhooks": "1.0.0" } }, "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ=="], - "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="], diff --git a/infra/console.ts b/infra/console.ts index d92fcaa8e2..ab6502a8f8 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -1,5 +1,6 @@ import { domain } from "./stage" import { EMAILOCTOPUS_API_KEY } from "./app" +import { SECRET } from "./secret" //////////////// // DATABASE @@ -221,8 +222,6 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", { properties: { value: stripeWebhook.secret }, }) -const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET") -const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL") const gatewayKv = new sst.cloudflare.Kv("GatewayKv") @@ -233,6 +232,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv") const bucket = new sst.cloudflare.Bucket("ZenData") const bucketNew = new sst.cloudflare.Bucket("ZenDataNew") +const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL") const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID") const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY") @@ -254,8 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", { database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, - INCIDENT_WEBHOOK_SIGNING_SECRET, DISCORD_INCIDENT_WEBHOOK_URL, + SECRET.HoneycombWebhookSecret, STRIPE_SECRET_KEY, EMAILOCTOPUS_API_KEY, AWS_SES_ACCESS_KEY_ID, diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 4fb7183a2f..4e22e3d812 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -1,318 +1,91 @@ -const displayName = (s: string) => - s - .split("-") - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(" ") - .replace(/(?<=\d) (?=\d)/g, ".") +import { SECRET } from "./secret" +import { domain } from "./stage" -const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "") - -const varSpec = (label: string, name: string) => - $jsonStringify({ - content: [ - { - content: [ - { - attrs: { - name, - label, - missing: false, - }, - type: "varSpec", - }, - ], - type: "paragraph", - }, - ], - type: "doc", - }) - -const fields = { - model: incident.getAlertAttributeOutput({ name: "Model" }), - product: incident.getAlertAttributeOutput({ name: "Product" }), -} - -const alertSource = new incident.AlertSource("HoneycombAlertSource", { - name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`, - sourceType: "honeycomb", - template: { - title: { - literal: varSpec("Payload -> Title", "title"), - }, - description: { - literal: varSpec("Payload -> Description", "description"), - }, - attributes: [ - { - alertAttributeId: fields.model.id, - binding: { - value: { - reference: 'expressions["model"]', - }, - mergeStrategy: "first_wins", - }, - }, - { - alertAttributeId: fields.product.id, - binding: { - value: { - reference: 'expressions["product"]', - }, - mergeStrategy: "first_wins", - }, - }, - ], - expressions: [ - { - label: "Model", - operations: [ - { - operationType: "parse", - parse: { - returns: { - array: false, - type: fields.model.type, - }, - source: "$['model']", - }, - }, - ], - reference: "model", - rootReference: "payload", - }, - { - label: "Product", - operations: [ - { - operationType: "parse", - parse: { - returns: { - array: false, - type: fields.product.type, - }, - source: "$['product']", - }, - }, - ], - reference: "product", - rootReference: "payload", - }, - ], - }, -}) - -const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, { - name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`, - url: alertSource.alertEventsUrl, - secret: alertSource.secretToken, +const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", { + name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`, + url: `https://${domain}/honeycomb/webhook`, + secret: SECRET.HoneycombWebhookSecret.result, templates: [ { type: "trigger", - body: $jsonStringify({ - title: "{{ .Name }}", - description: "{{ .Description }}", - status: "{{ .Alert.Status }}", - deduplication_key: "{{ .Alert.InstanceID }}", - source_url: "{{ .Result.URL }}", - model: "{{ .Vars.model }}", - product: "{{ .Vars.product }}", - }), + body: `{ + "url": {{ .Result.URL | quote }}, + "type": {{ .Vars.type | quote }}, + "name": {{ .Name | quote }}, + "status": {{ .Alert.Status | quote }}, + "isTest": {{ .Alert.IsTest }}, + "groups": {{ .Result.GroupsTriggered | toJson }} + }`, }, ], variables: [ { - name: "model", - }, - { - name: "product", + name: "type", }, ], }) -new incident.AlertRoute("HoneycombAlertRoute", { - name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`, - enabled: true, - isPrivate: false, - alertSources: [ - { - alertSourceId: alertSource.id, - conditionGroups: [ - { - conditions: [ - { - subject: "alert.title", - operation: "is_set", - paramBindings: [], - }, - ], - }, - ], - }, - ], - conditionGroups: [ - { - conditions: [ - { - subject: "alert.title", - operation: "is_set", - paramBindings: [], - }, - ], - }, - ], - expressions: [], - escalationConfig: { - autoCancelEscalations: true, - escalationTargets: [], - }, - incidentConfig: { - autoDeclineEnabled: true, - enabled: true, - conditionGroups: [], - deferTimeSeconds: 0, - groupingKeys: [ - { - reference: $interpolate`alert.attributes.${fields.model.id}`, - }, +const modelHttpErrorsQuery = (product: "go" | "zen") => { + const filters = [ + { column: "model", op: "exists" }, + { column: "event_type", op: "=", value: "completions" }, + { column: "user_agent", op: "contains", value: "opencode" }, + { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, + ] + + return honeycomb.getQuerySpecificationOutput({ + breakdowns: ["model"], + calculatedFields: [ { - reference: $interpolate`alert.attributes.${fields.product.id}`, + name: "is_failed_http_status", + expression: `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`, }, ], - groupingWindowSeconds: 3600, - }, - incidentTemplate: { - name: { - value: { - literal: varSpec("Alert -> Title", "alert.title"), - }, - }, - summary: { - value: { - literal: varSpec("Alert -> Description", "alert.description"), - }, - }, - startInTriage: { - value: { - literal: "true", - }, - }, - severity: { - mergeStrategy: "first-wins", - }, - incidentMode: { - value: { - literal: $app.stage === "production" ? "standard" : "test", - }, - }, - }, -}) - -type Product = "go" | "zen" - -type Trigger = (opts: { model: string; product: Product }) => { - id: string - title: string - description: string - json: honeycomb.GetQuerySpecificationOutputArgs - threshold: { op: ">=" | "<="; value: number } -} - -type Model = { id: string; products: Product[]; triggers: Trigger[] } - -const httpErrors: Trigger = ({ model, product }) => ({ - id: "increased-http-errors", - title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`, - description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`, - json: { calculations: [ - { - op: "COUNT", - name: "TOTAL", - filterCombination: "AND", - filters: [ - { column: "model", op: "=", value: model }, - { column: "event_type", op: "=", value: "completions" }, - { column: "user_agent", op: "contains", value: "opencode" }, - { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, - ], - }, - { - op: "COUNT", - name: "FAILED", - filterCombination: "AND", - filters: [ - { column: "model", op: "=", value: model }, - { column: "event_type", op: "=", value: "completions" }, - { column: "user_agent", op: "contains", value: "opencode" }, - { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, - { column: "status", op: ">=", value: "400" }, - { column: "status", op: "!=", value: "401" }, - ], - }, + { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters }, + { op: "SUM", name: "FAILED", column: "is_failed_http_status", filterCombination: "AND", filters }, ], - formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }], + formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 2500), DIV($FAILED, $TOTAL), 0)" }], timeRange: 900, - }, - threshold: { op: ">=", value: 0.8 }, -}) - -const models: Model[] = [ - { id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] }, - // { id: "glm-5", products: ["go"], triggers: [httpErrors] }, - { id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] }, - { id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] }, - // { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] }, - { id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] }, - // { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] }, - // { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] }, - // { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] }, - { id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] }, - // { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] }, - // { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] }, - { id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] }, - { id: "big-pickle", products: ["zen"], triggers: [httpErrors] }, - // { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] }, - // { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] }, - // { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] }, - // { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] }, - // { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] }, -] - -if ($app.stage !== "production") { - models.splice(1) + }).json } -for (const model of models) { - for (const product of model.products) { - for (const trigger of model.triggers) { - const spec = trigger({ model: model.id, product }) +const description = "Managed by SST (Don't edit in Honeycomb UI)" - new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), { - name: spec.title, - description: spec.description, - queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json, - alertType: "on_change", - frequency: 300, - thresholds: [{ ...spec.threshold, exceededLimit: 1 }], - recipients: [ - { - id: webhookRecipient.id, - notificationDetails: [ - { - variables: [ - { name: "model", value: model.id }, - { name: "product", value: product }, - ], - }, - ], - }, - ], - }) - } - } -} +new honeycomb.Trigger("IncreasedModelHttpErrorsGo", { + name: "Increased Model HTTP Errors [Go]", + description, + queryJson: modelHttpErrorsQuery("go"), + alertType: "on_change", + frequency: 300, + thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], + recipients: [ + // { + // id: webhookRecipient.id, + // notificationDetails: [ + // { + // variables: [{ name: "type", value: "model_http_errors" }], + // }, + // ], + // }, + ], +}) + +new honeycomb.Trigger("IncreasedModelHttpErrorsZen", { + name: "Increased Model HTTP Errors [Zen]", + description, + queryJson: modelHttpErrorsQuery("zen"), + alertType: "on_change", + frequency: 300, + thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], + recipients: [ + // { + // id: webhookRecipient.id, + // notificationDetails: [ + // { + // variables: [{ name: "type", value: "model_http_errors" }], + // }, + // ], + // }, + ], +}) diff --git a/infra/secret.ts b/infra/secret.ts index 0b1870fa15..d4e8b148fc 100644 --- a/infra/secret.ts +++ b/infra/secret.ts @@ -1,4 +1,11 @@ +sst.Linkable.wrap(random.RandomPassword, (resource) => ({ + properties: { + value: resource.result, + }, +})) + export const SECRET = { R2AccessKey: new sst.Secret("R2AccessKey", "unknown"), R2SecretKey: new sst.Secret("R2SecretKey", "unknown"), + HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }), } diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 71d37d1553..298ae4a8cf 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -31,7 +31,6 @@ "solid-js": "catalog:", "solid-list": "0.3.0", "solid-stripe": "0.8.1", - "svix": "1.92.2", "vite": "catalog:", "zod": "catalog:" }, diff --git a/packages/console/app/src/routes/honeycomb/webhook.ts b/packages/console/app/src/routes/honeycomb/webhook.ts new file mode 100644 index 0000000000..b4d5e4bf7e --- /dev/null +++ b/packages/console/app/src/routes/honeycomb/webhook.ts @@ -0,0 +1,81 @@ +import type { APIEvent } from "@solidjs/start/server" +import { z } from "zod" +import { Resource } from "@opencode-ai/console-resource" +import { safeEqual } from "@opencode-ai/console-core/util/crypto.js" + +const DISCORD_ALERT_ROLE_ID = "1501447160175136838" + +const basePayload = z.object({ + name: z.string().optional(), + status: z.string().optional(), + isTest: z.boolean().optional(), + url: z.string(), +}) + +const groups = z.object({ group: z.object({ key: z.string(), value: z.string() }).array() }).array() + +const honeycombWebhookPayload = z.discriminatedUnion("type", [ + basePayload.extend({ + type: z.literal("model_http_errors"), + groups, + }), + basePayload.extend({ + type: z.literal("provider_http_errors"), + groups, + }), +]) + +const postDiscordMessage = async (payload: z.infer) => { + const group = payload.type === "model_http_errors" ? "model" : "provider" + const names = (payload.groups ?? []).flatMap((item) => item.group.map((g) => g.value)) + + const content = [ + `[**${payload.isTest ? "[TEST] " : ""}${payload.name ?? "Honeycomb alert"}**](${payload.url})`, + names.length > 0 ? `Affected ${group}s:` : undefined, + ...names.map((name) => `- ${name}`), + "", + `<@&${DISCORD_ALERT_ROLE_ID}>`, + ] + .filter((line) => line !== undefined) + .join("\n") + + return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + content, + allowed_mentions: { roles: [DISCORD_ALERT_ROLE_ID] }, + flags: 4, + }), + }) +} + +export async function POST(input: APIEvent) { + const token = input.request.headers.get("X-Honeycomb-Webhook-Token") + if (!safeEqual(token ?? "", Resource.HoneycombWebhookSecret.value)) { + console.debug("Invalid Honeycomb webhook token") + return Response.json({ message: "invalid token" }, { status: 401 }) + } + + const body = await input.request.json() + console.log(body, JSON.stringify(body, null, 2)) + + const parsed = honeycombWebhookPayload.safeParse(body) + + if (!parsed.success) { + console.error(parsed.error) + return Response.json({ message: "invalid payload" }, { status: 400 }) + } + + if (parsed.data.status !== "TRIGGERED") { + console.debug("Skipping resolved alert Honeycomb webhook") + return Response.json({ message: "ignored" }, { status: 200 }) + } + + const response = await postDiscordMessage(parsed.data) + if (!response.ok) { + return Response.json({ message: "discord webhook failed" }, { status: 502 }) + } + + return Response.json({ message: "sent" }, { status: 200 }) +} diff --git a/packages/console/app/src/routes/incident/webhook.ts b/packages/console/app/src/routes/incident/webhook.ts deleted file mode 100644 index ce7b0a0d9f..0000000000 --- a/packages/console/app/src/routes/incident/webhook.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { Resource } from "@opencode-ai/console-resource" -import { Webhook } from "svix" - -const DISCORD_INCIDENT_ROLE_ID = "1501447160175136838" - -type Incident = { - mode?: "test" | "standard" - name?: string - permalink?: string - summary?: string -} - -type IncidentWebhookPayload = { - event_type?: string - "public_incident.incident_created_v2"?: Incident -} - -const verifyWebhook = async (request: Request) => { - const body = await request.text() - try { - return new Webhook(Resource.INCIDENT_WEBHOOK_SIGNING_SECRET.value).verify( - body, - Object.fromEntries(request.headers.entries()), - ) as IncidentWebhookPayload - } catch { - return undefined - } -} - -const postDiscordMessage = async (incident: Incident) => { - return fetch(Resource.DISCORD_INCIDENT_WEBHOOK_URL.value, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - content: [ - `**${incident.mode === "test" ? "[TEST] " : ""}${incident.name ?? "Incident has been created"}**`, - incident.summary, - "", - `<@&${DISCORD_INCIDENT_ROLE_ID}>`, - "", - incident.permalink, - ] - .filter((line) => line !== undefined) - .join("\n"), - allowed_mentions: { - roles: [DISCORD_INCIDENT_ROLE_ID], - }, - flags: 4, - }), - }) -} - -export async function POST(input: APIEvent) { - const payload = await verifyWebhook(input.request) - if (!payload) { - return Response.json({ message: "invalid signature" }, { status: 401 }) - } - - if (payload.event_type !== "public_incident.incident_created_v2") { - return Response.json({ message: "ignored event" }, { status: 200 }) - } - - const incident = payload["public_incident.incident_created_v2"] - if (!incident) { - return Response.json({ message: "missing incident" }, { status: 400 }) - } - - const response = await postDiscordMessage(incident) - if (!response.ok) { - return Response.json({ message: "discord webhook failed" }, { status: 502 }) - } - - return Response.json({ message: "sent" }, { status: 200 }) -} diff --git a/packages/console/core/src/util/crypto.ts b/packages/console/core/src/util/crypto.ts new file mode 100644 index 0000000000..46f53ae391 --- /dev/null +++ b/packages/console/core/src/util/crypto.ts @@ -0,0 +1,8 @@ +import { timingSafeEqual } from "node:crypto" + +export function safeEqual(a: string, b: string): boolean { + const encoder = new TextEncoder() + const aBytes = encoder.encode(a) + const bBytes = encoder.encode(b) + return aBytes.length === bBytes.length && timingSafeEqual(aBytes, bBytes) +} diff --git a/packages/console/core/sst-env.d.ts b/packages/console/core/sst-env.d.ts index bc56bd789d..9680a53aab 100644 --- a/packages/console/core/sst-env.d.ts +++ b/packages/console/core/sst-env.d.ts @@ -91,8 +91,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "R2AccessKey": { diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index bc56bd789d..9680a53aab 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -91,8 +91,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "R2AccessKey": { diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index bc56bd789d..9680a53aab 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -91,8 +91,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "R2AccessKey": { diff --git a/packages/enterprise/sst-env.d.ts b/packages/enterprise/sst-env.d.ts index bc56bd789d..9680a53aab 100644 --- a/packages/enterprise/sst-env.d.ts +++ b/packages/enterprise/sst-env.d.ts @@ -91,8 +91,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "R2AccessKey": { diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index bc56bd789d..9680a53aab 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -91,8 +91,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "R2AccessKey": { diff --git a/sst-env.d.ts b/sst-env.d.ts index 52702acd7c..e75c54d056 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -114,8 +114,8 @@ declare module "sst" { "type": "sst.sst.Secret" "value": string } - "INCIDENT_WEBHOOK_SIGNING_SECRET": { - "type": "sst.sst.Secret" + "HoneycombWebhookSecret": { + "type": "random.index/randomPassword.RandomPassword" "value": string } "LogProcessor": { diff --git a/sst.config.ts b/sst.config.ts index a7e513ca0a..d82c7d18d9 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -11,15 +11,10 @@ export default $config({ stripe: { apiKey: process.env.STRIPE_SECRET_KEY!, }, + random: "4.19.2", planetscale: "0.4.1", - honeycomb: { - version: "0.49.0", - apiKey: process.env.HONEYCOMB_API_KEY!, - }, - incident: { - version: "5.35.0", - apiKey: process.env.INCIDENT_API_KEY!, - }, + honeycomb: "0.49.0", + incident: "5.35.0", }, } }, @@ -27,7 +22,7 @@ export default $config({ await import("./infra/app.js") await import("./infra/console.js") await import("./infra/enterprise.js") - if ($app.stage === "production") { + if ($app.stage === "production" || $app.stage === "vimtor") { await import("./infra/monitoring.js") } }, From 1ea01fdad07a717165e91e1f92eda2197f17e9ce Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 7 May 2026 08:08:37 +0000 Subject: [PATCH 54/70] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index a765e803d2..078b600d05 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-cgqwEUyOYcOnh07Wz20qkPIrDeBaCBmKiis6HO1EAIU=", - "aarch64-linux": "sha256-AVy0RQuuXiseIwJV9f9API8OEo1jcy84dVidkEXgnX8=", - "aarch64-darwin": "sha256-RIS3/SuSXaMV9WcTXxOJWGDw96LcCFT6E8Ktc28/544=", - "x86_64-darwin": "sha256-GBwXIZQxy5F7tH9TJyWAonX5aETbZ/veAjeznDtsYmk=" + "x86_64-linux": "sha256-MHeO1KTmjYa+V4ZBYrQq93cYpjnkGfO9e3MOWwkzjVY=", + "aarch64-linux": "sha256-EqTRG7DrdKKT7CEvnaNk5VhjTRhlZ9juP9/Nnr3dJ+g=", + "aarch64-darwin": "sha256-c8dWd8Pgp5uIAOdYbHIeGKqWfkF/l4Ze7ArYUMvTNkE=", + "x86_64-darwin": "sha256-61NpSO0AZ4iZG19RQ6zg0SJec+VQE46WJKOdRrNofT0=" } } From 0b2e65f16d35b0c21c4206c9249b6550919ec7e5 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 10:15:40 +0200 Subject: [PATCH 55/70] chore: reactivate alerts --- .github/workflows/deploy.yml | 1 - infra/monitoring.ts | 34 +++++++++++++++++----------------- sst.config.ts | 1 - 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 10b8dc180b..abd8bafdd6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,7 +37,6 @@ jobs: PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }} STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }} HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} - INCIDENT_API_KEY: ${{ secrets.INCIDENT_API_KEY }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 4e22e3d812..84add2f8e6 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -45,7 +45,7 @@ const modelHttpErrorsQuery = (product: "go" | "zen") => { { op: "COUNT", name: "TOTAL", filterCombination: "AND", filters }, { op: "SUM", name: "FAILED", column: "is_failed_http_status", filterCombination: "AND", filters }, ], - formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 2500), DIV($FAILED, $TOTAL), 0)" }], + formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 500), DIV($FAILED, $TOTAL), 0)" }], timeRange: 900, }).json } @@ -60,14 +60,14 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsGo", { frequency: 300, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ - // { - // id: webhookRecipient.id, - // notificationDetails: [ - // { - // variables: [{ name: "type", value: "model_http_errors" }], - // }, - // ], - // }, + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [{ name: "type", value: "model_http_errors" }], + }, + ], + }, ], }) @@ -79,13 +79,13 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsZen", { frequency: 300, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ - // { - // id: webhookRecipient.id, - // notificationDetails: [ - // { - // variables: [{ name: "type", value: "model_http_errors" }], - // }, - // ], - // }, + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [{ name: "type", value: "model_http_errors" }], + }, + ], + }, ], }) diff --git a/sst.config.ts b/sst.config.ts index d82c7d18d9..696a6fa768 100644 --- a/sst.config.ts +++ b/sst.config.ts @@ -14,7 +14,6 @@ export default $config({ random: "4.19.2", planetscale: "0.4.1", honeycomb: "0.49.0", - incident: "5.35.0", }, } }, From b2cc40f09c0f558ed698ae450abea6f3f8a9c233 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 10:30:29 +0200 Subject: [PATCH 56/70] chore: first provider alert version --- infra/monitoring.ts | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 84add2f8e6..1da54fe63d 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -50,6 +50,48 @@ const modelHttpErrorsQuery = (product: "go" | "zen") => { }).json } +const providerHttpErrorsQuery = (product: "go" | "zen") => { + const filters = [ + { column: "provider", op: "exists" }, + { column: "user_agent", op: "contains", value: "opencode" }, + { column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" }, + ] + + return honeycomb.getQuerySpecificationOutput({ + breakdowns: ["provider"], + calculatedFields: [ + { + name: "is_success_http_status", + expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`, + }, + { + name: "is_failed_provider_http_status", + expression: `IF(AND(GTE($llm.error.code, "400"), NOT(EQUALS($llm.error.code, "401"))), 1, 0)`, + }, + ], + calculations: [ + { + op: "SUM", + name: "SUCCESS", + column: "is_success_http_status", + filterCombination: "AND", + filters: [...filters, { column: "event_type", op: "=", value: "completions" }], + }, + { + op: "SUM", + name: "FAILED", + column: "is_failed_provider_http_status", + filterCombination: "AND", + filters: [...filters, { column: "event_type", op: "=", value: "llm.error" }], + }, + ], + formulas: [ + { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 500), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" }, + ], + timeRange: 900, + }).json +} + const description = "Managed by SST (Don't edit in Honeycomb UI)" new honeycomb.Trigger("IncreasedModelHttpErrorsGo", { @@ -89,3 +131,41 @@ new honeycomb.Trigger("IncreasedModelHttpErrorsZen", { }, ], }) + +new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", { + name: "Increased Provider HTTP Errors [Go]", + description, + queryJson: providerHttpErrorsQuery("go"), + alertType: "on_change", + frequency: 300, + thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], + recipients: [ + // { + // id: webhookRecipient.id, + // notificationDetails: [ + // { + // variables: [{ name: "type", value: "provider_http_errors" }], + // }, + // ], + // }, + ], +}) + +new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", { + name: "Increased Provider HTTP Errors [Zen]", + description, + queryJson: providerHttpErrorsQuery("zen"), + alertType: "on_change", + frequency: 300, + thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], + recipients: [ + // { + // id: webhookRecipient.id, + // notificationDetails: [ + // { + // variables: [{ name: "type", value: "provider_http_errors" }], + // }, + // ], + // }, + ], +}) From 1219691c114c9aec251bc855b3a4b53f7d12ff14 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Thu, 7 May 2026 16:31:37 +0800 Subject: [PATCH 57/70] docs(desktop): update README from Tauri to Electron (#26146) --- packages/desktop/README.md | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/desktop/README.md b/packages/desktop/README.md index ebaf488223..6dd9a202ad 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -1,32 +1,19 @@ # OpenCode Desktop -Native OpenCode desktop app, built with Tauri v2. +The OpenCode Desktop app, built with Electron. ## Development -From the repo root: - ```bash bun install -bun run --cwd packages/desktop tauri dev -``` - -This starts the Vite dev server on http://localhost:1420 and opens the native window. - -If you only want the web dev server (no native shell): - -```bash -bun run --cwd packages/desktop dev +bun dev ``` ## Build -To create a production `dist/` and build the native app bundle: +Run the `build` script to build the app's JS assets, then `package` to +bundle the assets as an application. The resulting app will be in `dist/`. ```bash -bun run --cwd packages/desktop tauri build +bun run build && bun run package ``` - -## Prerequisites - -Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions. From cee04f2924b16718bd7f60b05a1e946c17f8ea7e Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 10:56:37 +0200 Subject: [PATCH 58/70] chore: make provider down queries live --- infra/app.ts | 1 + infra/monitoring.ts | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/infra/app.ts b/infra/app.ts index bb627f51ec..2ede5a1f4a 100644 --- a/infra/app.ts +++ b/infra/app.ts @@ -30,6 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", { transform: { worker: (args) => { args.logpush = true + if ($app.stage === "vimtor") return args.bindings = $resolve(args.bindings).apply((bindings) => [ ...bindings, { diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 1da54fe63d..9956e2ed70 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -88,7 +88,7 @@ const providerHttpErrorsQuery = (product: "go" | "zen") => { formulas: [ { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 500), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" }, ], - timeRange: 900, + timeRange: 1800, }).json } @@ -140,14 +140,14 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", { frequency: 300, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ - // { - // id: webhookRecipient.id, - // notificationDetails: [ - // { - // variables: [{ name: "type", value: "provider_http_errors" }], - // }, - // ], - // }, + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [{ name: "type", value: "provider_http_errors" }], + }, + ], + }, ], }) @@ -159,13 +159,13 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", { frequency: 300, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ - // { - // id: webhookRecipient.id, - // notificationDetails: [ - // { - // variables: [{ name: "type", value: "provider_http_errors" }], - // }, - // ], - // }, + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [{ name: "type", value: "provider_http_errors" }], + }, + ], + }, ], }) From 193c169ca51103db79331d53bf1884262beffe7a Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 11:00:43 +0200 Subject: [PATCH 59/70] chore: improve provider down query --- infra/monitoring.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 9956e2ed70..b2716bcabb 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -66,7 +66,7 @@ const providerHttpErrorsQuery = (product: "go" | "zen") => { }, { name: "is_failed_provider_http_status", - expression: `IF(AND(GTE($llm.error.code, "400"), NOT(EQUALS($llm.error.code, "401"))), 1, 0)`, + expression: `IF(GTE($llm.error.code, "400"), 1, 0)`, }, ], calculations: [ @@ -86,7 +86,7 @@ const providerHttpErrorsQuery = (product: "go" | "zen") => { }, ], formulas: [ - { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 500), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" }, + { name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 250), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" }, ], timeRange: 1800, }).json From 30c4fcb1a596335057888bb76fa168f5039426c1 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 11:10:36 +0200 Subject: [PATCH 60/70] chore: fix honeycomb query frequency --- infra/monitoring.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index b2716bcabb..26ba573a07 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -137,7 +137,7 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", { description, queryJson: providerHttpErrorsQuery("go"), alertType: "on_change", - frequency: 300, + frequency: 600, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ { @@ -156,7 +156,7 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", { description, queryJson: providerHttpErrorsQuery("zen"), alertType: "on_change", - frequency: 300, + frequency: 600, thresholds: [{ op: ">=", value: 0.8, exceededLimit: 1 }], recipients: [ { From fea9a0bd4c1c9d9b66af84a43936a84626d723d8 Mon Sep 17 00:00:00 2001 From: YGoetschel <54545214+YGoetschel@users.noreply.github.com> Date: Thu, 7 May 2026 12:55:40 +0200 Subject: [PATCH 61/70] fix: guard undefined contents in diff renderer to fix share viewer SSR crash (#21763) --- packages/ui/src/components/file-ssr.tsx | 8 ++++++-- packages/ui/src/components/message-part.tsx | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/components/file-ssr.tsx b/packages/ui/src/components/file-ssr.tsx index ad05555bdf..6f11ca2433 100644 --- a/packages/ui/src/components/file-ssr.tsx +++ b/packages/ui/src/components/file-ssr.tsx @@ -128,8 +128,12 @@ function DiffSSRViewer(props: SSRDiffFileProps) { prerenderedHTML: local.preloadedDiff.prerenderedHTML, } : { - oldFile: local.before, - newFile: local.after, + oldFile: local.before + ? { ...local.before, contents: typeof local.before.contents === "string" ? local.before.contents : "" } + : local.before, + newFile: local.after + ? { ...local.after, contents: typeof local.after.contents === "string" ? local.after.contents : "" } + : local.after, lineAnnotations: annotations, fileContainer: fileDiffRef, containerWrapper: container, diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index cc046fdfc5..c36a52f81e 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -1906,11 +1906,11 @@ ToolRegistry.register({ mode="diff" before={{ name: props.metadata?.filediff?.file || props.input.filePath, - contents: props.metadata?.filediff?.before || props.input.oldString, + contents: props.metadata?.filediff?.before || props.input.oldString || "", }} after={{ name: props.metadata?.filediff?.file || props.input.filePath, - contents: props.metadata?.filediff?.after || props.input.newString, + contents: props.metadata?.filediff?.after || props.input.newString || "", }} />
    From 95280ebec9a8aa851f862fbdb4a48ec1243d93d9 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 7 May 2026 17:05:35 +0530 Subject: [PATCH 62/70] fix(tui): restore custom provider in /connect (#26168) --- .../cli/cmd/tui/component/dialog-provider.tsx | 131 +++++++++++++++--- .../test/cli/cmd/tui/provider-options.test.ts | 29 ++++ 2 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/test/cli/cmd/tui/provider-options.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index d6cbda4133..16812fa8ab 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -25,6 +25,60 @@ const PROVIDER_PRIORITY: Record = { google: 5, } +const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__" +const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ + +type ProviderOptionBase = { + title: string + value: string + description?: string + category: string +} + +type ProviderOption = + | (ProviderOptionBase & { + type: "provider" + providerID: string + }) + | (ProviderOptionBase & { + type: "custom" + }) + +export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] { + return [ + ...pipe( + list, + sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99), + map((provider) => ({ + type: "provider" as const, + title: provider.name, + value: provider.id, + providerID: provider.id, + description: { + opencode: "(Recommended)", + anthropic: "(API key)", + openai: "(ChatGPT Plus/Pro or API key)", + "opencode-go": "Low cost subscription for everyone", + }[provider.id], + category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers", + })), + ), + { + type: "custom", + title: "Other", + value: CUSTOM_PROVIDER_OPTION_VALUE, + description: "Custom provider", + category: "Providers", + }, + ] +} + +export function normalizeCustomProviderID(value: string) { + const providerID = value.trim().replace(/^@ai-sdk\//, "") + if (!CUSTOM_PROVIDER_ID.test(providerID)) return + return providerID +} + export function createDialogProviderOptions() { const sync = useSync() const dialog = useDialog() @@ -32,30 +86,61 @@ export function createDialogProviderOptions() { const toast = useToast() const { theme } = useTheme() const onboarded = useConnected() + + async function promptCustomProviderID(): Promise { + const value = await DialogPrompt.show(dialog, "Other", { + placeholder: "Provider id", + description: () => ( + + This only stores a credential. Configure the provider in opencode.json to use it. + + ), + }) + if (value === null) return + + const providerID = normalizeCustomProviderID(value) + if (providerID) return providerID + + toast.show({ + variant: "error", + message: "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", + }) + return promptCustomProviderID() + } + const options = createMemo(() => { return pipe( - sync.data.provider_next.all, - sortBy((x) => PROVIDER_PRIORITY[x.id] ?? 99), + providerOptions(sync.data.provider_next.all), map((provider) => { - const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, provider.id) - const connected = sync.data.provider_next.connected.includes(provider.id) + if (provider.type === "custom") { + return { + title: provider.title, + value: provider.value, + description: provider.description, + category: provider.category, + async onSelect() { + const providerID = await promptCustomProviderID() + if (!providerID) return + return dialog.replace(() => ) + }, + } + } + + const providerID = provider.providerID + const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID) + const connected = sync.data.provider_next.connected.includes(providerID) return { - title: provider.name, - value: provider.id, - description: { - opencode: "(Recommended)", - anthropic: "(API key)", - openai: "(ChatGPT Plus/Pro or API key)", - "opencode-go": "Low cost subscription for everyone", - }[provider.id], + title: provider.title, + value: provider.value, + description: provider.description, footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined, - category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Other", + category: provider.category, gutter: connected && onboarded() ? () => : undefined, async onSelect() { if (consoleManaged) return - const methods = sync.data.provider_auth[provider.id] ?? [ + const methods = sync.data.provider_auth[providerID] ?? [ { type: "api", label: "API key", @@ -93,7 +178,7 @@ export function createDialogProviderOptions() { } const result = await sdk.client.provider.oauth.authorize({ - providerID: provider.id, + providerID, method: index, inputs, }) @@ -108,7 +193,7 @@ export function createDialogProviderOptions() { if (result.data?.method === "code") { dialog.replace(() => ( ( ( - + )) } }, @@ -256,11 +341,13 @@ interface ApiMethodProps { providerID: string title: string metadata?: Record + custom?: boolean } function ApiMethod(props: ApiMethodProps) { const dialog = useDialog() const sdk = useSDK() const sync = useSync() + const toast = useToast() const { theme } = useTheme() return ( @@ -305,6 +392,14 @@ function ApiMethod(props: ApiMethodProps) { }) await sdk.client.instance.dispose() await sync.bootstrap() + if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) { + toast.show({ + variant: "info", + message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`, + }) + dialog.clear() + return + } dialog.replace(() => ) }} /> diff --git a/packages/opencode/test/cli/cmd/tui/provider-options.test.ts b/packages/opencode/test/cli/cmd/tui/provider-options.test.ts new file mode 100644 index 0000000000..39d6398379 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/provider-options.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { normalizeCustomProviderID, providerOptions } from "../../../../src/cli/cmd/tui/component/dialog-provider" + +describe("providerOptions", () => { + test("includes a synthetic Other option for custom providers", () => { + expect(providerOptions([{ id: "openai", name: "OpenAI" }]).at(-1)).toMatchObject({ + title: "Other", + description: "Custom provider", + category: "Providers", + }) + }) + + test("does not use Other as the generic provider category", () => { + expect(providerOptions([{ id: "mistral", name: "Mistral" }])[0]?.category).toBe("Providers") + }) + + test("does not collide with a configured provider named other", () => { + const values = providerOptions([{ id: "other", name: "Other Provider" }]).map((option) => option.value) + expect(new Set(values).size).toBe(values.length) + }) + + test("normalizes and validates custom provider ids", () => { + expect(normalizeCustomProviderID(" custom-provider ")).toBe("custom-provider") + expect(normalizeCustomProviderID("custom_provider")).toBe("custom_provider") + expect(normalizeCustomProviderID("@ai-sdk/custom-provider")).toBe("custom-provider") + expect(normalizeCustomProviderID("-custom-provider")).toBeUndefined() + expect(normalizeCustomProviderID("Custom Provider")).toBeUndefined() + }) +}) From fbb7b5b1bf031e16f4bbe6db34038ccf501a7f3e Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 13:32:32 +0200 Subject: [PATCH 63/70] chore: add free tier usage alert --- infra/monitoring.ts | 35 +++++++++++++++++++ .../app/src/routes/honeycomb/webhook.ts | 10 ++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 26ba573a07..908078ba19 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -169,3 +169,38 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", { }, ], }) + +new honeycomb.Trigger("IncreasedFreeTierRequests", { + disabled: true, + name: "Increased Free Tier Requests", + description, + queryJson: honeycomb.getQuerySpecificationOutput({ + calculations: [ + { + op: "COUNT", + name: "REQUESTS", + filterCombination: "AND", + filters: [ + { column: "event_type", op: "=", value: "completions" }, + { column: "user_agent", op: "contains", value: "opencode" }, + { column: "isFreeTier", op: "=", value: "true" }, + ], + }, + ], + timeRange: 14400, + }).json, + alertType: "on_change", + frequency: 3600, + thresholds: [{ op: ">=", value: 50, exceededLimit: 2 }], + baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }], + recipients: [ + { + id: webhookRecipient.id, + notificationDetails: [ + { + variables: [{ name: "type", value: "custom" }], + }, + ], + }, + ], +}) diff --git a/packages/console/app/src/routes/honeycomb/webhook.ts b/packages/console/app/src/routes/honeycomb/webhook.ts index b4d5e4bf7e..367a93aeb0 100644 --- a/packages/console/app/src/routes/honeycomb/webhook.ts +++ b/packages/console/app/src/routes/honeycomb/webhook.ts @@ -23,15 +23,19 @@ const honeycombWebhookPayload = z.discriminatedUnion("type", [ type: z.literal("provider_http_errors"), groups, }), + basePayload.extend({ + type: z.literal("custom"), + }), ]) const postDiscordMessage = async (payload: z.infer) => { - const group = payload.type === "model_http_errors" ? "model" : "provider" - const names = (payload.groups ?? []).flatMap((item) => item.group.map((g) => g.value)) + const group = + payload.type === "model_http_errors" ? "model" : payload.type === "provider_http_errors" ? "provider" : undefined + const names = payload.type === "custom" ? [] : payload.groups.flatMap((item) => item.group.map((g) => g.value)) const content = [ `[**${payload.isTest ? "[TEST] " : ""}${payload.name ?? "Honeycomb alert"}**](${payload.url})`, - names.length > 0 ? `Affected ${group}s:` : undefined, + group && names.length > 0 ? `Affected ${group}s:` : undefined, ...names.map((name) => `- ${name}`), "", `<@&${DISCORD_ALERT_ROLE_ID}>`, From 844fb719382decad09ba55d1f8e49d811ce550be Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 7 May 2026 11:37:34 +0000 Subject: [PATCH 64/70] chore: generate --- .../cli/cmd/tui/component/dialog-provider.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 16812fa8ab..e12492a2d0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -103,7 +103,8 @@ export function createDialogProviderOptions() { toast.show({ variant: "error", - message: "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", + message: + "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", }) return promptCustomProviderID() } @@ -192,22 +193,12 @@ export function createDialogProviderOptions() { } if (result.data?.method === "code") { dialog.replace(() => ( - + )) } if (result.data?.method === "auto") { dialog.replace(() => ( - + )) } } From d6e06c8950cc58a376c70b27667a1746fd282539 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 13:40:29 +0200 Subject: [PATCH 65/70] chore: fix free tier query --- infra/monitoring.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index 908078ba19..e976044707 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -178,7 +178,6 @@ new honeycomb.Trigger("IncreasedFreeTierRequests", { calculations: [ { op: "COUNT", - name: "REQUESTS", filterCombination: "AND", filters: [ { column: "event_type", op: "=", value: "completions" }, From 9c9bc09f526d46c095c9d82b6ae7761c64281d4c Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 13:58:41 +0200 Subject: [PATCH 66/70] chore: fix free tier query --- infra/monitoring.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index e976044707..baf1f5d68b 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -175,22 +175,17 @@ new honeycomb.Trigger("IncreasedFreeTierRequests", { name: "Increased Free Tier Requests", description, queryJson: honeycomb.getQuerySpecificationOutput({ - calculations: [ - { - op: "COUNT", - filterCombination: "AND", - filters: [ - { column: "event_type", op: "=", value: "completions" }, - { column: "user_agent", op: "contains", value: "opencode" }, - { column: "isFreeTier", op: "=", value: "true" }, - ], - }, + calculations: [{ op: "COUNT" }], + filters: [ + { column: "event_type", op: "=", value: "completions" }, + { column: "user_agent", op: "contains", value: "opencode" }, + { column: "isFreeTier", op: "=", value: "true" }, ], - timeRange: 14400, + timeRange: 3600, }).json, alertType: "on_change", - frequency: 3600, - thresholds: [{ op: ">=", value: 50, exceededLimit: 2 }], + frequency: 900, + thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }], baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }], recipients: [ { From b6ff1b18c739c307c8e55aa9ab64e5ef2040f919 Mon Sep 17 00:00:00 2001 From: vimtor Date: Thu, 7 May 2026 14:10:47 +0200 Subject: [PATCH 67/70] chore: activate free tier requests query --- infra/monitoring.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infra/monitoring.ts b/infra/monitoring.ts index baf1f5d68b..aad090aa80 100644 --- a/infra/monitoring.ts +++ b/infra/monitoring.ts @@ -171,7 +171,6 @@ new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", { }) new honeycomb.Trigger("IncreasedFreeTierRequests", { - disabled: true, name: "Increased Free Tier Requests", description, queryJson: honeycomb.getQuerySpecificationOutput({ @@ -185,7 +184,7 @@ new honeycomb.Trigger("IncreasedFreeTierRequests", { }).json, alertType: "on_change", frequency: 900, - thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }], + thresholds: [{ op: ">=", value: 60, exceededLimit: 1 }], baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }], recipients: [ { From 3c4b4d5faf226b22fbb277bd7699b81484d49684 Mon Sep 17 00:00:00 2001 From: James Long Date: Thu, 7 May 2026 10:24:17 -0400 Subject: [PATCH 68/70] feat(core): copy file changes when warping (#26190) --- .../cmd/tui/component/dialog-session-list.tsx | 8 +- .../tui/component/dialog-workspace-create.tsx | 39 ++++- .../dialog-workspace-file-changes.tsx | 138 ++++++++++++++++++ .../cli/cmd/tui/component/prompt/index.tsx | 12 +- .../opencode/src/control-plane/workspace.ts | 100 ++++++++++++- packages/opencode/src/git/index.ts | 17 ++- packages/opencode/src/project/vcs.ts | 85 ++++++++++- .../src/server/routes/control/workspace.ts | 33 ++++- .../instance/httpapi/groups/instance.ts | 48 +++++- .../instance/httpapi/groups/workspace.ts | 14 +- .../instance/httpapi/handlers/instance.ts | 27 ++++ .../instance/httpapi/handlers/workspace.ts | 25 +++- .../src/server/routes/instance/index.ts | 98 ++++++++++++- packages/opencode/src/util/locale.ts | 5 + .../cmd/tui/dialog-workspace-create.test.ts | 25 ++++ .../test/control-plane/workspace.test.ts | 58 +++++++- .../test/plugin/workspace-adapter.test.ts | 8 +- .../server/httpapi-instance-context.test.ts | 9 +- .../test/server/httpapi-session.test.ts | 10 +- .../server/httpapi-workspace-routing.test.ts | 9 +- .../test/server/httpapi-workspace.test.ts | 8 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 110 ++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 97 +++++++++++- 23 files changed, 955 insertions(+), 28 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-workspace-file-changes.tsx diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 09d952ef81..a521e07b1d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -12,7 +12,11 @@ import { Flag } from "@opencode-ai/core/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" -import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create" +import { + openWorkspaceSelect, + type WorkspaceSelection, + warpWorkspaceSession, +} from "./dialog-workspace-create" import { Spinner } from "./spinner" import { errorMessage } from "@/util/error" import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed" @@ -70,8 +74,10 @@ export function DialogSessionList() { sync, project, toast, + sourceWorkspaceID: session.workspaceID, workspaceID, sessionID: session.id, + copyChanges: false, done: list, }) } diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 157ca20582..31955dcf31 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -3,10 +3,13 @@ import { useDialog } from "@tui/ui/dialog" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" import { useSync } from "@tui/context/sync" import { useProject } from "@tui/context/project" +import { useRoute } from "@tui/context/route" import { createMemo, createSignal, onMount } from "solid-js" import { errorMessage } from "@/util/error" import { useSDK } from "../context/sdk" import { useToast } from "../ui/toast" +import { DialogAlert } from "../ui/dialog-alert" +import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes" type Adapter = { type: string @@ -38,6 +41,7 @@ export function recentConnectedWorkspaces( get: (workspaceID: string) => WorkspaceInfo | undefined status: (workspaceID: string) => string | undefined limit?: number + omitWorkspaceID?: string }) { const workspaces = input.sessions .toSorted((a, b) => b.time.updated - a.time.updated) @@ -45,6 +49,7 @@ export function recentConnectedWorkspaces( const workspace = session.workspaceID ? input.get(session.workspaceID) : undefined return workspace && input.status(workspace.id) === "connected" ? [workspace] : [] }) + .filter((workspace) => workspace.id !== input.omitWorkspaceID) .filter((workspace, index, list) => list.findIndex((item) => item.id === workspace.id) === index) const recent = workspaces.slice(0, input.limit ?? 3) @@ -93,17 +98,29 @@ export async function warpWorkspaceSession(input: { sync: ReturnType project: ReturnType toast: ReturnType + sourceWorkspaceID?: string workspaceID: string | null sessionID: string + copyChanges: boolean done?: () => void }): Promise { const result = await input.sdk.client.experimental.workspace .warp({ id: input.workspaceID, sessionID: input.sessionID, + copyChanges: input.copyChanges, }) .catch(() => undefined) if (!result?.data) { + if (result?.error?.name === "VcsApplyError") { + await DialogAlert.show( + input.dialog, + "Unable to Warp Session", + "Unable to apply file changes to this workspace. It has existing changes that conflict or is based off a different branch. Session has not been warped.", + ) + return false + } + input.toast.show({ message: `Failed to warp session: ${errorMessage(result?.error ?? "no response")}`, variant: "error", @@ -143,16 +160,29 @@ export async function warpWorkspaceSession(input: { return true } +export async function confirmWorkspaceFileChanges(input: { + dialog: ReturnType + sdk: ReturnType + sourceWorkspaceID?: string +}) { + const status = await input.sdk.client.vcs.status({ workspace: input.sourceWorkspaceID }).catch(() => undefined) + const fileChangeChoice = status?.data?.length ? await DialogWorkspaceFileChanges.show(input.dialog, status.data) : "no" + if (!fileChangeChoice) return + return fileChangeChoice === "yes" +} + export function DialogWorkspaceSelect(props: { adapters?: Adapter[] onSelect: (selection: WorkspaceSelection) => Promise | void }) { const dialog = useDialog() const project = useProject() + const route = useRoute() const sync = useSync() const sdk = useSDK() const toast = useToast() const [adapters, setAdapters] = createSignal(props.adapters) + const omittedWorkspaceID = createMemo(() => (route.data.type === "session" ? project.workspace.current() : undefined)) onMount(() => { dialog.setSize("medium") @@ -171,6 +201,7 @@ export function DialogWorkspaceSelect(props: { sessions: sync.data.session, get: project.workspace.get, status: project.workspace.status, + omitWorkspaceID: omittedWorkspaceID(), }) return [ ...list.map((adapter) => ({ @@ -231,19 +262,23 @@ export function DialogWorkspaceSelect(props: { return } - dialog.replace(() => ) + dialog.replace(() => ) }} /> ) } -function DialogExistingWorkspaceSelect(props: { onSelect: (selection: WorkspaceSelection) => Promise | void }) { +function DialogExistingWorkspaceSelect(props: { + omitWorkspaceID?: string + onSelect: (selection: WorkspaceSelection) => Promise | void +}) { const project = useProject() const options = createMemo[]>(() => project.workspace .list() .filter((workspace) => project.workspace.status(workspace.id) === "connected") + .filter((workspace) => workspace.id !== props.omitWorkspaceID) .map((workspace: Workspace) => ({ title: workspace.name, description: `(${workspace.type})`, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-file-changes.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-file-changes.tsx new file mode 100644 index 0000000000..b2cb20630c --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-file-changes.tsx @@ -0,0 +1,138 @@ +import { TextAttributes } from "@opentui/core" +import { useKeyboard } from "@opentui/solid" +import type { VcsFileStatus } from "@opencode-ai/sdk/v2" +import { createMemo, For } from "solid-js" +import { createStore } from "solid-js/store" +import { Locale } from "@/util/locale" +import { useTheme } from "../context/theme" +import { useTuiConfig } from "../context/tui-config" +import { useDialog, type DialogContext } from "../ui/dialog" +import { getScrollAcceleration } from "../util/scroll" + +const options = ["no", "yes"] as const + +export type WorkspaceFileChangesChoice = (typeof options)[number] + +function statusLabel(status: VcsFileStatus["status"]) { + if (status === "added") return "A" + if (status === "deleted") return "D" + return "M" +} + +function changeCountWidth(file: VcsFileStatus) { + // The "plus 2" is for spaces + return `${file.additions ? `+${file.additions}` : ""}${file.deletions ? ` -${file.deletions}` : ""}`.length + 2 +} + +export function DialogWorkspaceFileChanges(props: { + files: VcsFileStatus[] + onSelect: (choice: WorkspaceFileChangesChoice) => void +}) { + const dialog = useDialog() + const { theme } = useTheme() + const tuiConfig = useTuiConfig() + const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig)) + const [store, setStore] = createStore({ active: "yes" as WorkspaceFileChangesChoice }) + const height = createMemo(() => Math.min(props.files.length, 8)) + const fileNameWidth = createMemo(() => 48 - Math.max(Math.max(7, ...props.files.map(changeCountWidth)) - 7, 0)) + + function confirm() { + props.onSelect(store.active) + dialog.clear() + } + + useKeyboard((evt) => { + if (evt.name === "return") { + evt.preventDefault() + evt.stopPropagation() + confirm() + return + } + if (evt.name === "left") { + evt.preventDefault() + evt.stopPropagation() + const index = options.indexOf(store.active) + setStore("active", options[Math.max(index - 1, 0)]) + return + } + if (evt.name === "right") { + evt.preventDefault() + evt.stopPropagation() + const index = options.indexOf(store.active) + setStore("active", options[Math.min(index + 1, options.length - 1)]) + } + }) + + return ( + + + + File Changes Found + + dialog.clear()}> + esc + + + + + {(item) => ( + + + + {statusLabel(item.status)} + + + {Locale.truncateLeft(item.file, fileNameWidth())} + + + + + {" "} + {item.additions ? +{item.additions} : null} + {item.deletions ? -{item.deletions} : null} + + + + )} + + + + + Do you want to apply these changes after warping? + + + + + {(item) => ( + { + setStore("active", item) + props.onSelect(item) + dialog.clear() + }} + > + {item} + + )} + + + + ) +} + +DialogWorkspaceFileChanges.show = (dialog: DialogContext, files: VcsFileStatus[]) => { + return new Promise((resolve) => { + dialog.replace( + () => , + () => resolve(undefined), + ) + }) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 41e32539ee..73ef5477e9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -42,7 +42,12 @@ import { useKV } from "../../context/kv" import { createFadeIn } from "../../util/signal" import { useTextareaKeybindings } from "../textarea-keybindings" import { DialogSkill } from "../dialog-skill" -import { openWorkspaceSelect, warpWorkspaceSession, type WorkspaceSelection } from "../dialog-workspace-create" +import { + confirmWorkspaceFileChanges, + openWorkspaceSelect, + warpWorkspaceSession, + type WorkspaceSelection, +} from "../dialog-workspace-create" import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" import { Flag } from "@opencode-ai/core/flag/flag" @@ -230,6 +235,9 @@ export function Prompt(props: PromptProps) { if (selection.type === "new") void createWorkspace(selection) return } + const sourceWorkspaceID = project.workspace.current() + const copyChanges = await confirmWorkspaceFileChanges({ dialog, sdk, sourceWorkspaceID }) + if (copyChanges === undefined) return selectWorkspace(selection) dialog.clear() @@ -247,8 +255,10 @@ export function Prompt(props: PromptProps) { sync, project, toast, + sourceWorkspaceID, workspaceID: workspace.id, sessionID: props.sessionID, + copyChanges, }) if (warped) showWarpNotice(workspace.name) } diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 24ca0e61bf..f9bab469b7 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -18,7 +18,7 @@ import { ProjectID } from "@/project/schema" import { Slug } from "@opencode-ai/core/util/slug" import { WorkspaceTable } from "./workspace.sql" import { getAdapter } from "./adapters" -import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" +import { type Target, type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" import { WorkspaceID } from "./schema" import { Session } from "@/session/session" import { SessionPrompt } from "@/session/prompt" @@ -31,6 +31,9 @@ import { WorkspaceContext } from "./workspace-context" import { EffectBridge } from "@/effect/bridge" import { withStatics } from "@/util/schema" import { zod as effectZod, zodObject } from "@/util/effect-zod" +import { Vcs } from "@/project/vcs" +import { InstanceStore } from "@/project/instance-store" +import { InstanceBootstrap } from "@/project/bootstrap" export const Info = WorkspaceInfoSchema export type Info = WorkspaceInfo @@ -86,6 +89,7 @@ export type CreateInput = Schema.Schema.Type export const SessionWarpInput = Schema.Struct({ workspaceID: Schema.NullOr(WorkspaceID), sessionID: SessionID, + copyChanges: Schema.optional(Schema.Boolean), }).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) }))) export type SessionWarpInput = Schema.Schema.Type @@ -137,6 +141,7 @@ type SessionWarpError = | WorkspaceNotFoundError | SessionEventsNotFoundError | SessionWarpHttpError + | Vcs.PatchApplyError | HttpClientError.HttpClientError type WaitForSyncError = SyncTimeoutError | SyncAbortedError type SyncLoopError = SyncHttpError | HttpClientError.HttpClientError @@ -167,6 +172,7 @@ export const layer = Layer.effect( const prompt = yield* SessionPrompt.Service const http = yield* HttpClient.HttpClient const sync = yield* SyncEvent.Service + const vcs = yield* Vcs.Service const connections = new Map() const syncFibers = yield* FiberMap.make() @@ -255,6 +261,66 @@ export const layer = Layer.effect( ) }) + const runInWorkspace = (input: { + workspaceID?: WorkspaceID + local: () => Effect.Effect + remote: (input: { + workspace: Info + target: Extract + }) => HttpClientRequest.HttpClientRequest + fallback: A + response?: "json" | "text" + }) => + Effect.gen(function* () { + if (!input.workspaceID) return yield* input.local() + + const workspace = yield* get(input.workspaceID) + if (!workspace) return input.fallback + + const adapter = getAdapter(workspace.projectID, workspace.type) + const target = yield* EffectBridge.fromPromise(() => adapter.target(workspace)) + + if (target.type === "local") { + const store = yield* InstanceStore.Service + return yield* store.provide({ directory: target.directory }, input.local()) + } + + const response = yield* http.execute(input.remote({ workspace, target })).pipe( + Effect.catch((error) => + Effect.sync(() => { + log.warn("workspace target request failed", { + workspaceID: workspace.id, + error: errorData(error), + }) + }), + ), + ) + if (!response) return input.fallback + if (response.status < 200 || response.status >= 300) { + const body = yield* response.text.pipe(Effect.catch(() => Effect.succeed(""))) + log.warn("workspace target request failed", { + workspaceID: workspace.id, + status: response.status, + body, + }) + return input.fallback + } + + const body = input.response === "text" ? response.text : response.json + return yield* body.pipe( + Effect.map((result) => result as A), + Effect.catch((error) => + Effect.sync(() => { + log.warn("workspace target response decode failed", { + workspaceID: workspace.id, + error: errorData(error), + }) + return input.fallback + }), + ), + ) + }) + const syncHistory = Effect.fn("Workspace.syncHistory")(function* ( space: Info, url: URL | string, @@ -557,6 +623,36 @@ export const layer = Layer.effect( } } + const sourcePatch = + input.copyChanges && current?.workspaceID + ? yield* runInWorkspace({ + workspaceID: current?.workspaceID ?? undefined, + local: () => vcs.diffRaw(), + remote: ({ target }) => + HttpClientRequest.get(route(target.url, "/vcs/diff/raw"), { + headers: new Headers(target.headers), + }), + fallback: "", + response: "text", + }).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)))) + : "" + + if (sourcePatch) { + // Attempt to apply the file changes to the new workspace. + // We intentionally do first so if it fails we don't warp + // the session. + yield* runInWorkspace({ + workspaceID: input.workspaceID ?? undefined, + local: () => vcs.apply({ patch: sourcePatch }), + remote: ({ target }) => + HttpClientRequest.post(route(target.url, "/vcs/apply"), { + headers: new Headers(target.headers), + body: HttpBody.jsonUnsafe({ patch: sourcePatch }), + }), + fallback: { applied: false }, + }).pipe(Effect.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)))) + } + if (input.workspaceID === null) { yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { @@ -866,6 +962,8 @@ export const defaultLayer = layer.pipe( Layer.provide(Session.defaultLayer), Layer.provide(SyncEvent.defaultLayer), Layer.provide(SessionPrompt.defaultLayer), + Layer.provide(Project.defaultLayer), + Layer.provide(Vcs.defaultLayer), Layer.provide(FetchHttpClient.layer), ) diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index fff1d70b2a..349bbad466 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -68,6 +68,7 @@ export interface Options { readonly cwd: string readonly env?: Record readonly maxOutputBytes?: number + readonly stdin?: ChildProcess.CommandInput } export interface Interface { @@ -85,6 +86,7 @@ export interface Interface { readonly patchAll: (cwd: string, ref: string, options?: PatchOptions) => Effect.Effect readonly patchUntracked: (cwd: string, file: string, options?: PatchOptions) => Effect.Effect readonly statUntracked: (cwd: string, file: string) => Effect.Effect + readonly applyPatch: (cwd: string, patch: string) => Effect.Effect } const kind = (code: string): Kind => { @@ -101,6 +103,8 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const encoder = new TextEncoder() + const stdin = (text: string) => Stream.make(encoder.encode(text)) const run = Effect.fn("Git.run")( function* (args: string[], opts: Options) { @@ -108,7 +112,7 @@ export const layer = Layer.effect( cwd: opts.cwd, env: opts.env, extendEnv: true, - stdin: "ignore", + stdin: opts.stdin ?? "ignore", stdout: "pipe", stderr: "pipe", }) @@ -316,9 +320,13 @@ export const layer = Layer.effect( cwd, maxOutputBytes: 4096, }) + if (result.truncated) return - const parts = result.text().split("\t") + const text = result.text() + + const parts = text.split("\t") if (parts.length < 2) return + const additions = parts[0] === "-" ? 0 : Number.parseInt(parts[0] || "0", 10) const deletions = parts[1] === "-" ? 0 : Number.parseInt(parts[1] || "0", 10) return { @@ -328,6 +336,10 @@ export const layer = Layer.effect( } satisfies Stat }) + const applyPatch = Effect.fn("Git.applyPatch")(function* (cwd: string, patch: string) { + return yield* run(["apply", "-"], { cwd, stdin: stdin(patch) }) + }) + return Service.of({ run, branch, @@ -343,6 +355,7 @@ export const layer = Layer.effect( patchAll, patchUntracked, statUntracked, + applyPatch, }) }), ) diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 8b3bedbf5b..02173453db 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -6,7 +6,7 @@ import { InstanceState } from "@/effect/instance-state" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import * as Log from "@opencode-ai/core/util/log" -import { zod } from "@/util/effect-zod" +import { zod, zodObject } from "@/util/effect-zod" import { NonNegativeInt, withStatics } from "@/util/schema" const log = Log.create({ service: "vcs" }) @@ -239,11 +239,39 @@ export const FileDiff = Schema.Struct({ .pipe(withStatics((s) => ({ zod: zod(s) }))) export type FileDiff = Schema.Schema.Type +export const FileStatus = Schema.Struct({ + file: Schema.String, + additions: NonNegativeInt, + deletions: NonNegativeInt, + status: Schema.Literals(["added", "deleted", "modified"]), +}) + .annotate({ identifier: "VcsFileStatus" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type FileStatus = Schema.Schema.Type + +export const ApplyInput = Schema.Struct({ + patch: Schema.String, +}).pipe(withStatics((s) => ({ zod: zod(s), zodObject: zodObject(s) }))) +export type ApplyInput = Schema.Schema.Type + +export const ApplyResult = Schema.Struct({ + applied: Schema.Boolean, +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ApplyResult = Schema.Schema.Type + +export class PatchApplyError extends Schema.TaggedErrorClass()("VcsPatchApplyError", { + message: Schema.String, + reason: Schema.Literals(["non-git", "not-clean"]), +}) {} + export interface Interface { readonly init: () => Effect.Effect readonly branch: () => Effect.Effect readonly defaultBranch: () => Effect.Effect + readonly status: () => Effect.Effect readonly diff: (mode: Mode) => Effect.Effect + readonly diffRaw: () => Effect.Effect + readonly apply: (input: ApplyInput) => Effect.Effect } interface State { @@ -304,6 +332,31 @@ export const layer: Layer.Layer = Lay defaultBranch: Effect.fn("Vcs.defaultBranch")(function* () { return yield* InstanceState.use(state, (x) => x.root?.name) }), + status: Effect.fn("Vcs.status")(function* () { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") return [] + const ref = (yield* git.hasHead(ctx.directory)) ? "HEAD" : undefined + const [list, stats] = yield* Effect.all( + [git.status(ctx.directory), ref ? git.stats(ctx.directory, ref) : Effect.succeed([])], + { concurrency: 2 }, + ) + const map = nums(stats) + return yield* Effect.forEach( + list.toSorted((a, b) => a.file.localeCompare(b.file)), + (item) => + Effect.gen(function* () { + const stat = + map.get(item.file) ?? + (item.status === "added" ? yield* git.statUntracked(ctx.worktree, item.file) : undefined) + return { + file: item.file, + additions: stat?.additions ?? 0, + deletions: stat?.deletions ?? 0, + status: item.status, + } satisfies FileStatus + }), + ) + }), diff: Effect.fn("Vcs.diff")(function* (mode: Mode) { const value = yield* InstanceState.get(state) const ctx = yield* InstanceState.context @@ -318,6 +371,36 @@ export const layer: Layer.Layer = Lay if (!ref) return [] return yield* diffAgainstRef(git, ctx.directory, ref) }), + diffRaw: Effect.fn("Vcs.diffRaw")(function* () { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") return "" + const [hasHead, status] = yield* Effect.all([git.hasHead(ctx.directory), git.status(ctx.directory)], { + concurrency: 2, + }) + const tracked = hasHead ? (yield* git.patchAll(ctx.directory, "HEAD")).text : "" + const untracked = yield* Effect.forEach( + status.filter((item) => item.code === "??"), + (item) => git.patchUntracked(ctx.directory, item.file).pipe(Effect.map((patch) => patch.text)), + ) + return [tracked, ...untracked].filter(Boolean).join("\n") + }), + apply: Effect.fn("Vcs.apply")(function* (input: ApplyInput) { + const ctx = yield* InstanceState.context + if (ctx.project.vcs !== "git") { + return yield* new PatchApplyError({ + message: "Patch can't be applied because the project is not git-based", + reason: "non-git", + }) + } + const applied = yield* git.applyPatch(ctx.directory, input.patch) + if (applied.exitCode !== 0) { + return yield* new PatchApplyError({ + message: "Patch can't be applied", + reason: "not-clean", + }) + } + return { applied: true } + }), }) }), ) diff --git a/packages/opencode/src/server/routes/control/workspace.ts b/packages/opencode/src/server/routes/control/workspace.ts index 788aef3176..0c1bf252ed 100644 --- a/packages/opencode/src/server/routes/control/workspace.ts +++ b/packages/opencode/src/server/routes/control/workspace.ts @@ -8,6 +8,7 @@ import { AppRuntime } from "@/effect/app-runtime" import { WorkspaceAdapterEntry } from "@/control-plane/types" import { zodObject } from "@/util/effect-zod" import { Instance } from "@/project/instance" +import { Vcs } from "@/project/vcs" import { errors } from "../../error" import { lazy } from "@/util/lazy" @@ -164,19 +165,47 @@ export const WorkspaceRoutes = lazy(() => z.object({ id: zodObject(Workspace.Info).shape.id.nullable(), sessionID: Workspace.SessionWarpInput.zodObject.shape.sessionID, + copyChanges: z.boolean().optional(), }), ), async (c) => { const body = c.req.valid("json") - await AppRuntime.runPromise( + return AppRuntime.runPromise( Workspace.Service.use((workspace) => workspace.sessionWarp({ workspaceID: body.id, sessionID: body.sessionID, + copyChanges: body.copyChanges, + }), + ).pipe( + Effect.match({ + onFailure: (error) => { + if (error instanceof Vcs.PatchApplyError) { + return c.json( + { + name: "VcsApplyError", + data: { + message: error.message, + reason: error.reason, + }, + }, + 400, + ) + } + return c.json( + { + name: "WorkspaceWarpError", + data: { + message: error.message, + }, + }, + 400, + ) + }, + onSuccess: () => c.body(null, 204), }), ), ) - return c.body(null, 204) }, ), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts index 463ea1ae4c..f2b0504a05 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts @@ -5,7 +5,7 @@ import { LSP } from "@/lsp/lsp" import { Vcs } from "@/project/vcs" import { Skill } from "@/skill" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" @@ -23,11 +23,25 @@ export const VcsDiffQuery = Schema.Struct({ mode: Vcs.Mode, }) +export class ApiVcsApplyError extends Schema.ErrorClass("VcsApplyError")( + { + name: Schema.Literal("VcsApplyError"), + data: Schema.Struct({ + message: Schema.String, + reason: Schema.Literals(["non-git", "not-clean"]), + }), + }, + { httpApiStatus: 400 }, +) {} + export const InstancePaths = { dispose: "/instance/dispose", path: "/path", vcs: "/vcs", + vcsStatus: "/vcs/status", vcsDiff: "/vcs/diff", + vcsDiffRaw: "/vcs/diff/raw", + vcsApply: "/vcs/apply", command: "/command", agent: "/agent", skill: "/skill", @@ -68,6 +82,15 @@ export const InstanceApi = HttpApi.make("instance") "Retrieve version control system (VCS) information for the current project, such as git branch.", }), ), + HttpApiEndpoint.get("vcsStatus", InstancePaths.vcsStatus, { + success: described(Schema.Array(Vcs.FileStatus), "VCS status"), + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.status", + summary: "Get VCS status", + description: "Retrieve changed files in the current working tree without patches.", + }), + ), HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { query: VcsDiffQuery, success: described(Schema.Array(Vcs.FileDiff), "VCS diff"), @@ -78,6 +101,29 @@ export const InstanceApi = HttpApi.make("instance") description: "Retrieve the current git diff for the working tree or against the default branch.", }), ), + HttpApiEndpoint.get("vcsDiffRaw", InstancePaths.vcsDiffRaw, { + success: described( + Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/x-diff; charset=utf-8" })), + "Raw VCS diff", + ), + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.diff.raw", + summary: "Get raw VCS diff", + description: "Retrieve a raw patch for current uncommitted changes.", + }), + ), + HttpApiEndpoint.post("vcsApply", InstancePaths.vcsApply, { + payload: Vcs.ApplyInput, + success: described(Vcs.ApplyResult, "VCS patch applied"), + error: ApiVcsApplyError, + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.apply", + summary: "Apply VCS patch", + description: "Apply a raw patch to the current working tree.", + }), + ), HttpApiEndpoint.get("command", InstancePaths.command, { success: described(Schema.Array(Command.Info), "List of commands"), }).annotateMerge( diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index f197ab9765..66422c13b6 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -2,6 +2,7 @@ import { Workspace } from "@/control-plane/workspace" import { WorkspaceAdapterEntry } from "@/control-plane/types" import { Schema, Struct } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" +import { ApiVcsApplyError } from "./instance" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing" @@ -12,8 +13,19 @@ export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fie export const WarpPayload = Schema.Struct({ id: Schema.NullOr(Workspace.Info.fields.id), sessionID: Workspace.SessionWarpInput.fields.sessionID, + copyChanges: Workspace.SessionWarpInput.fields.copyChanges, }) +export class ApiWorkspaceWarpError extends Schema.ErrorClass("WorkspaceWarpError")( + { + name: Schema.Literal("WorkspaceWarpError"), + data: Schema.Struct({ + message: Schema.String, + }), + }, + { httpApiStatus: 400 }, +) {} + export const WorkspacePaths = { adapters: `${root}/adapter`, list: root, @@ -78,7 +90,7 @@ export const WorkspaceApi = HttpApi.make("workspace") HttpApiEndpoint.post("warp", WorkspacePaths.warp, { payload: WarpPayload, success: described(HttpApiSchema.NoContent, "Session warped"), - error: HttpApiError.BadRequest, + error: [ApiWorkspaceWarpError, ApiVcsApplyError], }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.warp", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts index c2a4503b48..50a7fecfa7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/instance.ts @@ -9,6 +9,7 @@ import { Skill } from "@/skill" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" +import { ApiVcsApplyError } from "../groups/instance" import { markInstanceForDisposal } from "../lifecycle" export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) => @@ -41,10 +42,33 @@ export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance" return { branch, default_branch } }) + const getVcsStatus = Effect.fn("InstanceHttpApi.vcsStatus")(function* () { + return yield* vcs.status() + }) + const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) { return yield* vcs.diff(ctx.query.mode) }) + const getVcsDiffRaw = Effect.fn("InstanceHttpApi.vcsDiffRaw")(function* () { + return yield* vcs.diffRaw() + }) + + const applyVcs = Effect.fn("InstanceHttpApi.vcsApply")(function* (ctx: { payload: Vcs.ApplyInput }) { + return yield* vcs.apply(ctx.payload).pipe( + Effect.mapError( + (error) => + new ApiVcsApplyError({ + name: "VcsApplyError", + data: { + message: error.message, + reason: error.reason, + }, + }), + ), + ) + }) + const getCommand = Effect.fn("InstanceHttpApi.command")(function* () { return yield* command.list() }) @@ -69,7 +93,10 @@ export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance" .handle("dispose", dispose) .handle("path", getPath) .handle("vcs", getVcs) + .handle("vcsStatus", getVcsStatus) .handle("vcsDiff", getVcsDiff) + .handle("vcsDiffRaw", getVcsDiffRaw) + .handle("vcsApply", applyVcs) .handle("command", getCommand) .handle("agent", getAgent) .handle("skill", getSkill) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts index b415943a62..d908eda9d1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts @@ -1,10 +1,12 @@ import { listAdapters } from "@/control-plane/adapters" import { Workspace } from "@/control-plane/workspace" import * as InstanceState from "@/effect/instance-state" +import { Vcs } from "@/project/vcs" import { Effect } from "effect" import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { CreatePayload, WarpPayload } from "../groups/workspace" +import { ApiVcsApplyError } from "../groups/instance" +import { ApiWorkspaceWarpError, CreatePayload, WarpPayload } from "../groups/workspace" export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) => Effect.gen(function* () { @@ -44,8 +46,27 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac .sessionWarp({ workspaceID: ctx.payload.id, sessionID: ctx.payload.sessionID, + copyChanges: ctx.payload.copyChanges, }) - .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + .pipe( + Effect.mapError((error) => { + if (error instanceof Vcs.PatchApplyError) { + return new ApiVcsApplyError({ + name: "VcsApplyError", + data: { + message: error.message, + reason: error.reason, + }, + }) + } + return new ApiWorkspaceWarpError({ + name: "WorkspaceWarpError", + data: { + message: error.message, + }, + }) + }), + ) }) return handlers diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 71662dea90..b6bf8baa74 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -27,7 +27,7 @@ import { ProviderRoutes } from "./provider" import { EventRoutes } from "./event" import { SyncRoutes } from "./sync" import { InstanceMiddleware } from "./middleware" -import { jsonRequest } from "./trace" +import { jsonRequest, runRequest } from "./trace" import { ExperimentalHttpApiServer } from "./httpapi/server" import { EventPaths } from "./httpapi/event" import { ExperimentalPaths } from "./httpapi/groups/experimental" @@ -40,6 +40,7 @@ import { SyncPaths } from "./httpapi/groups/sync" import { TuiPaths } from "./httpapi/groups/tui" import { WorkspacePaths } from "./httpapi/groups/workspace" import type { CorsOptions } from "@/server/cors" +import { errors } from "@/server/error" export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): Hono => { const app = new Hono() @@ -86,7 +87,10 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): H app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcsStatus, (c) => handler(c.req.raw, context)) app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcsDiffRaw, (c) => handler(c.req.raw, context)) + app.post(InstancePaths.vcsApply, (c) => handler(c.req.raw, context)) app.get(InstancePaths.command, (c) => handler(c.req.raw, context)) app.get(InstancePaths.agent, (c) => handler(c.req.raw, context)) app.get(InstancePaths.skill, (c) => handler(c.req.raw, context)) @@ -288,6 +292,98 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): H return yield* vcs.diff(c.req.valid("query").mode) }), ) + .get( + "/vcs/status", + describeRoute({ + summary: "Get VCS status", + description: "Retrieve changed files in the current working tree without patches.", + operationId: "vcs.status", + responses: { + 200: { + description: "VCS status", + content: { + "application/json": { + schema: resolver(Vcs.FileStatus.zod.array()), + }, + }, + }, + }, + }), + async (c) => + jsonRequest("InstanceRoutes.vcs.status", c, function* () { + const vcs = yield* Vcs.Service + return yield* vcs.status() + }), + ) + .get( + "/vcs/diff/raw", + describeRoute({ + summary: "Get raw VCS diff", + description: "Retrieve a raw patch for current uncommitted changes.", + operationId: "vcs.diff.raw", + responses: { + 200: { + description: "Raw VCS diff", + content: { + "text/x-diff": { + schema: resolver(z.string()), + }, + }, + }, + }, + }), + async (c) => { + const patch = await runRequest( + "InstanceRoutes.vcs.diffRaw", + c, + Vcs.Service.use((vcs) => vcs.diffRaw()), + ) + return c.text(patch, 200, { "content-type": "text/x-diff; charset=utf-8" }) + }, + ) + .post( + "/vcs/apply", + describeRoute({ + summary: "Apply VCS patch", + description: "Apply a raw patch to the current working tree.", + operationId: "vcs.apply", + responses: { + 200: { + description: "VCS patch applied", + content: { + "application/json": { + schema: resolver(Vcs.ApplyResult.zod), + }, + }, + }, + ...errors(400), + }, + }), + validator("json", Vcs.ApplyInput.zodObject), + async (c) => { + const result = await runRequest( + "InstanceRoutes.vcs.apply", + c, + Vcs.Service.use((vcs) => vcs.apply(c.req.valid("json") as Vcs.ApplyInput)).pipe( + Effect.match({ + onFailure: (error) => ({ ok: false as const, error }), + onSuccess: (value) => ({ ok: true as const, value }), + }), + ), + ) + if (result.ok) return c.json(result.value) + return c.json( + { + name: "VcsApplyError", + data: { + message: result.error.message, + reason: result.error.reason, + }, + }, + 400, + ) + }, + ) .get( "/command", describeRoute({ diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 49f60e9311..ec900b4416 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -63,6 +63,11 @@ export function truncate(str: string, len: number): string { return str.slice(0, len - 1) + "…" } +export function truncateLeft(str: string, len: number): string { + if (str.length <= len) return str + return "…" + str.slice(-(len - 1)) +} + export function truncateMiddle(str: string, maxLength: number = 35): string { if (str.length <= maxLength) return str diff --git a/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts index 7d051923f6..a32dc61125 100644 --- a/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts +++ b/packages/opencode/test/cli/cmd/tui/dialog-workspace-create.test.ts @@ -35,4 +35,29 @@ describe("recentConnectedWorkspaces", () => { expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_a", "wrk_d", "wrk_e"]) }) + + test("omits the active workspace before limiting recent workspaces", () => { + const workspaces = [ + { id: "wrk_a", name: "alpha" }, + { id: "wrk_b", name: "beta" }, + { id: "wrk_c", name: "gamma" }, + { id: "wrk_d", name: "delta" }, + ] + + const { recent, hasMore } = recentConnectedWorkspaces({ + sessions: [ + { workspaceID: "wrk_a", time: { updated: 400 } }, + { workspaceID: "wrk_b", time: { updated: 300 } }, + { workspaceID: "wrk_c", time: { updated: 200 } }, + { workspaceID: "wrk_d", time: { updated: 100 } }, + ], + get: (workspaceID) => workspaces.find((workspace) => workspace.id === workspaceID), + status: () => "connected", + limit: 3, + omitWorkspaceID: "wrk_a", + }) + + expect(recent.map((workspace) => workspace.id)).toEqual(["wrk_b", "wrk_c", "wrk_d"]) + expect(hasMore).toBe(false) + }) }) diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 769e78fe9a..0eba431e1a 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { $ } from "bun" import fs from "node:fs/promises" import Http from "node:http" import path from "node:path" @@ -29,12 +30,17 @@ import { WorkspaceTable } from "../../src/control-plane/workspace.sql" import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types" import * as WorkspaceOld from "../../src/control-plane/workspace" import { AppRuntime } from "@/effect/app-runtime" +import { InstanceStore } from "@/project/instance-store" +import { InstanceBootstrap } from "@/project/bootstrap" void Log.init({ print: false }) const testServerLayer = Layer.mergeAll( NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }), - WorkspaceOld.defaultLayer, + WorkspaceOld.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), + ), SessionNs.defaultLayer, ) const it = testEffect(testServerLayer) @@ -107,6 +113,18 @@ async function withInstance(fn: (dir: string) => T | Promise) { }) } +async function initGitRepo(dir: string) { + await fs.mkdir(dir, { recursive: true }) + 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() + await fs.writeFile(path.join(dir, "tracked.txt"), "base\n") + await $`git add tracked.txt`.cwd(dir).quiet() + await $`git commit -m "base"`.cwd(dir).quiet() +} + const runWorkspace = (effect: Effect.Effect) => AppRuntime.runPromise(effect) const createWorkspace = (input: WorkspaceOld.CreateInput) => runWorkspace(WorkspaceOld.Service.use((workspace) => workspace.create(input))) @@ -644,6 +662,33 @@ describe("workspace-old CRUD", () => { }) }) + test("sessionWarp applies source workspace patch to local target workspace", async () => { + await withInstance(async (dir) => { + const previousType = unique("warp-patch-prev-local") + const targetType = unique("warp-patch-target-local") + const previousDir = path.join(dir, "warp-patch-prev-local") + const targetDir = path.join(dir, "warp-patch-target-local") + await initGitRepo(previousDir) + await initGitRepo(targetDir) + await fs.writeFile(path.join(previousDir, "tracked.txt"), "changed\n") + await fs.writeFile(path.join(previousDir, "new.txt"), "new\n") + + const previous = workspaceInfo(Instance.project.id, previousType) + const target = workspaceInfo(Instance.project.id, targetType) + insertWorkspace(previous) + insertWorkspace(target) + registerAdapter(Instance.project.id, previousType, localAdapter(previousDir, { createDir: false }).adapter) + registerAdapter(Instance.project.id, targetType, localAdapter(targetDir, { createDir: false }).adapter) + const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) + attachSessionToWorkspace(session.id, previous.id) + + await warpWorkspaceSession({ workspaceID: target.id, sessionID: session.id, copyChanges: true }) + + expect(await fs.readFile(path.join(targetDir, "tracked.txt"), "utf8")).toBe("changed\n") + expect(await fs.readFile(path.join(targetDir, "new.txt"), "utf8")).toBe("new\n") + }) + }) + test("sessionWarp detaches a session to the local project and claims project ownership", async () => { await withInstance(async (dir) => { const previousType = unique("warp-detach-local") @@ -696,10 +741,12 @@ describe("workspace-old CRUD", () => { }, ]) } + if (call.url.pathname === "/warp-source/vcs/diff/raw") return HttpServerResponse.text("remote patch") if (call.url.pathname === "/warp-target/sync/replay") return yield* HttpServerResponse.json({ sessionID: "ok" }) if (call.url.pathname === "/warp-target/sync/steal") return yield* HttpServerResponse.json({ sessionID: "ok" }) + if (call.url.pathname === "/warp-target/vcs/apply") return yield* HttpServerResponse.json({ applied: true }) return HttpServerResponse.text("unexpected", { status: 500 }) }), ) @@ -722,15 +769,18 @@ describe("workspace-old CRUD", () => { historySessionID = session.id historyNextSeq = (sessionSequence(session.id) ?? -1) + 1 - yield* workspace.sessionWarp({ workspaceID: target.id, sessionID: session.id }) + yield* workspace.sessionWarp({ workspaceID: target.id, sessionID: session.id, copyChanges: true }) expect(calls.map((call) => `${call.method} ${call.url.pathname}`)).toEqual([ "POST /warp-source/sync/history", + "GET /warp-source/vcs/diff/raw", + "POST /warp-target/vcs/apply", "POST /warp-target/sync/replay", "POST /warp-target/sync/steal", ]) expect(calls[0].json).toEqual({ [session.id]: historyNextSeq - 1 }) - expect(calls[1].json).toMatchObject({ + expect(calls[2].json).toEqual({ patch: "remote patch" }) + expect(calls[3].json).toMatchObject({ directory: "remote-target-dir", events: [ { @@ -745,7 +795,7 @@ describe("workspace-old CRUD", () => { }, ], }) - expect(calls[2].json).toEqual({ sessionID: session.id }) + expect(calls[4].json).toEqual({ sessionID: session.id }) expect((yield* sessionSvc.get(session.id)).title).toBe("from source history") expect(sessionSequenceOwner(session.id)).toBe(target.id) }), diff --git a/packages/opencode/test/plugin/workspace-adapter.test.ts b/packages/opencode/test/plugin/workspace-adapter.test.ts index 249087808d..9199a85a61 100644 --- a/packages/opencode/test/plugin/workspace-adapter.test.ts +++ b/packages/opencode/test/plugin/workspace-adapter.test.ts @@ -12,8 +12,14 @@ process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" const { Flag } = await import("@opencode-ai/core/flag/flag") const { Plugin } = await import("../../src/plugin/index") const { Workspace } = await import("../../src/control-plane/workspace") +const { InstanceBootstrap } = await import("../../src/project/bootstrap") const { Instance } = await import("../../src/project/instance") -const it = testEffect(Layer.mergeAll(Plugin.defaultLayer, Workspace.defaultLayer, CrossSpawnSpawner.defaultLayer)) +const { InstanceStore } = await import("../../src/project/instance-store") +const workspaceLayer = Workspace.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), +) +const it = testEffect(Layer.mergeAll(Plugin.defaultLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer)) const experimental = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index 410dbe7426..5e00d77708 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -10,8 +10,10 @@ import { registerAdapter } from "../../src/control-plane/adapters" import type { WorkspaceAdapter } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref" +import { InstanceBootstrap } from "../../src/project/bootstrap" import { Instance } from "../../src/project/instance" import { InstanceLayer } from "../../src/project/instance-layer" +import { InstanceStore } from "../../src/project/instance-store" import { Project } from "../../src/project/project" import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle" import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context" @@ -36,6 +38,11 @@ const testStateLayer = Layer.effectDiscard( }), ) +const workspaceLayer = Workspace.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), +) + const it = testEffect( Layer.mergeAll( testStateLayer, @@ -43,7 +50,7 @@ const it = testEffect( NodeServices.layer, InstanceLayer.layer, Project.defaultLayer, - Workspace.defaultLayer, + workspaceLayer, ), ) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index c45aacce75..c1d82446b9 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect } from "bun:test" import { mkdir } from "node:fs/promises" import path from "node:path" -import { Effect } from "effect" +import { Effect, Layer } from "effect" import { Flag } from "@opencode-ai/core/flag/flag" import { registerAdapter } from "../../src/control-plane/adapters" import type { WorkspaceAdapter } from "../../src/control-plane/types" @@ -9,6 +9,8 @@ import { Workspace } from "../../src/control-plane/workspace" import { PermissionID } from "../../src/permission/schema" import { ModelID, ProviderID } from "../../src/provider/schema" import { WithInstance } from "../../src/project/with-instance" +import { InstanceBootstrap } from "../../src/project/bootstrap" +import { InstanceStore } from "../../src/project/instance-store" import { Project } from "../../src/project/project" import { Server } from "../../src/server/server" import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session" @@ -30,6 +32,10 @@ void Log.init({ print: false }) const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES +const workspaceLayer = Workspace.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), +) function app(experimental = true) { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental @@ -106,7 +112,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri extra: null, projectID: input.projectID, }), - ).pipe(Effect.provide(Workspace.defaultLayer)) + ).pipe(Effect.provide(workspaceLayer)) }) function request(path: string, init?: RequestInit) { diff --git a/packages/opencode/test/server/httpapi-workspace-routing.test.ts b/packages/opencode/test/server/httpapi-workspace-routing.test.ts index b0b276841d..379b71a91e 100644 --- a/packages/opencode/test/server/httpapi-workspace-routing.test.ts +++ b/packages/opencode/test/server/httpapi-workspace-routing.test.ts @@ -20,6 +20,8 @@ import { WorkspaceID } from "../../src/control-plane/schema" import type { WorkspaceAdapter } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { WorkspaceTable } from "../../src/control-plane/workspace.sql" +import { InstanceBootstrap } from "../../src/project/bootstrap" +import { InstanceStore } from "../../src/project/instance-store" import { Project } from "../../src/project/project" import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace" import { @@ -45,13 +47,18 @@ const testStateLayer = Layer.effectDiscard( }), ) +const workspaceLayer = Workspace.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), +) + const it = testEffect( Layer.mergeAll( testStateLayer, NodeHttpServer.layerTest, NodeServices.layer, Project.defaultLayer, - Workspace.defaultLayer, + workspaceLayer, Socket.layerWebSocketConstructorGlobal, ), ) diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index 21bf4120c9..9b38cb44a2 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -14,6 +14,8 @@ import { Server } from "../../src/server/server" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture" import { Instance } from "../../src/project/instance" +import { InstanceBootstrap } from "../../src/project/bootstrap" +import { InstanceStore } from "../../src/project/instance-store" import { Project } from "../../src/project/project" import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance" import { WorkspaceRef } from "../../src/effect/instance-ref" @@ -23,9 +25,11 @@ void Log.init({ print: false }) const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI -const it = testEffect( - Layer.mergeAll(NodeServices.layer, Project.defaultLayer, Session.defaultLayer, Workspace.defaultLayer), +const workspaceLayer = Workspace.defaultLayer.pipe( + Layer.provide(InstanceStore.defaultLayer), + Layer.provide(InstanceBootstrap.defaultLayer), ) +const it = testEffect(Layer.mergeAll(NodeServices.layer, Project.defaultLayer, Session.defaultLayer, workspaceLayer)) function request(path: string, directory: string, init: RequestInit = {}, httpApi = true) { return Effect.promise(() => { diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 803d9ed16e..ebedb1dd6b 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -202,8 +202,12 @@ import type { V2SessionMessagesResponses, V2SessionPromptResponses, V2SessionWaitResponses, + VcsApplyErrors, + VcsApplyResponses, + VcsDiffRawResponses, VcsDiffResponses, VcsGetResponses, + VcsStatusResponses, WorktreeCreateErrors, WorktreeCreateInput, WorktreeCreateResponses, @@ -1022,6 +1026,7 @@ export class Workspace extends HeyApiClient { workspace?: string id?: string | null sessionID?: string + copyChanges?: boolean }, options?: Options, ) { @@ -1034,6 +1039,7 @@ export class Workspace extends HeyApiClient { { in: "query", key: "workspace" }, { in: "body", key: "id" }, { in: "body", key: "sessionID" }, + { in: "body", key: "copyChanges" }, ], }, ], @@ -1555,6 +1561,38 @@ export class Path extends HeyApiClient { } } +export class Diff extends HeyApiClient { + /** + * Get raw VCS diff + * + * Retrieve a raw patch for current uncommitted changes. + */ + public raw( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs/diff/raw", + ...options, + ...params, + }) + } +} + export class Vcs extends HeyApiClient { /** * Get VCS info @@ -1586,6 +1624,36 @@ export class Vcs extends HeyApiClient { }) } + /** + * Get VCS status + * + * Retrieve changed files in the current working tree without patches. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs/status", + ...options, + ...params, + }) + } + /** * Get VCS diff * @@ -1617,6 +1685,48 @@ export class Vcs extends HeyApiClient { ...params, }) } + + /** + * Apply VCS patch + * + * Apply a raw patch to the current working tree. + */ + public apply( + parameters?: { + directory?: string + workspace?: string + patch?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "patch" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/vcs/apply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _diff?: Diff + get diff2(): Diff { + return (this._diff ??= new Diff({ client: this.client })) + } } export class Command extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b58f6cfc2b..175fe69e66 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1474,6 +1474,13 @@ export type VcsInfo = { default_branch?: string } +export type VcsFileStatus = { + file: string + additions: number + deletions: number + status: "added" | "deleted" | "modified" +} + export type VcsFileDiff = { file: string patch: string @@ -1482,6 +1489,14 @@ export type VcsFileDiff = { status?: "added" | "deleted" | "modified" } +export type VcsApplyError = { + name: "VcsApplyError" + data: { + message: string + reason: "non-git" | "not-clean" + } +} + export type Command = { name: string description?: string @@ -1736,6 +1751,13 @@ export type Workspace = { projectID: string } +export type WorkspaceWarpError = { + name: "WorkspaceWarpError" + data: { + message: string + } +} + export type SyncEventMessageUpdated = { type: "sync" name: "message.updated.1" @@ -4020,6 +4042,25 @@ export type VcsGetResponses = { export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] +export type VcsStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/status" +} + +export type VcsStatusResponses = { + /** + * VCS status + */ + 200: Array +} + +export type VcsStatusResponse = VcsStatusResponses[keyof VcsStatusResponses] + export type VcsDiffData = { body?: never path?: never @@ -4040,6 +4081,57 @@ export type VcsDiffResponses = { export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] +export type VcsDiffRawData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/diff/raw" +} + +export type VcsDiffRawResponses = { + /** + * Raw VCS diff + */ + 200: string +} + +export type VcsDiffRawResponse = VcsDiffRawResponses[keyof VcsDiffRawResponses] + +export type VcsApplyData = { + body?: { + patch: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/apply" +} + +export type VcsApplyErrors = { + /** + * VcsApplyError + */ + 400: VcsApplyError +} + +export type VcsApplyError2 = VcsApplyErrors[keyof VcsApplyErrors] + +export type VcsApplyResponses = { + /** + * VCS patch applied + */ + 200: { + applied: boolean + } +} + +export type VcsApplyResponse = VcsApplyResponses[keyof VcsApplyResponses] + export type CommandListData = { body?: never path?: never @@ -6667,6 +6759,7 @@ export type ExperimentalWorkspaceWarpData = { body?: { id: string | null sessionID: string + copyChanges?: boolean } path?: never query?: { @@ -6678,9 +6771,9 @@ export type ExperimentalWorkspaceWarpData = { export type ExperimentalWorkspaceWarpErrors = { /** - * Bad request + * WorkspaceWarpError | VcsApplyError */ - 400: BadRequestError + 400: WorkspaceWarpError | VcsApplyError } export type ExperimentalWorkspaceWarpError = ExperimentalWorkspaceWarpErrors[keyof ExperimentalWorkspaceWarpErrors] From 98e091796b6a293cf20b4187e8fc6a949e0295e4 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 7 May 2026 14:25:30 +0000 Subject: [PATCH 69/70] chore: generate --- .../cmd/tui/component/dialog-session-list.tsx | 6 +- .../tui/component/dialog-workspace-create.tsx | 8 +- packages/sdk/openapi.json | 252 +++++++++++++++++- 3 files changed, 257 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index a521e07b1d..e8dbaee394 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -12,11 +12,7 @@ import { Flag } from "@opencode-ai/core/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" import { createDebouncedSignal } from "../util/signal" import { useToast } from "../ui/toast" -import { - openWorkspaceSelect, - type WorkspaceSelection, - warpWorkspaceSession, -} from "./dialog-workspace-create" +import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create" import { Spinner } from "./spinner" import { errorMessage } from "@/util/error" import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index 31955dcf31..d7e212ab15 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -166,7 +166,9 @@ export async function confirmWorkspaceFileChanges(input: { sourceWorkspaceID?: string }) { const status = await input.sdk.client.vcs.status({ workspace: input.sourceWorkspaceID }).catch(() => undefined) - const fileChangeChoice = status?.data?.length ? await DialogWorkspaceFileChanges.show(input.dialog, status.data) : "no" + const fileChangeChoice = status?.data?.length + ? await DialogWorkspaceFileChanges.show(input.dialog, status.data) + : "no" if (!fileChangeChoice) return return fileChangeChoice === "yes" } @@ -262,7 +264,9 @@ export function DialogWorkspaceSelect(props: { return } - dialog.replace(() => ) + dialog.replace(() => ( + + )) }} /> ) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 477145f017..04c34e2dc1 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1897,6 +1897,54 @@ ] } }, + "/vcs/status": { + "get": { + "tags": ["instance"], + "operationId": "vcs.status", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "VCS status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VcsFileStatus" + }, + "description": "VCS status" + } + } + } + } + }, + "description": "Retrieve changed files in the current working tree without patches.", + "summary": "Get VCS status", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.status({\n ...\n})" + } + ] + } + }, "/vcs/diff": { "get": { "tags": ["instance"], @@ -1954,6 +2002,128 @@ ] } }, + "/vcs/diff/raw": { + "get": { + "tags": ["instance"], + "operationId": "vcs.diff.raw", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Raw VCS diff", + "content": { + "text/x-diff; charset=utf-8": { + "schema": { + "type": "string" + } + } + } + } + }, + "description": "Retrieve a raw patch for current uncommitted changes.", + "summary": "Get raw VCS diff", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.diff.raw({\n ...\n})" + } + ] + } + }, + "/vcs/apply": { + "post": { + "tags": ["instance"], + "operationId": "vcs.apply", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "VCS patch applied", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "applied": { + "type": "boolean" + } + }, + "required": ["applied"], + "additionalProperties": false, + "description": "VCS patch applied" + } + } + } + }, + "400": { + "description": "VcsApplyError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VcsApplyError" + } + } + } + } + }, + "description": "Apply a raw patch to the current working tree.", + "summary": "Apply VCS patch", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "patch": { + "type": "string" + } + }, + "required": ["patch"], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.vcs.apply({\n ...\n})" + } + ] + } + }, "/command": { "get": { "tags": ["instance"], @@ -8396,11 +8566,18 @@ "description": "Session warped" }, "400": { - "description": "Bad request", + "description": "WorkspaceWarpError | VcsApplyError", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "anyOf": [ + { + "$ref": "#/components/schemas/WorkspaceWarpError" + }, + { + "$ref": "#/components/schemas/VcsApplyError" + } + ] } } } @@ -8426,6 +8603,9 @@ }, "sessionID": { "type": "string" + }, + "copyChanges": { + "type": "boolean" } }, "required": ["id", "sessionID"], @@ -12665,6 +12845,28 @@ }, "additionalProperties": false }, + "VcsFileStatus": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "additions": { + "type": "integer", + "minimum": 0 + }, + "deletions": { + "type": "integer", + "minimum": 0 + }, + "status": { + "type": "string", + "enum": ["added", "deleted", "modified"] + } + }, + "required": ["file", "additions", "deletions", "status"], + "additionalProperties": false + }, "VcsFileDiff": { "type": "object", "properties": { @@ -12690,6 +12892,31 @@ "required": ["file", "patch", "additions", "deletions"], "additionalProperties": false }, + "VcsApplyError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["VcsApplyError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "reason": { + "type": "string", + "enum": ["non-git", "not-clean"] + } + }, + "required": ["message", "reason"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, "Command": { "type": "object", "properties": { @@ -13431,6 +13658,27 @@ "required": ["id", "type", "name", "branch", "directory", "extra", "projectID"], "additionalProperties": false }, + "WorkspaceWarpError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["WorkspaceWarpError"] + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"], + "additionalProperties": false + } + }, + "required": ["name", "data"], + "additionalProperties": false + }, "SyncEventMessageUpdated": { "type": "object", "properties": { From fe594693a447fb4f456327888ae2fe5ffc4b6f3d Mon Sep 17 00:00:00 2001 From: opencode Date: Thu, 7 May 2026 14:52:09 +0000 Subject: [PATCH 70/70] sync release versions for v1.14.41 --- bun.lock | 30 +++++++++++++------------- packages/app/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++------ packages/function/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/slack/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 18 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bun.lock b/bun.lock index 8e3c9b7452..4e70576306 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -119,7 +119,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -146,7 +146,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -170,7 +170,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -194,7 +194,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.14.40", + "version": "1.14.41", "bin": { "opencode": "./bin/opencode", }, @@ -228,7 +228,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -282,7 +282,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -311,7 +311,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -327,7 +327,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.40", + "version": "1.14.41", "bin": { "opencode": "./bin/opencode", }, @@ -469,7 +469,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -504,7 +504,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "cross-spawn": "catalog:", }, @@ -519,7 +519,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -554,7 +554,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -603,7 +603,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 45908e45b8..600c011b6b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.40", + "version": "1.14.41", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 298ae4a8cf..f2471d2926 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index c1acfab6e0..4ca29eb4c7 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.14.40", + "version": "1.14.41", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 9c0ce79d74..7e1d77d7dc 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.40", + "version": "1.14.41", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index d9648b3243..34ddd073f0 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.40", + "version": "1.14.41", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/core/package.json b/packages/core/package.json index 9d92e96e1d..995ab18ee5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.40", + "version": "1.14.41", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 431de79bc5..49e35c5db8 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 867d2155da..beccdb6991 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.40", + "version": "1.14.41", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 666198d55e..8b4850c885 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.14.40" +version = "1.14.41" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.41/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.41/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.41/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.41/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.40/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.41/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index f5bd20d0be..70812ab10a 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.40", + "version": "1.14.41", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 245bb86621..985e2c747f 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.40", + "version": "1.14.41", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index fa9e4214e8..861208770c 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8029d2c9ae..2959cba2dd 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index 6d2cd71e30..34175d66a2 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index 3e875f7524..fc065be9ef 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.40", + "version": "1.14.41", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 59390274d5..252c81a295 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.14.40", + "version": "1.14.41", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 4052393c0d..3eaca42fb7 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.14.40", + "version": "1.14.41", "publisher": "sst-dev", "repository": { "type": "git",