From bb8d2cdd108618c1057a8890ac1e655198db866e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 27 Mar 2026 14:45:53 +0000 Subject: [PATCH 001/138] 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 5eaac2de42..4971aa4eb9 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-a2eTu0ISjqPuojkNPnPXzVb/PLlDvw/DXDvmxi9RD5k=", - "aarch64-linux": "sha256-yLaTXRzZ7M/6j2WDP+IL1YCY3+rYY4Qmq3xTDatNzD0=", - "aarch64-darwin": "sha256-uGSVe8S/QvnW+RCI/CxzrlfAAJ1YA+NrhzRE0GTcnvE=", - "x86_64-darwin": "sha256-tplWx2tLg6jWvOBmM41lODJV8pHpkAm4HKWRG7lpkcU=" + "x86_64-linux": "sha256-4XhUHjgqinKxOeT8K5hGAjpFA2vzOp8QpEg0uYCZwvg=", + "aarch64-linux": "sha256-X2YTNOpJocIkWkkfS8RnuDW+tvj4riHs7CXM+cS9iv0=", + "aarch64-darwin": "sha256-pN0rY+cpdW+6gNWeegVprdmhc2H72OZ9WxKDIs1fvJM=", + "x86_64-darwin": "sha256-l8+Yz/6UfSPJrdgfcqy/L2SvxN2i9Apv2R0B61rpEmw=" } } From e528ed5d86dc386044552c9306af0e35baea1b95 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 27 Mar 2026 11:20:11 -0400 Subject: [PATCH 002/138] effectify Plugin service internals (#19365) --- packages/opencode/src/plugin/index.ts | 128 ++++++++++-------- .../test/plugin/auth-override.test.ts | 5 +- 2 files changed, 70 insertions(+), 63 deletions(-) diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index e7bb2a91d0..fe4be0372c 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -176,76 +176,86 @@ export namespace Plugin { Service, Effect.gen(function* () { const bus = yield* Bus.Service + const config = yield* Config.Service const cache = yield* InstanceState.make( Effect.fn("Plugin.state")(function* (ctx) { const hooks: Hooks[] = [] - yield* Effect.promise(async () => { - const { Server } = await import("../server/server") + const { Server } = yield* Effect.promise(() => import("../server/server")) - const client = createOpencodeClient({ - baseUrl: "http://localhost:4096", - directory: ctx.directory, - headers: Flag.OPENCODE_SERVER_PASSWORD - ? { - Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`, - } - : undefined, - fetch: async (...args) => Server.Default().fetch(...args), - }) - const cfg = await Config.get() - const input: PluginInput = { - client, - project: ctx.project, - worktree: ctx.worktree, - directory: ctx.directory, - get serverUrl(): URL { - return Server.url ?? new URL("http://localhost:4096") - }, - $: Bun.$, - } + const client = createOpencodeClient({ + baseUrl: "http://localhost:4096", + directory: ctx.directory, + headers: Flag.OPENCODE_SERVER_PASSWORD + ? { + Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`, + } + : undefined, + fetch: async (...args) => Server.Default().fetch(...args), + }) + const cfg = yield* config.get() + const input: PluginInput = { + client, + project: ctx.project, + worktree: ctx.worktree, + directory: ctx.directory, + get serverUrl(): URL { + return Server.url ?? new URL("http://localhost:4096") + }, + $: Bun.$, + } - for (const plugin of INTERNAL_PLUGINS) { - log.info("loading internal plugin", { name: plugin.name }) - const init = await plugin(input).catch((err) => { + for (const plugin of INTERNAL_PLUGINS) { + log.info("loading internal plugin", { name: plugin.name }) + const init = yield* Effect.tryPromise({ + try: () => plugin(input), + catch: (err) => { log.error("failed to load internal plugin", { name: plugin.name, error: err }) - }) - if (init) hooks.push(init) - } + }, + }).pipe(Effect.option) + if (init._tag === "Some") hooks.push(init.value) + } - const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin ?? []) - if (Flag.OPENCODE_PURE && cfg.plugin?.length) { - log.info("skipping external plugins in pure mode", { count: cfg.plugin.length }) - } - if (plugins.length) await Config.waitForDependencies() + const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin ?? []) + if (Flag.OPENCODE_PURE && cfg.plugin?.length) { + log.info("skipping external plugins in pure mode", { count: cfg.plugin.length }) + } + if (plugins.length) yield* config.waitForDependencies() - const loaded = await Promise.all(plugins.map((item) => prepPlugin(item))) - for (const load of loaded) { - if (!load) continue + const loaded = yield* Effect.promise(() => Promise.all(plugins.map((item) => prepPlugin(item)))) + for (const load of loaded) { + if (!load) continue - // Keep plugin execution sequential so hook registration and execution - // order remains deterministic across plugin runs. - await applyPlugin(load, input, hooks).catch((err) => { + // Keep plugin execution sequential so hook registration and execution + // order remains deterministic across plugin runs. + yield* Effect.tryPromise({ + try: () => applyPlugin(load, input, hooks), + catch: (err) => { const message = errorMessage(err) log.error("failed to load plugin", { path: load.spec, error: message }) - Bus.publish(Session.Event.Error, { + return message + }, + }).pipe( + Effect.catch((message) => + bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message: `Failed to load plugin ${load.spec}: ${message}`, }).toObject(), - }) - }) - } + }), + ), + ) + } - // Notify plugins of current config - for (const hook of hooks) { - try { - await (hook as any).config?.(cfg) - } catch (err) { + // Notify plugins of current config + for (const hook of hooks) { + yield* Effect.tryPromise({ + try: () => Promise.resolve((hook as any).config?.(cfg)), + catch: (err) => { log.error("plugin config hook failed", { error: err }) - } - } - }) + }, + }).pipe(Effect.ignore) + } // Subscribe to bus events, fiber interrupted when scope closes yield* bus.subscribeAll().pipe( @@ -270,13 +280,11 @@ export namespace Plugin { >(name: Name, input: Input, output: Output) { if (!name) return output const state = yield* InstanceState.get(cache) - yield* Effect.promise(async () => { - for (const hook of state.hooks) { - const fn = hook[name] as any - if (!fn) continue - await fn(input, output) - } - }) + for (const hook of state.hooks) { + const fn = hook[name] as any + if (!fn) continue + yield* Effect.promise(() => fn(input, output)) + } return output }) @@ -293,7 +301,7 @@ export namespace Plugin { }), ) - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer)) const { runPromise } = makeRuntime(Service, defaultLayer) export async function trigger< diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index c25984be6f..6b77083828 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -64,12 +64,11 @@ describe("plugin.config-hook-error-isolation", () => { test("config hooks are individually error-isolated in the layer factory", async () => { const src = await Bun.file(file).text() - // The config hook try/catch lives in the InstanceState factory (layer definition), - // not in init() which now just delegates to the Effect service. + // Each hook's config call is wrapped in Effect.tryPromise with error logging + Effect.ignore expect(src).toContain("plugin config hook failed") const pattern = - /for\s*\(const hook of hooks\)\s*\{[\s\S]*?try\s*\{[\s\S]*?\.config\?\.\([\s\S]*?\}\s*catch\s*\(err\)\s*\{[\s\S]*?plugin config hook failed[\s\S]*?\}/ + /for\s*\(const hook of hooks\)\s*\{[\s\S]*?Effect\.tryPromise[\s\S]*?\.config\?\.\([\s\S]*?plugin config hook failed[\s\S]*?Effect\.ignore/ expect(pattern.test(src)).toBe(true) }) }) From a76be695c7d2e60683fe79c8a6dc2c402ab13349 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 27 Mar 2026 11:51:21 -0400 Subject: [PATCH 003/138] refactor(core): split out instance and route through workspaces (#19335) --- .../workspace-router-middleware.ts | 45 ++- packages/opencode/src/server/instance.ts | 307 ++++++++++++++++ packages/opencode/src/server/middleware.ts | 29 ++ packages/opencode/src/server/routes/event.ts | 6 +- packages/opencode/src/server/server.ts | 347 +----------------- packages/sdk/js/src/v2/gen/sdk.gen.ts | 224 +++++------ packages/sdk/js/src/v2/gen/types.gen.ts | 94 ++--- packages/sdk/openapi.json | 168 ++++----- 8 files changed, 622 insertions(+), 598 deletions(-) create mode 100644 packages/opencode/src/server/instance.ts create mode 100644 packages/opencode/src/server/middleware.ts diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts index 283350532b..1fc19a22b1 100644 --- a/packages/opencode/src/control-plane/workspace-router-middleware.ts +++ b/packages/opencode/src/control-plane/workspace-router-middleware.ts @@ -3,6 +3,8 @@ import { Flag } from "../flag/flag" import { getAdaptor } from "./adaptors" import { WorkspaceID } from "./schema" import { Workspace } from "./workspace" +import { InstanceRoutes } from "../server/instance" +import { lazy } from "../util/lazy" type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } @@ -20,16 +22,25 @@ function local(method: string, path: string) { return false } -async function routeRequest(req: Request) { - const url = new URL(req.url) - const raw = url.searchParams.get("workspace") || req.headers.get("x-opencode-workspace") +const routes = lazy(() => InstanceRoutes()) - if (!raw) return +export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + return routes().fetch(c.req.raw, c.env) + } - if (local(req.method, url.pathname)) return + const url = new URL(c.req.url) + const raw = url.searchParams.get("workspace") + + if (!raw) { + return routes().fetch(c.req.raw, c.env) + } + + if (local(c.req.method, url.pathname)) { + return routes().fetch(c.req.raw, c.env) + } const workspaceID = WorkspaceID.make(raw) - const workspace = await Workspace.get(workspaceID) if (!workspace) { return new Response(`Workspace not found: ${workspaceID}`, { @@ -41,27 +52,13 @@ async function routeRequest(req: Request) { } const adaptor = await getAdaptor(workspace.type) - - const headers = new Headers(req.headers) + const headers = new Headers(c.req.raw.headers) headers.delete("x-opencode-workspace") return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { - method: req.method, - body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(), - signal: req.signal, + method: c.req.method, + body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), + signal: c.req.raw.signal, headers, }) } - -export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => { - // Only available in development for now - if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { - return next() - } - - const response = await routeRequest(c.req.raw) - if (response) { - return response - } - return next() -} diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts new file mode 100644 index 0000000000..b99cf3d99f --- /dev/null +++ b/packages/opencode/src/server/instance.ts @@ -0,0 +1,307 @@ +import { describeRoute, resolver } from "hono-openapi" +import { Hono } from "hono" +import { proxy } from "hono/proxy" +import z from "zod" +import { createHash } from "node:crypto" +import { Log } from "../util/log" +import { Format } from "../format" +import { TuiRoutes } from "./routes/tui" +import { Instance } from "../project/instance" +import { Vcs } from "../project/vcs" +import { Agent } from "../agent/agent" +import { Skill } from "../skill" +import { Global } from "../global" +import { LSP } from "../lsp" +import { Command } from "../command" +import { Flag } from "../flag/flag" +import { Filesystem } from "@/util/filesystem" +import { QuestionRoutes } from "./routes/question" +import { PermissionRoutes } from "./routes/permission" +import { ProjectRoutes } from "./routes/project" +import { SessionRoutes } from "./routes/session" +import { PtyRoutes } from "./routes/pty" +import { McpRoutes } from "./routes/mcp" +import { FileRoutes } from "./routes/file" +import { ConfigRoutes } from "./routes/config" +import { ExperimentalRoutes } from "./routes/experimental" +import { ProviderRoutes } from "./routes/provider" +import { EventRoutes } from "./routes/event" +import { InstanceBootstrap } from "../project/bootstrap" +import { errorHandler } from "./middleware" + +const log = Log.create({ service: "server" }) + +const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI + ? Promise.resolve(null) + : // @ts-expect-error - generated file at build time + import("opencode-web-ui.gen.ts").then((module) => module.default as Record).catch(() => null) + +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 'self' data:" + +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 'self' data:` + +export const InstanceRoutes = (app?: Hono) => + (app ?? new Hono()) + .onError(errorHandler(log)) + .use(async (c, next) => { + const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() + const directory = Filesystem.resolve( + (() => { + try { + return decodeURIComponent(raw) + } catch { + return raw + } + })(), + ) + + return Instance.provide({ + directory, + init: InstanceBootstrap, + async fn() { + return next() + }, + }) + }) + .route("/project", ProjectRoutes()) + .route("/pty", PtyRoutes()) + .route("/config", ConfigRoutes()) + .route("/experimental", ExperimentalRoutes()) + .route("/session", SessionRoutes()) + .route("/permission", PermissionRoutes()) + .route("/question", QuestionRoutes()) + .route("/provider", ProviderRoutes()) + .route("/", FileRoutes()) + .route("/", EventRoutes()) + .route("/mcp", McpRoutes()) + .route("/tui", TuiRoutes()) + .post( + "/instance/dispose", + describeRoute({ + summary: "Dispose instance", + description: "Clean up and dispose the current OpenCode instance, releasing all resources.", + operationId: "instance.dispose", + responses: { + 200: { + description: "Instance disposed", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + async (c) => { + await Instance.dispose() + return c.json(true) + }, + ) + .get( + "/path", + describeRoute({ + summary: "Get paths", + description: "Retrieve the current working directory and related path information for the OpenCode instance.", + operationId: "path.get", + responses: { + 200: { + description: "Path", + content: { + "application/json": { + schema: resolver( + z + .object({ + home: z.string(), + state: z.string(), + config: z.string(), + worktree: z.string(), + directory: z.string(), + }) + .meta({ + ref: "Path", + }), + ), + }, + }, + }, + }, + }), + async (c) => { + return c.json({ + home: Global.Path.home, + state: Global.Path.state, + config: Global.Path.config, + worktree: Instance.worktree, + directory: Instance.directory, + }) + }, + ) + .get( + "/vcs", + describeRoute({ + summary: "Get VCS info", + description: "Retrieve version control system (VCS) information for the current project, such as git branch.", + operationId: "vcs.get", + responses: { + 200: { + description: "VCS info", + content: { + "application/json": { + schema: resolver(Vcs.Info), + }, + }, + }, + }, + }), + async (c) => { + const branch = await Vcs.branch() + return c.json({ + branch, + }) + }, + ) + .get( + "/command", + describeRoute({ + summary: "List commands", + description: "Get a list of all available commands in the OpenCode system.", + operationId: "command.list", + responses: { + 200: { + description: "List of commands", + content: { + "application/json": { + schema: resolver(Command.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const commands = await Command.list() + return c.json(commands) + }, + ) + .get( + "/agent", + describeRoute({ + summary: "List agents", + description: "Get a list of all available AI agents in the OpenCode system.", + operationId: "app.agents", + responses: { + 200: { + description: "List of agents", + content: { + "application/json": { + schema: resolver(Agent.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const modes = await Agent.list() + return c.json(modes) + }, + ) + .get( + "/skill", + describeRoute({ + summary: "List skills", + description: "Get a list of all available skills in the OpenCode system.", + operationId: "app.skills", + responses: { + 200: { + description: "List of skills", + content: { + "application/json": { + schema: resolver(Skill.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const skills = await Skill.all() + return c.json(skills) + }, + ) + .get( + "/lsp", + describeRoute({ + summary: "Get LSP status", + description: "Get LSP server status", + operationId: "lsp.status", + responses: { + 200: { + description: "LSP server status", + content: { + "application/json": { + schema: resolver(LSP.Status.array()), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await LSP.status()) + }, + ) + .get( + "/formatter", + describeRoute({ + summary: "Get formatter status", + description: "Get formatter status", + operationId: "formatter.status", + responses: { + 200: { + description: "Formatter status", + content: { + "application/json": { + schema: resolver(Format.Status.array()), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await Format.status()) + }, + ) + .all("/*", async (c) => { + const embeddedWebUI = await embeddedUIPromise + const path = c.req.path + + if (embeddedWebUI) { + const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null + if (!match) return c.json({ error: "Not Found" }, 404) + const file = Bun.file(match) + if (await file.exists()) { + c.header("Content-Type", file.type) + if (file.type.startsWith("text/html")) { + c.header("Content-Security-Policy", DEFAULT_CSP) + } + return c.body(await file.arrayBuffer()) + } else { + return c.json({ error: "Not Found" }, 404) + } + } else { + const response = await proxy(`https://app.opencode.ai${path}`, { + ...c.req, + headers: { + ...c.req.raw.headers, + host: "app.opencode.ai", + }, + }) + const match = response.headers.get("content-type")?.includes("text/html") + ? (await response.clone().text()).match( + /]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i, + ) + : undefined + const hash = match ? createHash("sha256").update(match[2]).digest("base64") : "" + response.headers.set("Content-Security-Policy", csp(hash)) + return response + } + }) diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts new file mode 100644 index 0000000000..ebf0163cd6 --- /dev/null +++ b/packages/opencode/src/server/middleware.ts @@ -0,0 +1,29 @@ +import { Provider } from "../provider/provider" +import { NamedError } from "@opencode-ai/util/error" +import { NotFoundError } from "../storage/db" +import type { ContentfulStatusCode } from "hono/utils/http-status" +import type { ErrorHandler } from "hono" +import { HTTPException } from "hono/http-exception" +import type { Log } from "../util/log" + +export function errorHandler(log: Log.Logger): ErrorHandler { + return (err, c) => { + log.error("failed", { + error: err, + }) + if (err instanceof NamedError) { + let status: ContentfulStatusCode + if (err instanceof NotFoundError) status = 404 + else if (err instanceof Provider.ModelNotFoundError) status = 400 + else if (err.name === "ProviderAuthValidationFailed") status = 400 + else if (err.name.startsWith("Worktree")) status = 400 + else status = 500 + return c.json(err.toObject(), { status }) + } + if (err instanceof HTTPException) return err.getResponse() + const message = err instanceof Error && err.stack ? err.stack : err.toString() + return c.json(new NamedError.Unknown({ message }).toObject(), { + status: 500, + }) + } +} diff --git a/packages/opencode/src/server/routes/event.ts b/packages/opencode/src/server/routes/event.ts index 96284242f9..989b857710 100644 --- a/packages/opencode/src/server/routes/event.ts +++ b/packages/opencode/src/server/routes/event.ts @@ -4,12 +4,11 @@ import { streamSSE } from "hono/streaming" import { Log } from "@/util/log" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { lazy } from "../../util/lazy" import { AsyncQueue } from "../../util/queue" const log = Log.create({ service: "server" }) -export const EventRoutes = lazy(() => +export const EventRoutes = () => new Hono().get( "/event", describeRoute({ @@ -81,5 +80,4 @@ export const EventRoutes = lazy(() => } }) }, - ), -) + ) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 7dc6ec1bdc..cfb22929bc 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1,67 +1,30 @@ -import { createHash } from "node:crypto" import { Log } from "../util/log" import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi" import { Hono } from "hono" import { compress } from "hono/compress" import { cors } from "hono/cors" -import { proxy } from "hono/proxy" import { basicAuth } from "hono/basic-auth" import z from "zod" -import { Provider } from "../provider/provider" -import { NamedError } from "@opencode-ai/util/error" -import { LSP } from "../lsp" -import { Format } from "../format" -import { TuiRoutes } from "./routes/tui" -import { Instance } from "../project/instance" -import { Vcs } from "../project/vcs" -import { Agent } from "../agent/agent" -import { Skill } from "../skill" import { Auth } from "../auth" import { Flag } from "../flag/flag" -import { Command } from "../command" -import { Global } from "../global" -import { WorkspaceID } from "../control-plane/schema" import { ProviderID } from "../provider/schema" import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware" -import { ProjectRoutes } from "./routes/project" -import { SessionRoutes } from "./routes/session" -import { PtyRoutes } from "./routes/pty" -import { McpRoutes } from "./routes/mcp" -import { FileRoutes } from "./routes/file" -import { ConfigRoutes } from "./routes/config" -import { ExperimentalRoutes } from "./routes/experimental" -import { ProviderRoutes } from "./routes/provider" -import { EventRoutes } from "./routes/event" -import { InstanceBootstrap } from "../project/bootstrap" -import { NotFoundError } from "../storage/db" -import type { ContentfulStatusCode } from "hono/utils/http-status" import { websocket } from "hono/bun" -import { HTTPException } from "hono/http-exception" import { errors } from "./error" -import { Filesystem } from "@/util/filesystem" -import { QuestionRoutes } from "./routes/question" -import { PermissionRoutes } from "./routes/permission" import { GlobalRoutes } from "./routes/global" import { MDNS } from "./mdns" import { lazy } from "@/util/lazy" +import { errorHandler } from "./middleware" +import { InstanceRoutes } from "./instance" import { initProjectors } from "./projectors" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false -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 'self' data:` - initProjectors() export namespace Server { const log = Log.create({ service: "server" }) - 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 'self' data:" - const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI - ? Promise.resolve(null) - : // @ts-expect-error - generated file at build time - import("opencode-web-ui.gen.ts").then((module) => module.default as Record).catch(() => null) const zipped = compress() @@ -71,30 +34,12 @@ export namespace Server { return false } - export const Default = lazy(() => createApp({})) + export const Default = lazy(() => ControlPlaneRoutes()) - export const createApp = (opts: { cors?: string[] }): Hono => { + export const ControlPlaneRoutes = (opts?: { cors?: string[] }): Hono => { const app = new Hono() return app - .onError((err, c) => { - log.error("failed", { - error: err, - }) - if (err instanceof NamedError) { - let status: ContentfulStatusCode - if (err instanceof NotFoundError) status = 404 - else if (err instanceof Provider.ModelNotFoundError) status = 400 - else if (err.name === "ProviderAuthValidationFailed") status = 400 - else if (err.name.startsWith("Worktree")) status = 400 - else status = 500 - return c.json(err.toObject(), { status }) - } - if (err instanceof HTTPException) return err.getResponse() - const message = err instanceof Error && err.stack ? err.stack : err.toString() - return c.json(new NamedError.Unknown({ message }).toObject(), { - status: 500, - }) - }) + .onError(errorHandler(log)) .use((c, next) => { // Allow CORS preflight requests to succeed without auth. // Browser clients sending Authorization headers will preflight with OPTIONS. @@ -105,8 +50,8 @@ export namespace Server { return basicAuth({ username, password })(c, next) }) .use(async (c, next) => { - const skipLogging = c.req.path === "/log" - if (!skipLogging) { + const skip = c.req.path === "/log" + if (!skip) { log.info("request", { method: c.req.method, path: c.req.path, @@ -117,7 +62,7 @@ export namespace Server { path: c.req.path, }) await next() - if (!skipLogging) { + if (!skip) { timer.stop() } }) @@ -215,27 +160,6 @@ export namespace Server { return c.json(true) }, ) - .use(async (c, next) => { - if (c.req.path === "/log") return next() - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = Filesystem.resolve( - (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })(), - ) - - return Instance.provide({ - directory, - init: InstanceBootstrap, - async fn() { - return next() - }, - }) - }) .get( "/doc", openAPIRouteHandler(app, { @@ -258,126 +182,6 @@ export namespace Server { }), ), ) - .use(WorkspaceRouterMiddleware) - .route("/project", ProjectRoutes()) - .route("/pty", PtyRoutes()) - .route("/config", ConfigRoutes()) - .route("/experimental", ExperimentalRoutes()) - .route("/session", SessionRoutes()) - .route("/permission", PermissionRoutes()) - .route("/question", QuestionRoutes()) - .route("/provider", ProviderRoutes()) - .route("/", FileRoutes()) - .route("/", EventRoutes()) - .route("/mcp", McpRoutes()) - .route("/tui", TuiRoutes()) - .post( - "/instance/dispose", - describeRoute({ - summary: "Dispose instance", - description: "Clean up and dispose the current OpenCode instance, releasing all resources.", - operationId: "instance.dispose", - responses: { - 200: { - description: "Instance disposed", - content: { - "application/json": { - schema: resolver(z.boolean()), - }, - }, - }, - }, - }), - async (c) => { - await Instance.dispose() - return c.json(true) - }, - ) - .get( - "/path", - describeRoute({ - summary: "Get paths", - description: "Retrieve the current working directory and related path information for the OpenCode instance.", - operationId: "path.get", - responses: { - 200: { - description: "Path", - content: { - "application/json": { - schema: resolver( - z - .object({ - home: z.string(), - state: z.string(), - config: z.string(), - worktree: z.string(), - directory: z.string(), - }) - .meta({ - ref: "Path", - }), - ), - }, - }, - }, - }, - }), - async (c) => { - return c.json({ - home: Global.Path.home, - state: Global.Path.state, - config: Global.Path.config, - worktree: Instance.worktree, - directory: Instance.directory, - }) - }, - ) - .get( - "/vcs", - describeRoute({ - summary: "Get VCS info", - description: "Retrieve version control system (VCS) information for the current project, such as git branch.", - operationId: "vcs.get", - responses: { - 200: { - description: "VCS info", - content: { - "application/json": { - schema: resolver(Vcs.Info), - }, - }, - }, - }, - }), - async (c) => { - const branch = await Vcs.branch() - return c.json({ - branch, - }) - }, - ) - .get( - "/command", - describeRoute({ - summary: "List commands", - description: "Get a list of all available commands in the OpenCode system.", - operationId: "command.list", - responses: { - 200: { - description: "List of commands", - content: { - "application/json": { - schema: resolver(Command.Info.array()), - }, - }, - }, - }, - }), - async (c) => { - const commands = await Command.list() - return c.json(commands) - }, - ) .post( "/log", describeRoute({ @@ -430,132 +234,21 @@ export namespace Server { return c.json(true) }, ) - .get( - "/agent", - describeRoute({ - summary: "List agents", - description: "Get a list of all available AI agents in the OpenCode system.", - operationId: "app.agents", - responses: { - 200: { - description: "List of agents", - content: { - "application/json": { - schema: resolver(Agent.Info.array()), - }, - }, - }, - }, - }), - async (c) => { - const modes = await Agent.list() - return c.json(modes) - }, - ) - .get( - "/skill", - describeRoute({ - summary: "List skills", - description: "Get a list of all available skills in the OpenCode system.", - operationId: "app.skills", - responses: { - 200: { - description: "List of skills", - content: { - "application/json": { - schema: resolver(Skill.Info.array()), - }, - }, - }, - }, - }), - async (c) => { - const skills = await Skill.all() - return c.json(skills) - }, - ) - .get( - "/lsp", - describeRoute({ - summary: "Get LSP status", - description: "Get LSP server status", - operationId: "lsp.status", - responses: { - 200: { - description: "LSP server status", - content: { - "application/json": { - schema: resolver(LSP.Status.array()), - }, - }, - }, - }, - }), - async (c) => { - return c.json(await LSP.status()) - }, - ) - .get( - "/formatter", - describeRoute({ - summary: "Get formatter status", - description: "Get formatter status", - operationId: "formatter.status", - responses: { - 200: { - description: "Formatter status", - content: { - "application/json": { - schema: resolver(Format.Status.array()), - }, - }, - }, - }, - }), - async (c) => { - return c.json(await Format.status()) - }, - ) - .all("/*", async (c) => { - const embeddedWebUI = await embeddedUIPromise - const path = c.req.path + .use(WorkspaceRouterMiddleware) + } - if (embeddedWebUI) { - const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null - if (!match) return c.json({ error: "Not Found" }, 404) - const file = Bun.file(match) - if (await file.exists()) { - c.header("Content-Type", file.type) - if (file.type.startsWith("text/html")) { - c.header("Content-Security-Policy", DEFAULT_CSP) - } - return c.body(await file.arrayBuffer()) - } else { - return c.json({ error: "Not Found" }, 404) - } - } else { - const response = await proxy(`https://app.opencode.ai${path}`, { - ...c.req, - headers: { - ...c.req.raw.headers, - host: "app.opencode.ai", - }, - }) - const match = response.headers.get("content-type")?.includes("text/html") - ? (await response.clone().text()).match( - /]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i, - ) - : undefined - const hash = match ? createHash("sha256").update(match[2]).digest("base64") : "" - response.headers.set("Content-Security-Policy", csp(hash)) - return response - } - }) as unknown as Hono + export function createApp(opts: { cors?: string[] }) { + return ControlPlaneRoutes(opts) } export async function openapi() { - // Cast to break excessive type recursion from long route chains - const result = await generateSpecs(Default(), { + // Build a fresh app with all routes registered directly so + // hono-openapi can see describeRoute metadata (`.route()` wraps + // handlers when the sub-app has a custom errorHandler, which + // strips the metadata symbol). + const app = ControlPlaneRoutes() + InstanceRoutes(app) + const result = await generateSpecs(app, { documentation: { info: { title: "opencode", @@ -579,7 +272,7 @@ export namespace Server { cors?: string[] }) { url = new URL(`http://${opts.hostname}:${opts.port}`) - const app = createApp(opts) + const app = ControlPlaneRoutes({ cors: opts.cors }) const args = { hostname: opts.hostname, idleTimeout: 0, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 4109068443..527584e7e2 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -411,6 +411,113 @@ export class Auth extends HeyApiClient { } } +export class App extends HeyApiClient { + /** + * Write log + * + * Write a log entry to the server logs with specified level and metadata. + */ + public log( + parameters?: { + directory?: string + workspace?: string + service?: string + level?: "debug" | "info" | "error" | "warn" + message?: string + extra?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "service" }, + { in: "body", key: "level" }, + { in: "body", key: "message" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/log", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List agents + * + * Get a list of all available AI agents in the OpenCode system. + */ + public agents( + 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: "/agent", + ...options, + ...params, + }) + } + + /** + * List skills + * + * Get a list of all available skills in the OpenCode system. + */ + public skills( + 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: "/skill", + ...options, + ...params, + }) + } +} + export class Project extends HeyApiClient { /** * List all projects @@ -3773,113 +3880,6 @@ export class Command extends HeyApiClient { } } -export class App extends HeyApiClient { - /** - * Write log - * - * Write a log entry to the server logs with specified level and metadata. - */ - public log( - parameters?: { - directory?: string - workspace?: string - service?: string - level?: "debug" | "info" | "error" | "warn" - message?: string - extra?: { - [key: string]: unknown - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "service" }, - { in: "body", key: "level" }, - { in: "body", key: "message" }, - { in: "body", key: "extra" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/log", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * List agents - * - * Get a list of all available AI agents in the OpenCode system. - */ - public agents( - 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: "/agent", - ...options, - ...params, - }) - } - - /** - * List skills - * - * Get a list of all available skills in the OpenCode system. - */ - public skills( - 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: "/skill", - ...options, - ...params, - }) - } -} - export class Lsp extends HeyApiClient { /** * Get LSP status @@ -3962,6 +3962,11 @@ export class OpencodeClient extends HeyApiClient { return (this._auth ??= new Auth({ client: this.client })) } + private _app?: App + get app(): App { + return (this._app ??= new App({ client: this.client })) + } + private _project?: Project get project(): Project { return (this._project ??= new Project({ client: this.client })) @@ -4062,11 +4067,6 @@ export class OpencodeClient extends HeyApiClient { return (this._command ??= new Command({ client: this.client })) } - private _app?: App - get app(): App { - return (this._app ??= new App({ client: this.client })) - } - private _lsp?: Lsp get lsp(): Lsp { return (this._lsp ??= new Lsp({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4d0b13539f..318b8907a9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2249,6 +2249,53 @@ export type AuthSetResponses = { export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + /** + * Additional metadata for the log entry + */ + extra?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/log" +} + +export type AppLogErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] + export type ProjectListData = { body?: never path?: never @@ -5036,53 +5083,6 @@ export type CommandListResponses = { export type CommandListResponse = CommandListResponses[keyof CommandListResponses] -export type AppLogData = { - body?: { - /** - * Service name for the log entry - */ - service: string - /** - * Log level - */ - level: "debug" | "info" | "error" | "warn" - /** - * Log message - */ - message: string - /** - * Additional metadata for the log entry - */ - extra?: { - [key: string]: unknown - } - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/log" -} - -export type AppLogErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppLogError = AppLogErrors[keyof AppLogErrors] - -export type AppLogResponses = { - /** - * Log entry written successfully - */ - 200: boolean -} - -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] - export type AppAgentsData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 007391177b..5362e1daac 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -356,6 +356,90 @@ ] } }, + "/log": { + "post": { + "operationId": "app.log", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Write log", + "description": "Write a log entry to the server logs with specified level and metadata.", + "responses": { + "200": { + "description": "Log entry written successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "service": { + "description": "Service name for the log entry", + "type": "string" + }, + "level": { + "description": "Log level", + "type": "string", + "enum": ["debug", "info", "error", "warn"] + }, + "message": { + "description": "Log message", + "type": "string" + }, + "extra": { + "description": "Additional metadata for the log entry", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["service", "level", "message"] + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})" + } + ] + } + }, "/project": { "get": { "operationId": "project.list", @@ -6762,90 +6846,6 @@ ] } }, - "/log": { - "post": { - "operationId": "app.log", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "Write log", - "description": "Write a log entry to the server logs with specified level and metadata.", - "responses": { - "200": { - "description": "Log entry written successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { - "description": "Service name for the log entry", - "type": "string" - }, - "level": { - "description": "Log level", - "type": "string", - "enum": ["debug", "info", "error", "warn"] - }, - "message": { - "description": "Log message", - "type": "string" - }, - "extra": { - "description": "Additional metadata for the log entry", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["service", "level", "message"] - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})" - } - ] - } - }, "/agent": { "get": { "operationId": "app.agents", From af2ccc94ebc632d0014f54ea5c5e6c2e26b5dda5 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:22:16 -0500 Subject: [PATCH 004/138] chore(app): more spacing controls --- packages/ui/src/components/collapsible.css | 3 +- packages/ui/src/components/message-part.css | 13 +++-- packages/ui/src/components/message-part.tsx | 2 +- .../timeline-playground.stories.tsx | 48 ++++++++++++++++++- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/collapsible.css b/packages/ui/src/components/collapsible.css index bab2c4f926..a999f62986 100644 --- a/packages/ui/src/components/collapsible.css +++ b/packages/ui/src/components/collapsible.css @@ -9,7 +9,8 @@ overflow: visible; &.tool-collapsible { - gap: 8px; + --tool-content-gap: 8px; + gap: var(--tool-content-gap); } [data-slot="collapsible-trigger"] { diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index bb16581d66..d9893503fb 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -636,14 +636,17 @@ } [data-component="context-tool-group-list"] { - padding: 6px 0 4px 0; + padding-top: 6px; + padding-right: 0; + padding-bottom: 4px; + padding-left: 13px; display: flex; flex-direction: column; - gap: 2px; + gap: 8px; [data-slot="context-tool-group-item"] { min-width: 0; - padding: 6px 0; + padding: 0; } } @@ -1154,8 +1157,8 @@ position: sticky; top: var(--sticky-accordion-top, 0px); z-index: 20; - height: 40px; - padding-bottom: 8px; + height: calc(32px + var(--tool-content-gap)); + padding-bottom: var(--tool-content-gap); background-color: var(--background-stronger); } } diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8b572aff81..0e5c98d8ff 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -790,7 +790,7 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) { const summary = createMemo(() => contextToolSummary(props.parts)) return ( - +
Date: Fri, 27 Mar 2026 22:06:47 +0530 Subject: [PATCH 005/138] fix(ui): make streamed markdown feel more continuous (#19404) --- packages/ui/src/components/message-part.tsx | 88 +++++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 0e5c98d8ff..1555a09a07 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -156,37 +156,75 @@ export type PartComponent = Component export const PART_MAPPING: Record = {} -const TEXT_RENDER_THROTTLE_MS = 100 +const TEXT_RENDER_PACE_MS = 24 +const TEXT_RENDER_SNAP = /[\s.,!?;:)\]]/ -function createThrottledValue(getValue: () => string) { +function step(size: number) { + if (size <= 12) return 2 + if (size <= 48) return 4 + if (size <= 96) return 8 + return Math.min(24, Math.ceil(size / 8)) +} + +function next(text: string, start: number) { + const end = Math.min(text.length, start + step(text.length - start)) + const max = Math.min(text.length, end + 8) + for (let i = end; i < max; i++) { + if (TEXT_RENDER_SNAP.test(text[i] ?? "")) return i + 1 + } + return end +} + +function createPacedValue(getValue: () => string, live?: () => boolean) { const [value, setValue] = createSignal(getValue()) + let shown = getValue() let timeout: ReturnType | undefined - let last = 0 - createEffect(() => { - const next = getValue() - const now = Date.now() + const clear = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = undefined + } - const remaining = TEXT_RENDER_THROTTLE_MS - (now - last) - if (remaining <= 0) { - if (timeout) { - clearTimeout(timeout) - timeout = undefined - } - last = now - setValue(next) + const sync = (text: string) => { + shown = text + setValue(text) + } + + const run = () => { + timeout = undefined + const text = getValue() + if (!live?.()) { + sync(text) return } - if (timeout) clearTimeout(timeout) - timeout = setTimeout(() => { - last = Date.now() - setValue(next) - timeout = undefined - }, remaining) + if (!text.startsWith(shown) || text.length <= shown.length) { + sync(text) + return + } + const end = next(text, shown.length) + sync(text.slice(0, end)) + if (end < text.length) timeout = setTimeout(run, TEXT_RENDER_PACE_MS) + } + + createEffect(() => { + const text = getValue() + if (!live?.()) { + clear() + sync(text) + return + } + if (!text.startsWith(shown) || text.length < shown.length) { + clear() + sync(text) + return + } + if (text.length === shown.length || timeout) return + timeout = setTimeout(run, TEXT_RENDER_PACE_MS) }) onCleanup(() => { - if (timeout) clearTimeout(timeout) + clear() }) return value @@ -1332,11 +1370,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { return items.filter((x) => !!x).join(" \u00B7 ") }) - const displayText = () => (part().text ?? "").trim() - const throttledText = createThrottledValue(displayText) const streaming = createMemo( () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) + const displayText = () => (part().text ?? "").trim() + const throttledText = createPacedValue(displayText, streaming) const isLastTextPart = createMemo(() => { const last = (data.store.part?.[props.message.id] ?? []) .filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) @@ -1395,11 +1433,11 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) { const part = () => props.part as ReasoningPart - const text = () => part().text.trim() - const throttledText = createThrottledValue(text) const streaming = createMemo( () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", ) + const text = () => part().text.trim() + const throttledText = createPacedValue(text, streaming) return ( From bdd7829c689830668ae9a6026f3187196774797c Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 27 Mar 2026 16:39:13 +0000 Subject: [PATCH 006/138] fix(app): resize layout viewport when mobile keyboard appears (#15841) --- packages/app/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/index.html b/packages/app/index.html index 6fa3455351..8fad7efb3a 100644 --- a/packages/app/index.html +++ b/packages/app/index.html @@ -2,7 +2,7 @@ - + OpenCode From d36b38e4a6f5b778644669ba281fb5a35cf2f028 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 27 Mar 2026 13:32:05 -0400 Subject: [PATCH 007/138] fix(desktop-electron): match dev dock icon inset on macOS (#19429) --- packages/desktop-electron/icons/README.md | 3 +++ packages/desktop-electron/icons/beta/dock.png | Bin 0 -> 33332 bytes packages/desktop-electron/icons/dev/dock.png | Bin 0 -> 50483 bytes packages/desktop-electron/icons/prod/dock.png | Bin 0 -> 38916 bytes packages/desktop-electron/src/main/windows.ts | 3 ++- 5 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/desktop-electron/icons/beta/dock.png create mode 100644 packages/desktop-electron/icons/dev/dock.png create mode 100644 packages/desktop-electron/icons/prod/dock.png diff --git a/packages/desktop-electron/icons/README.md b/packages/desktop-electron/icons/README.md index fa219a77ef..cf2f8e24c5 100644 --- a/packages/desktop-electron/icons/README.md +++ b/packages/desktop-electron/icons/README.md @@ -9,3 +9,6 @@ Here's the process I've been using to create icons: The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS, so app icons appear larger than expected. + +For unpackaged Electron on macOS, `app.dock.setIcon()` should use a PNG. Keep `dock.png` in each channel folder synced with the +extracted `icon_128x128@2x.png` from that channel's `icon.icns` so the dev Dock icon matches the packaged app inset. diff --git a/packages/desktop-electron/icons/beta/dock.png b/packages/desktop-electron/icons/beta/dock.png new file mode 100644 index 0000000000000000000000000000000000000000..f274ef64598bb3553bcc9ebda724cd6969fbd48e GIT binary patch literal 33332 zcmdSB_g7QT6E_~ZNK^3*Qlo+t=|y@+nuI1Qy-0w75PFAD1O%iTdJ9OGCcTFuz4sDC z0tBg{L~3XcpXZ$CeE){;Id{*_-rXPOo}HPU*UapV($!X@y3cqY002;FsK0&-01(`f z1b};_cb%zQiS7SJ?r+tU0F`6R+joQ4HijCuT3UdYcl12~L4-Yk_g9Za-47_ z75xgz4S7J^*X?AdAABT}jIAj%b$>tSe6Z;~cDr0Zeu+H$^DoQ43U=B)ZsbtqTO})V z$uQqjR@Q1uM-)U+)M|+s<2rZ`O_03CLe4!BiRDDJ z9fyC#J1*9|gU-BHa61Ma_|q!Kv2GyA+KB;f2x>OoQ z%v#PdaGmF&6XXnODh@;Wthgy%v%}#RD?>`HfP%*zigK(0hh1Ofca22P*sC7q+#Ffc z`+rsFGJ%Cm{9N&wrDHB+u1P8~aMKB=f3GVgg}QxwlQ{ zRaNW4;2-g2SZhmN6O3MjldMhBe{+N`d33=PAYsf~0jxR@x~govhN2(ynF^+Z>II=g z$QTz%6w<>`!5sV` zAC#J29kss?yx9!8>9V*;ws6%Gbe$8gRB`fZVp$e<)~umg>x_Q2DscUK{Q7rxmH+bY zO{nyK)4F^!wqfaJ>3fH8<`6nJCY0Sq6OkLM-`FraO?Hx4JmULtE2xDo%==x(^{z3^&0o$S94GJ`yK)2dmf|OFCu%wB z8QOfvV78G&$do#z*WGIvA+KyH_KPwPk zEGG&r$ZGNmVgBD7|BQ|P5`i7K$eD6*I;A0eN}tnpBP@C}UiRwn*X{Dd( z9Vknb6>0^c*tR4a<1b;A8d4{+o!nQ4Q$JSY*tM5F$>Y>v!2;|p=8`pJv4D;I;+W&( z?55K~><}RG_!Hb0%9TYQ6ulT9MsgEI4vW*8 z)N3=d`P&Hz^*(c#pX@9dp-M2a5yymq=?^<#1CwoK8LWc|t8U=bw@PO_w6A@&CZwQ@ z#|+V(sBM-b2quegavaJn&XTe3Ep2w2tR$>#81-{HTL{xvxZ3%xNd@}<2P>?Bj?2WRO`z1erv~eCUTqp)6}g& z?K|mvecIYa)~ZO+XSg*skVzDqnlC0%4J8X`SfngJE(A^4iscr9c3#9Wd)m-Oh|^Wp zCa$sudnG8!dycPd)0o-RyQavV8%?aE9|dQ^Jx`&I*w_a}qDMtP1D?doEWH3N@^sO+ z-mb`B{gS_e`}rM5P1%G$Ie7T{FqK677xh!lAnD!tA%ByKPUpaNrj_$jSKQL7l9VU? zSm)}`0#jT8r^kfGG>s?q3A_quljz!78YgTtpDF{@Co6u6K)&rD2gxPr>6k=vM*^$lqUZB9v6`S_OEq>njXDOW6n+~5#k%J)^94|mHAdu`9XK$bo_+R zOh_fnZf-I=Icc)UEnjO3#bh&^h^RrMrhqOob=Y*9OwlY^0n#Ei z$(oE!M9=bsi?_GHba1bFB5TDbo-?!Yo+sr*l5Tp6q_Q@u*_gtZrs@(1R9LYn57qiC4JZW z5gpqEz+gC$!}aYgwN7nUi2A9U`1Iy#k#CI@IxNyUi${(WvK!v3JDe7%EjDP;705|d zv-Pk(v1V$caQxvss~}W;J|(~=c8O8j1#d*2RNS752mKR2;BRLNeAqNe7hJ4$<5zUMhcdr*eR0zvc(Ld@Q82CN z^=8PLj6;4K-Rf8f+hBf;3r4YGe-Mo{jj>fxCw!Mbs+%WHp@9<35_~N#Z<3nR|6pc6 zm#if}6`Vhs$RUp%J65|XNtId~aLMJN9?$--$*W7BG~h!)agLj!|FSo1-h0_ZdM=et zK!E&#XH1!w*=lSZAT7;nuB!YuPA%wIP2e)_;5@FQq6e9QK`C-%Xbcc6lWT~T@+h;& z?0S7aop~7Nv9YRO(psOwx%Wutmib~PR<TiGeK_LJr?gzj-s~WRQNDrPxy$|CUUR zj)hH^))DCo<{8;O!;ugVY^(=ZN79=6FgNHZ+QRXA-<#m=hxE$o*=l~7(Qy#R#&m>_ z(uoMay@K_{qcm)ZL3c;(ANuoYtLA&h^ZI4c`Uevd^PKawGEm$tPT&^jZhncHzwvD^ z4onQ`V@Rr1Xh-=!*M6k)joa(9r~MpU;Lqb!8jS5}l7@7Cx$sVW0)2>`a=T(f)R`y~ zYMS$8lEafjESlmp>tD^!A!RZGuZWa$B%@(Js>@teJIxpn`JGykBD!&Y^ENFXVSQ|>5= z;_SEk_}_}3YmL-3M0+yQPGN`R`ZLo=DSRG3|2sOMcGht*^Me+dJUMM9^nft;f_BZT zzn$i=*6NMOUXtH~PFY*qD-y9pTLorFcV{l9$eSnhAUY-_=Cw?l*1b61>y0IUNYUcdN_ z@gVGcAol&r?e)s-#gCN-(ExGIN(0xmMu!{V?Gdn}-TcrOLNz4*=m8HGQIZ2>jIfPy zDA$eYO?R#GBOnv|xOPn?v!}h1Z>#0d3+)l3v?fqO8(BhttD_wm3nzm-s;lu-!bBPJ z?P$kI*4+u>M6cnNkHpT1J;?EZ3F&e#{ImmqNL6FnCOck`b@_Qu@AoO8J(1jWl^3n8 z<3u8-m>1po;`I-3O6lRO?bH_s)FV!`Tp6Bv?qs@l}qWTqcxtu8AG6Qr4fgvTm#wrA;g zmd?fFNZB_S$_H9USzqf^6tUpLLdl1f`nxJWha6~!obp1tM+?cRe}YF)1=^0MvL5k5 zT%M{(59qFDi{J$+1;V0!$#rNebr_i*o&lAQW^=2Prvn@#3{nZ4&u5C|P8<$=$Nh8U zB!oOV-DE$oz#2q?>Q7S6kZ0{Hx;*QMNhjKomqTdCL@U*cE=S1=E2>bfi!VL>mDE!W zg73y&S4aJ+d~U%2wnU3?<=kFvieFEPQxL;*M2g^%Av#O{%8|oWH|Smc#mQ8UXUzqN z;1?+~8q7#&*5Di3BBy=3EoD5Myt`YKssA&g#U0t4mAg+|7mFf$TTsmBi<`~M@!1)1 zaV)M#m%9eZ8I>&S<^9r#&VF!Qr&n`p1M@#wN#d)dS)eja&HT)HR#vws7;f%B(HVa= z5dZtA-r}-e=J}$>8;<)e+2iS72ghXzNFRld9odC#u#xrHIddRdM11Y0CL8%jpVaf2 zN$!P~4KMygPI#>51F!9*HGdXDmIh99ylMaiu`}a_C!AU=JN#KwVOzSYX7&TFB`aaO z$}OI`qPaG8nkII%kUsi0Gzk_LF2V-oBm+IrL?xy)CzHMwTY5g8Rvuq7rSH@O5k^f` zNr7(SVSC_;gRHB83^MQx$Irea>7XO}phO@T!x+)*V)l2fVgBW3qQ_TIj-b`X+0k!7 z#zgBDY3%S!X~5cNf%q_Yo#4T5@LT!}xyUe$%+(3E?yYH-V2BZ^93+4a z&@cqLPv(Q~HmAcfbX1$DdCdTK?P;=@pa1&)gXLKOY>SCerA6Y(3^`@4Z+@KUeWone zWPNDMA>#$edbPM+D1Qw&p-K7^36IA_Q>a}RjIZhVYQM%LlKeI+-LL*d9V|DF2&6k*|GfWKnlQuYKUTL{S2`!UV2j%mi=aU0(QP;_1SG@9X~Y2n^8XY+tDla~7n$B)$M&ibKN1V^x=({PI5IxI4_wx!9>Sn;i zBgQ`0Z7pZNX{hIwb9keY6)3IFEP>eV@8%6annZvMR31tS!xmwC^f|7%*^-KBc6UtF z%o#Z3q93;c>?pm)8nU+C*5c*rUW6fpqrl$fW!~m?ZQOToVB8KD#tlCoiIBp4CJFwX zz;+*^MMvnSu3>^k(Te4rt6w}7m-R(;eG#?E8+9!El?KN7|DuugkvuK1H}Rhf5eT{% zzO8DZ{QG6D>0U0K9G*N($}d1Xb_;>zH@n5OZSayjQ9_6)}VSm+fLjk>1ZGE zC1M`T*6QLUOy^N$=@dNtVu*y)8m{b9(w;>pLAg@NQX!ioKxn-AU3D z-^E9Wr6jQx;h~)r0)CUxVmi8B+it$UWU0N%2Syy{B`%8p2e?LIg8U?gNBAEGqYUc{ z-q#7zfSRuznk%Q!6)y)^)h)J_QG~-ClofECZs|cx^acBhCHv|)F*{Az>&?`o{x6;% zQJ-~|9gIoILc{14CCWB>RK%iBzkT7OIbWl;vd*(nbh4#LXP|+t)s#?}`P%1GZRH1H zxiYVk5Eoe|40uq0+kjJW)>H>(p()s&i6yQe1T4R5N(KGs9@P$yrO9Xk)cK7&2! zr_6c588kC`YTNpjRc5zTWWA90rbce(E%3q~&RpYfB_!D2sYfG^(oXZi>zT`W-uJZc z9$Q3WlEkm-xS63PJTtaZ zlg4YaHp~z}K&z=jeOh)lA!`GAbxRJ{`lx%e6$Z<<^Y~cJAgAAO+YCbz`E$ae;mD7`qvZY}nG@`;awI+yqa(sv(B8DKVMvSY6uwVDmOL;^^Q zO-!-mvy*WiyX1*1>pv@5by<##%#3N{Xt3&2NY7khg>$;?nY_(B$_J8pO(zdO0VSLk zPx|$V=#1P(*$WWcp}hIPorU}@wvED4HCcP~ABYu14LCf{;Ry~a;yc19=En%D;CB%C zQiPv|qBAM0wNoXw2RdR)b||~@PuBT|suL+y)IeBwe%d>9B#*$jl%KCoFjk_M`hnP* z68-!a7P#Qa6Hv}c^Wf&GD5soQbupC-S)R=Gb9l|f38(Bc2@)=CS1Aka>ME!_(aL4cUC2URej-C+xBYjX*)^wx3NzX_ngYIqnogw#R=3Sj z100IOVWPcx$}?be6cHasb{sAzI6Am3Y|EZ3;8;LQs%Bx`7|1C8**27rwT4>8#U2gT z$Y{cK8Y#vRA%%Ae+GIB1#R7JyhjDKBM?dnZu1gReo(1G5AI-Q}(nKBg>1!w~t!jwY zd!HRpmBg3h*+k84)FNeIqYd&^7iyBbsAB4z2x0OKCbFNbG`F?D^AKQD1tu+(`QMn1 z@_b}1s@%+Ukbgcl4)R{P<3mKcs2!LkgFNGCCVkLFGMHMNbam6S8P{2tDbbMKiC>*$ z_0E&L6g+~*Q=Zn9(pXDiH0Zy38|!IDmP;IcjThYbmI@UOv_!ozbMc&t6Q1}cKbM@T zXeZKr}q;tV$qHI7?U9$-4dT<`FoK#m8t0d0axZ44eGHrT7PKrv`3^z8K035kDbB_f&b+@r`caVI2m#)iD|g)a=ig!JE13LhMz8 z#Z;B_i8j##o^0^B8us5iMubsBjJ3(Kw2_74$*>#uj8Mkjla!o_pGqPd3*aHsL3*KM&(){- z1)+M4C2STgg4admlKRvod+PZ)r(Khf%Q0|I)h)it8LpQ;N$qjzm34JEqKs#ONu|`q zwwtMB_OZQ2vU=kn`!}8}<*b34F3E!yr%OG=mG6I(nEG7|TLd;&x&|7mrN49i$nly{ zSLOmmq*TntJD?1n61iCH7OCb#>1#_#{We|W?V*xC#_C$jCeZ7Yu;jIw;SM1xE%@#v z*;o!5^}!a=ZF29`{4sk^N8KcXu{gaxJV?M7^;xx=`WP28{3>VaC+)yDFmVfPc)@b= zaHLAhlf_$UTfv%p;mg5fq`{%y*{hrByIjGj3^lViz`%0j>bt`MVBpOX!Az$1c=_uU z5svtl!Qr1(>>>8xR6bIYd3O&%;bz(=o@vJc?X;0luZJg`gGH{kBMXXwybLP?tvQ}x z8^TD4Oe?B3I;Ul`*L%_GxunpzgQg_MPl1m|B?DLLBmF35@Y=-0M z#*XVMf^5o%Q;`%o939bQ*P@q)GqRdq7J9`gcPGos_GB{(Bs97w~_b zh=kUPTCUlv89j5uop{pGG|#vFedWy696!^g@bRIg2?g>@#j=7V*9zK8YWhP!x~(g$ zQ|OtJ?~H6q)J?jZ7l8SsGjULgNWo-k+mfHmkofn`;JPgfQApI7_~`_MiMyvinGOoM zS`hrOEz-Ib7hvC=o+An~(J{dV^E(c`0LiZ@VZ2r$i%%|QAV)8#(vQ_(yoXqUrxa4o z2i0u3)ln?bLhO9&59nXgg#CSHCW$u8orML9|CfCU00sV?cnR;=Ye=OI zc6dxw{4jOL@e;V&Q?MntoBkBe_KtoaI9v`_PE`F0u8hFS!x8?312tTNx=s6=i^4=< zy}<-YWGx2zF`J(}=4S>2Dz-O2Ia+9r)r=c2k@y{#^(;^=};Ws}2_AMkbmzshDJ7XpBJ<$vdy<6mXqg79#8 z4(;ZGB}H5bCs2xI{|*J+vsl7>3l8ODqaY-*|4m@2c9}n#4h$%T`wiVstR1=YQ6Yl^ z^gqawY_IkK<;9L`JF&V9TaTA`lALApWbzu4tEnw6(TCiF&SCfgl3-+^Kq7>*vjCFX z&(nS^0`45F(t$cyq}qC!_3^vrXD!lyF2=m&f;H_O zI_v2GU?_!RdsO|!FfB*V7d9?vex2isjb~*sg7*0pEIU1j5tyq2*5r z%N~Yzk576-df3g<0$jdUpP@8NCTv?u;Q>(gFddCMMJ^tRjQ86XII1h9vdfnfJ+XZt zXET$z_^!iY@8JM6T&L&cIP80lQZ=|lh4fq7QVdYZ z@1N>-F0v}A^>@J`v`|xW(LXWUZ;t1a+9$!SsH{oIDbuz3N1QUaar2e@83Fq8UV_?} zxYT1l{yLq6QRW}g>+v#-tR z@45TOuNi_O@q;?31xfByiO}G)HU?tTGY25U*bVPJ;KhI%O2=GQ&fnnE}@XUh$`0hVkQdFwp}8V_1&r!bV)(e zeDL9TSfmAGibhs&0$Dxsylka#V-(XxrxUN@F!5r^I!2y4)6-*jC`;emCnHAYY41WO zyomXjcLHmGkG%^{zgXGMb)KdDt{GHIfp!aK3N<<$4;kfUNcC`hTUSwe^{`vV=p?Z$ zd}oow6cCmlX{9jw@Zq>9)+IhmkZ3aI&Qm&HwY$_{X;70=2@hb+Q)#)lg1?_vLH%Fvg zt%UumLw8oEJ<*9Ne5W7)SHMO_UB$1_Og!LQklx;XC&r<{Y+3 zEW11RH^v=ZI4pSvJ{_!mb6XhrCH;%?DH-jlzFH95wT$o%;K)Bk8`m}KFz`Nb>x}p0 zIJVp~PxPr6#M2&^If47=Uov(+p=QC*Dur_GDq4})z5EMwV?JacvWH8hhV4B_-2D4f z<+5`ok>;H$3Z!j3^hRIDaQ)ySycMASN7~e#K=3@eJ_CCCUd&vg;hvO69=wfPWN7cl zLCqk$ZFGM+A`?c3(eIZ+N|v|Y)8%a7;WH@nE(}cSUoqbJgbI39z3}B9UZh`vLk{-$ zbwp9S5Zy^`?bg57k=5N2uB+ObuJmun+Pro>gz=~tk>Z}&Td0?|yk;wtQaW?P_&5iT1RqNc>X;bUK zIh-e-SNhc{=QSxmF@%|bML^QFNn?ONQC?wK{9f|%e>7fPDB73uahD@3HTLqp^?+ug ziJ4@C?~DvdWi{YQNy6;$#iGR8U;(@!(RpM_~-xjdjL=>mHe z0i-6QJ})m3S=adW2Q=p67%DtOF&%f#U&43JRv8yq!Q)_tJW>#+^Bo#PZy7A>eP|Jz zWqRf})l*+s>G{m#lCnh3BW|%LG>%#4bCb38o{KX8Wtfot-}Vlg>=vckZ)mHV+3PR# zefXL0flDmO)Bi~byW`pNpqUH&RgHQEd3dTDnt4zMwk3DD@x^@SWpSlfiT_g~?ak_Y z5@|E-=7M0PxzHX?)3H|O;G0FXwuNt6l^Jr+|9nlsi+~pp^Xw1(&(2?BE+6U=o7H7b zVyKL#s!iN&{b{ZK+UeBQ(*+AjFHvd}iYqFq2y=62GVBgKS2JT3<=}Jnt3EkftdwfT zvuWyiAD5bW6Po#5mbNui@H|__4@6Wt=hbviemS0YeCBTMzo21sJ-WARQj#Mz6_US< zDNU{G(OKW+S=Y>fy`XAE8|hd?IlzT}RVFs7v;%WlY-WBrO?956%F!Bl)0eA!A?al4 zlt6funhC)wNIDga(a9rAeeBI&GmmB}i>$lF!_IGus#VCuka~du;DFeDnLvWZsQN)^*kRk7SGc#Fkslt zy11AkI|ztJmOGGfXIAeDLq<&G5m3S1XT*<=3sq2DPjONM`6? zN{61$(n!2G!6?GxMwD)Ai~x4}@>HSkg^U*MTzxqDl1+W{YL-!Rte*RTe@bKmK`~`5 ze)ejnCPVD(af`W1YLa^+L-qkarNz8n-{fjrX)zPq{L7u zgY<8J@JvseiB(VS573nh?LQzgIS+cK9lUmv1A%T}D)c;ptH-Y$EK!#I_n(Yw&TP)~ z`EfEbcuk~Z0z-RSzOT>D&*Pi=y7>z)a6$btKl?b*Z|7ce8QzE1MFjEo*7)cZJ-~`> z>(g-Ws6K$UH>`FN%mz}Lg4AI`Yq9y3PI zx@+=PHgW17dIz6&e?$Meg=05PxY7@l@sQ_7eann5E@zQ_@g9BmeH)!DOn~6eW5l)( z(PY*OW>R_6PN)#XY3GImLP~9@i)katf{69g43sX|=(%HB)B_GPS-7$pqgz{(V)mFK zhk@mmm->!1F3;amM;<*<50UlEGlPCyt94%;6$xzJ=gAXHG(2|GYexmx$|p)+zR7Ai z=URF)9O0%x>%Hf~-AS0x8eEqy*0W(U&*5RmyoRIOqPBc`(CC*ylFQg+1h6GfRGk*q z;}~Ec)M2nUMI6ngWEYOsg_L`^yg}_J-ITJv;*jrW4APqj_CfrE1qq;)qrWMr%KwmeYPI^8b=!r$6-w`eKT4w&rT9S>|NFTX z7fj11k|ek<$EV2tn`4P5V@4AdVsgf2X(XEQBtlKR1visV&EHcgj0Y42O!Y#faFuk7 zv`7ZZ-<{UamfkZ9Y8O;jvYtA|*BllQ_*%c|A|-RfL3uo>cQk0#ELaW;^cxj6&z>~B z%hz3bQ8p%*3mtzlz0XT}Kr^79DkTw5axv{{I^=Vp$^eaT(NO!~yAG!@y($kp?VmTM z=!`-;eEZ{sbCKd1Bs*g=GX?7B>VdL6N&g$S)nKo&FXrY24wccoR+EyDy9j;C`%>Y4 zfDN5~8DQ<>BV1Zks1zA8gzm@)lwU5T9dbaZkQAJlg8 z@N}N{TtpkCY2hZQ%>{CCx8&{1aE%q+RakkDCF}ZzsV*~{8Y}{Prfilu(;Ic6Yg0iI zd@kN@I=|V7l7I_}=^{PG9(yD$^uM_KojHw{~T zI5zjX#|Jv%_u7AEq9RwAwoGi_s;InD|?c_KHNqPVB&AXJr`9Ga*km?TH_Dh zJ#gM{Lvq?mXC!5muZ9=2H_wiwg7r>vPX1WEwl(!F;;fHHUCBr!3%FKRt*QkRJnVT_h=hTz`tNxrHoZPdx~a?x$dvI=ks zO{*5=2MjGb?QWsyo{4^~*^?XO1i#QkJ?)R-vHDr!Z036nP-Cz;qniIs@PtR4K%D1A zY<=E6Lwb581XT)%GU_B1ru7{E7|XGj6*!vNbo4YcLfAnWHf7Gi7Sx;4I$|EZ24r~C z05qOkjTc=KTX$7e&4HGmCf3F@Uj=^+7`-cHoS zc=|M$NqXgAbfr%Dwlv9f{0H?}jn=_kAd{hT0-9=oK}m%-9O{z3-39JZ!PL?K z+LypIDSPSdRt!9lE9=R&M__%VN^Cg5xG&I{cVJR1uV$)G%FW!iiAceArjdVU4D^Au zhR*xLF+F4j!(B@!`gd~EaK6LCTSEg8%OsH9SRinMFP_Yd{LHyJvCR(RoZQ@qQol3n%0S=f zH*oU-xl@np4@3C?>FP|}BYlcXDr#{e_v0Mvl1EI?8%h7y?RH@Rhg|vkF1l4($IJ2^GP5HM%z#DGHBFJvW>D6H2<+0 z9%8SyGb!n)_cUTZ7qNi9shJ@q68L<(KQPFt zX8x?tbY(ZA9jLaPHjum!p}W(t*dyN70at4fxV1)Z$O>^I_@W&7RrdsU2>FDp6nYUYG)o^QZ!{ z`oBmE!Sdh{OIGy4dnk^>dd*V-WDM=^qvy*_iGvM@4{C8%7DB4cLCT5ZzeK3dXHMXL zhjYAlYT%eaHPUChE(2E3=e$6kX0}bc_?!<}QBI&m?)51?7l0PWzH(W&iuko#*36Qc(-GT;Ei1(;6? zh$g{nBC2M9DY_=N1wHS&(yZh9RYf`gQzCgnic5;%`2+(9L|Iv1g5cD%*O6x+YKjOz zgE%fO`=B)NQ1bETo#}A$`>~b;1E8>DTsFt0qYN||$Zs8VffFRbstLr8k_KHYmt1n2 zs2~p%-G2^^$GxS?@vy{ybK*rkd^10nm+OkNBA-tT7!T=<3ss79j1aO}K8LnqL6JFb z6D$Qv*uofOU8WdjJG2FE?wccexRAosSO=Y>#+H-lIMB#YuVoCT)8^r74F}nnC{%)?_%E{BoKL)|5*i81YJT zzCBWV72wH~9aGB9av0iR?eC%`)5^hI18y54lB`o>9VBXPY|{ykugjP5&_rn%T{v{z z*&{J^2{F=FjtL?;Y-l$d~pK=MT`2i$SC_Twr%0<44c$xF}IuR+gpbv0C38 zy*Q$>Dk>_IR47@gH^qR;>@y*!39iVZP1Fc!6*k> zIM!3hUh$BQ@%%|EcrhD}t3w17$b%WNn*F4wyrn!)Dxd#xk~JqtODU?SK-#uqDDsg5 zTpllnhFHx`N1W(6_vtLc)*Afl>?!L)LYWUMn>#8gZtH~q{;l&vO;t6(IjKry@eyef z|HjQ{ zNsB+H86DOWkMJv-cLnKQFFZk*k$AikZHdM1|Coye!eWKc0=`*h39F72%i*pDrXv0W zO7oQ`=~UKUiwZeV`7=(#v>2PDMrv!}R~a$>aiDq zgiHk8T}Fh(ex8WcnoMC`>pblqqn2r;?DAV2i5{xHLUea1s$8aS0};x)Ot?%(!T%{)`V6;r`Ug=bU&g0E%@ zZDcgWSvSflX7lCXA>(Nqq$RE;JXw>m1b;>**?z zd!Ng6mf1fayd2shD$%l^t9}MeZFeYK`f_JbWB4xih|j@X0Yszzggb!b>|TBvNq4GI zwiA6iDU!@%3i;29l&3UpdgiwZamOK7#wr~so!bc=@iHt8EO}o)O$87`YlI(8xOWbO7RfAeSwyJhe9vkUD=7P4xfc>QMf)SOPI;|w zL|Do1BQ1cVRhuo;YdqUHF$BEBRR1Onv-{PQcna z{eJt|9||Pt2qEBh>IJU9x{l>1r4>J1Vo7G5;bcs~Ea*c%-6EkDa`z+Ihmbyq)!Uql zk(2)voXDS<#3@qeuYupq_RUtV?dHfdmRXWg#HlxW9z@J!qn)Z^@ z3o!r^y)PzrQmWI~c;M82V%_)wm0k>*<=7<)%r!^;t|At>ll7GWSFzh}dOVf9PrOVw zLPwebe)7lrI{XQS6?U9BIxGZ|L$t0#R(g?4mk;F|63=C#cjbN8aH?EEe<@h6NonLE z|6vkOBs2pNS8z5PM2h!9P4(ayJ4g0^Ga|XdhiXhS@IEx7fzR<>pmaz6I8(^=uo6h=^$%|7%X!?r!qry+sPwE_ktOXob8T(ig2Y1JeX*)1 z$_~CcymX+OJHaWhyf`p&JX3Vgj?KPRE$jr&8o=h&N_6+e%0 z;`o?y+weW<2^WbZztB(ef7Sgv%QxM0r{tMi$707M!Eyz%kGC4~AAYzJBsJZt-OQC9 zHAEk*F?<&*DJzT9HF_q68M_3;X+qhV!4rL`!>2j~eHfd3uNO27r19go`fC2A4utR& zskKA3^mN&cko*ALX~_CYep~%*z8Ob~YaFep4TB@-ol{eFFSfJf+<|2x^k=l^wI5C2SpF+;jDOA4Gx_GBv3#9GOZ?n770abDl|YpQ7`+zGcN@a@+ptmSgY>mp=I+Y0?aiYRVRN0ZK4HZxD5oy`b~OUHol0bA z?AA0guEjrf=S)N8Db(cud2h$=TM=caw0_Q%_)hZ`)%_t}3f_5IG!9!^l5x6}vd0Va zTjIL%jC#V^fRaAA@VZ$zTnaZ4@;A`~OEAy>KXk#I5UOfY!*AA%iEYBOy)!UT+DZz; z`;I>72_c`xSU#c{mva@pt%JF+EuYZ{JqKPFHu7n?w}9f5~fpXjOP38 zXz&@mPg)Qe>ev^I9FJ4QD08t-t(3AIr9-jXlhqVIe}&i^KsY~`}uz~a_5~jv|9~z^A?M++&hc*Nyj|b z`M6rYe{QqS1*2^d-LDeX8}q$_7K;5|7Q`r?Abi>=w79}u+$KEJ#e$gO-T_p@aVYG! z9ec(n+xGgAF*v2W5@KT|40pcyPo6lulw&nj}IXs!op)0uT`Ge<06;=6x=!KXI)y%@4 zJMT@TeAtFl9#xF$nm_FAfH_viDodU8#0uG?i?`#&%8RM&=p60!8Fg1QOo;Bz*iPqP z`c3&GR8N^4-}d#@wBvXxuj=aJ36uNhN{C|JGC;T%k6RDv{@3VOab$BJy-gB*(HPQo z=@-4fopk(xQMV7PwqdT z>TQ4nv#lB$zg z3G-D?GTA5E$e5PS7*5aASfDl8;b;1`++J+@2{e)8V(4+s0r-5urj zWz!z<7fVz3{BH!Qy~M9zgn>0Na)tX6d(Z<~f%WuQmUEQoU9?CBvkW^V{<5Muql#CO z!v@znJb~GJuN*$5=zLN~KJaDSIzhyKa2!*>XMMN%IRW-4#kv3Jjn{@sYUz4(q=yvG z7CGM*yBFE^e?CLr6TC=mo>SNIQ&!mDCNZJpZ@^H#;TN})rbR$^SiBGGf>j8Xv6wzb z6GuzkZNe9|u@9+?WN&(3=X*$E4PdY&ZG1M?bYeswgp z#b}@y4|DI5kYD?3LklU=ga1!+XZ_dI|Nj5c(ki~F2?G%((x9UUihxKAMd^}87!A@P zA|fRY>6BJ##6TK>(OqMsLtrq05z>6;`}0qHx1YD2-Olrz=eZu&<8i+hvr=JBGCsn1 zy<{f1ngQ6hVACWEwOT>XRfB_U#22Crlg?k2!7lk}G8b$cOdZp*-oYk}T3c}o!{{NJ zKj+mvZVTaJ*K^Ys7IEzDO6`DTcDZeD_Q9Q-;(G^OHRC0q@9dz<7!3!8YWJ`hFL;b@ z3#yu=m^8TaB%@+A(+(g06NWg&ohD*FltyV>3o>YDQVrPfds^i3d|po1Vhkhj##NwV zwuM)FGx!8`)f-hE1|%wkyEMAikSW@FS@d%Z0Y=0CXM^7cVs`LkONZw^c!v%- zNo<&aBAoC`4;Glb4c(+M0Wh4F=J$YUZ%>WVfVNVF-EHlN9O_sDh_{}8pm)6^AC55p z`;C`Vou*;5`%;v>DT>V4aq4|=O$%mUH~k!ncS5Vsa`_5hXI4BY@bS~l&wWq?o{d*(R-DVHM)yVQQP#9V_+1;mSe*j7+Up)n~b&9j}8=vY)zzAurFL+ zqjCCYW2_S}eVI7g(rpeiZ#gKq%*&k!parWI=gtY7qFs=<_+(DYc5UCm1uHvSOC@qg zkg9`=v-HR@qh@NUlV*xS_syD#wGTfU$v2kq0t3FC2$JgenG3-94WP|h&-m4qPy0^Q zjO>VPPONQBSPYR^lkP1SMIK9$?UM*mvv8U+8PNJskQwn`3zs3PWc2=ZtJTxfPH#Vn zpB*6~&F9Mo{z#QGVo%W=_cRSX?iStb?Uw-!D+_T2VN8V z{$5tp`#3CAFe@VCZa!ji*gJKdQ zJVX9O0l&gU=W_fy{s_a$O7SWA7SJEDk}-0!VU^eVb!K5zSn74r?k8qru?=sfGR95} zE6|nGKX@!#Cn%6xF#Ma{$0{ss>YiU;wDW}uy)OVtHY%uE8}LqMeZ>!#)@ zC0P@x6Fy$55Q|eqcIk5+JmId)D zX5ec=Vrt}{!e7kX^1F8L`D*nBP#B{*ROKK4W6T(?4q4HqL0d-dCx zo_meQ_#s!wm>)YK-hukYZRVCKG6O$|kVA6?-WAv$tG26-Jk!JN? zqgDU;aEtWl`JD{*r9y}(_1NL z|IqSat>~UQ&@;|`phylGL8CZVpi7vyDDYCb3}jDT8B0Hg%2dsmx#M~}qf;#%9j53t z4sA+pK4^xWleCw~dmrFJVSh@6RxSYgFK&{WQ_cc#iDoil;(|C`LZKFMSBPtt&z31n z>h--+3!6VBr+x6e3H74qThRg@=jB%_wS?A2v;Dt2`zm!Ji%Ih=gIGJuBOf7T#4KO& z(oCV)%TrQi^k@FuvGQngd6{UP_urZtat&UBOPXTJujHWLS92V`7JN^NB=5Vw{*VAs zlT$z!sTys?J%39+qR!zsmZ12=CFTaHfXv~X^a02fwm+CarwN~og)ztt$tZtnSnLb4 ze}v9|Ow%1nPv@V<;DIFxMkrFXaev~|V2aaciIZ&n9;ftWY_C&r*4%zLh>-4A>sG01 zA9yDj;X97{=0p)v7V0dmo!R*$$>P>&m?py?DbwOs77xucDVt~jM3cIa@Y|x1%<$0X z=XJHTHE*SlVbCIsj{DZC6(U|>Mk!yAt@J}ym-@$k36I)-b?ih|jal&84hy#pSuwSC zfCVuHz4(aTAcn-=jEqn%c3bXe*M$AJ_Nn=lqbafYQ$; z*mGC0faGJUVRfw|AqdU&DR>VY#?wC&X)YX$S=G6~PQQ;|BY$MF7y_);(*A-lV% z>g+vEaR6GzgnC1NMafu`kXmnAfY5n&F9ZHmfC{RPk^ds;mW+VAS7|(g=l^+IsvV2yvS(h2jO&4~@mwt1WMkGf)o_fcia5A=+ z9Q-9dZtq@&SWbOY?dzY;P#nr{Z(-RXJUIYV+g#d;I$I34c(dT%YRdAQ%Sy`bHRu2* ze>tab!OkPP<*kuU|P-$-PUC&H% zQ^p98Txb$ynW$S`=O~2!UfdYQKPkq=8un$$w7x92b~3#CPTTNDB~zv5h}p8)$To>9 zwvR*OWnOsW?Bn?wr?$5zFS()hC(mTYzly~p>0&RDmcxk8s5P>L;V;O$K?}#cJ^&S8%X!+iVR-OMSa4rOn0=r~-0%FsoHTW<)6Yf=fHu|Xr-oW81PQg2oqEL&vjL~tTU{6zdHnH|D47Zy|lgxB0 zvc*TXlUz9*l~-l_z$w2*!f3<@@btc_-eZRiLmJ~`JF}oo{I9wmir>6f*DKAEtNC3e z4j|P9=YeMXo*iNpCz|=o4~kq4!%W{A0FRx?pXj6~ZCMHgbHy45)?LMplnh?IgMip^ z*k8sKEMRHgou<4NdAE?Dw>jK6RHh1Z=~j|gzTcJ|e`(iG4C<=;^1UeY^WhN<^+-9@ zAnyZ-$2$7wsCTm3EvU5iB^B8q#_d<%Nq7GHL?5<~)YzwA6sNS~BqwAIwP*AbbRGNb z^e0FR60h6`c#Uz?1N?H{;KMDEa!9lBE*v)u3~ovuMt!M<6!g9V-gdZfW@?UG<-VICcKeS8cB#hl@nV_ zF-vbV36^R68$pi#FynkDim@jh@_lG|B=9XqkRaIkw4k~qS)yqK*4by~c1n-8n?@eX zP>Q?ooE+x340OJhf3G2>tH*6axvyQRfpzCC}9rtcJd_}reXDt+HCTgQiHs#oUq#u z#Ft146VLm21&zZXfMcHn%!Ks7X_BbHMYt>G-BpW(I3p!=w=9t>#>Ah)LFNIs`cxkV zroDe^TM!2=?yh%$WzVh#ic-{`q_>qRW_~^RqIrn{qxN(hB_5*a9oBb`lZq&^&$ic% z7dC%5asAfIo@vdq7x)&K6WJgr#+WKRgaw(2riR95g*8_9U&tY*&tp^C%p zflU$s2w%SGq2EruK+g{6Eh^U_1vO7b)(W_S1jSwspJFSG$NuE+Z#BPNxY+iAz}-E3 z%Eq#4>yFZf`3F_EB1{i}q5N~icY};^vv(S2(xhJ`N2sTtg~iK>&mI7Rh|*9*(Pg`< zJTuS5K%@6F!aGV!yW zet#i*#pnGy#HBH*QyN)o*oR!_UJ>!xJEeadA)G7NrTl&NmTcrp#C@YnH+7{v*Pq61 zy8HAmsxs%nSNaWSYWegBhDaRiyRj8CPsr+fO|sLt%y!;SLH1d@f?2ZKr;hF=;abZj zDrdV(jDM0^`^a<`@>;x{?Krl2ao`pbOYPgk$rSB5YI{D@Jv)W9MG0RS#O_OF#E5Ju zA9K0LX5UkFCeww!M@2D*I*Oh4S0+Ih!oME(7}YN?0pCW5Q^hvcQrS6$#Fy16u52Tc z`P+5w(MRMN)T|XQd^2n7qKGKR`9@fjf~5&#-{@T#Z~iBd^NR`4EO&aRa+akS>a_R0 zQL^SLMpdQ(?mmwCwD5R#sy6SoQ^H9`Wp>02Dv-MI#$wc@6udI0vh3uAGG$rdbJ;-u`aFUDA^P z0#DT84U4hV1|wIKgD-e9{0iAB8?)3F!$XuTbn!X50kx_}vrf%p{eaM~X>=H|OUmZVW8KQQ z+M?>W<{W}=Zc`4@G1iSP>$q(`);I8-byzCo@%q#|Zue`S_}TM%P6#({(;nn|x^4d( zx+ga2N zUDhw%_)&;jlhK$I7j2OU7oUdd}nDAMAnNQlYOMA-SrBj*pva)=CCR4+b-Vhwwuzr z2RZ%YOx9nJy`nT(|MM{)MvP4A{l4xNY&>7R{!AIzBA~es@y+jt6*Fl34)WDE(G+;j zsoS7yJXJs@xT{bqjqU@XU<(%v`qJYuU^}Gb`_36R(_F?yV859;QEvF$jz!0eQ=k0O~#WY5f< zSN(YsM3U7{4>285o0p+`V&+pDHS1>stM=q?3f~6YrzLd%)%ZY*Y_GQbs%f-_Ijz3- z*^OI*w_516h5-|Hp!`TmbX9@mlwAU`DmJOKVSB|a5a=`cN9BKAF68J?0PCnE* z(`STY@&HaL-e=J|_7m$KsGW6R;X1!z`wNvH2iMJ=Yw3Pv8s=R7H*Y2O-q(Y-{^g0% zlhF_qM{60O|E?ao31~3IVqlQo)3Focb{IFfqC|M(NwH&oI-e{m3rH{YjZWA4Ke>diP?cKvmqFfe7Q<5k$irDgw6WK-oU|iUYW#FVTOqV zwK-xka&|v^^<8A5>w4KtcBi<(#a@;O8Xw#S9PB#t<|yj*S>4vCGNakMnWjL%?Sad^y8Y=m6oVWP%N>P67l?pkPjIYrLg z#E-M{^Ji>IO}+0|85(}pSWxekmWe<;%&%n=)aJ*gpxcgbwHHzB2ejt5zQ<14qy;PO zB)duVss~h9-q>y#E$yoJ4VrZsK5Z7kwG?GW-cL<7tG5o75$u`y%K<*mcZx51F`Hqh zWf!)S%Y1@=F#L^XbUkNrfi{M&dQx`h5af;w=L50z@z>oI_Cu4p`+ zNeqLGtSBRkEjzJey78hK)|=K!4? ze(?A#XDMBEpuPbn{5J(lSr#?yceGgmLxE*QrbBS;`YlVcubq&KTH5WB!G^l0N3AS_ zKGj^>;Y zwvr`zKVTbUoG(CmAS*4-V{AmdGfT_IRQq+9LOUW3d!+PAY^0`@8M6W|oKKQ~P^X$q z{>olVz<&#WjUD^GR`I>&>$}$Zfigw$u?U;tN7I{Z^m8%&9d{zoRh9=zF&e_cPXfa~ zt1`$R$p@2sJYi7lvZO8|imGts)^`YORN=Yee^`1!7E&5sz}xdR#)aF=k` zhXu2o`sbg`OzrL7$UNs~Lxq11161|Tz@K7ne;9lj>+BuVf3RwHS@bEELY$JXWpj8q22mA2 zCdChyMkWp!vLWM;xg|H$_@l3*t~h*F9ZF2FV++(z%-pLakhitv40VpPdS475{C&hn za+Ew0IIvNy#x;LoLY;_+Xug4T6kgv2pI%J=dh;S#!saZs;W9rZ8Nh%# z3VHq}%`8jv8m3J6={`AOZ{2eChKUHLU68@YfZSpG0dpY=kfuem@X}^Xmjja-Z%l+X#5)Q{5 zphV5&triU@&*YLMkv~GrljZu>01)vTPuC8j9WDWA}18T<1W zuDV#$ETorWI@`-}ETVToTE|(~##v9c`ur^9Dr|6Z!T@EEhU8~92F3R$+AXsguj)4l ze}#@D467R8OnNN0YY(cII%W^}CkQsW)F5F>(>II#v;rX_A1&g9H8iEoWxB0 zeE?#6UiZN{URY5E%uve zIMUCsiPb93I*RDiO%`}xr_ZY|@WEuV_B_T1Zu0w0;*7l_+j?z~9d68E8O1om5wu$U z@q~^9%!8=O5X-8s;q@E_dYAS_yRq26^@ z!sHs$`uI@$$j|_rb;2i@iMsEKUE8JYwhVQ?EVL?w#hyVo8NJ%g1ZJc2W!>JSXlT%2 znHKOEn?RaNUhI*@{MX;BU%I~WT1$O}Gb>8`^{+%#1EY;)4VuVqMpDZS<;2%|1Fv#% zz+0~Mcn#sx>AKA@VqeX7HWU1xfS9`l(~3#4PN26PG$(gBH>2J=Cpsqewhi}}7J+{c zzl=c`>h+ngA7&hKye$R3OW;+EwxoJI=l_9F37mQsc^RnOaA5jjB5Wofw3iO1Si#&O zA}zRR`tDE+u{V%d)!2#$`@$g;Dsf-@Gd+lUXLu5J^nsi0i14{)C_?|pLZ0+1uj@2P zMe9umCR)gisW2luf9&=9@xsz%o26%4@s7X#jtn~tzv2S+T~GGMLVu<6Q5*q3{|ZXl zhHdWN1a0lItHki56RX;4r18%|^l~v$-SfpTMa^E<5aNA~05o$I;N=R0K2)QU>+bti zH(jmIfvX6yxv2Xu7dnX^*N2^&Z;ddY<|*Q_qb+WUUxVLEyHIaCoyGZ!5}!b~T-V?n z){&$(XVMGv01ZvET_g7|9qMltCXAd9l_KY&Jj3ssQkVvi@wmlBW~5XbN+s+{g=tEr zrPaIDeDzmYUg~0TnNK;c$*dY1FPDh*$Q!o4Ko2S(nJphlj|6j@w3mkkXebyFxsv`B zOtt)pO21Zr$cHft`~T1m-mdrwa3-%vzxNmC08G=+;PjnJP+{X7;X8c01 z@Yd=;ETvprVqKObU%6J7$3l-u-oJw1YU{;YJ&^b#>qtYkv8Vc4h(hkeSpi$gL@r08 zbw<%Yw7Z7tXGHCWmQV)jRY}pc_+9jH=mk16@Kln%2|2v!gr;8Uth*R_1c|oRzDSK} z(Sykz>It~7-TyPp7PpTe|PIew=)R z@SPLY#DnnL-afQ}>o|XvIFR%!>TLs;)xA5+LC@T`dm_=kONF2ai$4i8@4ID|b4a*) z>a;*x5DORY$>q=+MjBD5i=a$&+sT*4?A#`u(zkeJE(Xclr$TN^ z^=MESz!Xj}eaI)7-*hStVXOJ_apQJ_93-3n+(6#M0$U53TpWEgL8ElsP7pa>PP>~M zr7Yoo+?W5{kx$9Hi$!kfL=ar@H;1>Q-6x@lDlj|rj2e@id>MZnn;o_-efs6^@utZs zJ9m)gf@ep3?=W+qOu4h!6icM5nUeA+-C#Z)sM^m&nky0vb%^x=IAob6+CbgdlZkq}$2N z+XLC4zN2~dz}*vfW;8&a65VOI9dIyi-nUjC?qX&}!cf*jzA$Smg=^d3$xm*-T(=E$xy$ZYL@o`5tN_!gg_Zz z6t*^WKd<#Lg{pUZl2Q+zK9-@nl@SUfc@m^`q)okDcU$^EFi6W{e7))ZkRo|^q@4Z3 zo3hbzW2GI*)N#z3%VQs_*~*7AY)=2dFRZ7Cmq5tw^# zt!XOj$V7gkOovl)VQ~LVV;}HQSe4^7AGeZA8UtpUa$ANZ$*ty9Y1#9U9fXw}*0}4zDmQrdv08!d+sEx5`4glW-sGzd6u* zvB*26`~M<+2o;(apD%H({`WdKs~)xw2KT7XD2f%us2-$eUWzSfZ-PV8Pqgpn#Apv$qeq{rg6( zzSxJs%u@5io!)kiQzNoL%d0M}(&reuDrFKyieRktZ~uv*=q4XY4^f!6MHY?xjUmzg zDuI=&h6yNoNv6X*hEqOmZr!uS)%#gWqDYdQKw`R7m5GwWF55xp?KIkSI4&TwNX6iD zR9pf&vNMLBLuk;m z1^>DQNRY8Jiohx^&EoD>!4(d!r1KUO0{GVb!7sUYgF^C z)C~Pc!uH)Qa}5$&pbHGvaa$gy)CJR8k7->GjHZ2`EH^bd%E~p68Y+^5SQr`+K<*h{ zGxH|X?eId_E3;IsEeP}hcm(_j0tr~f#5m`frE% z#mzRe?tvq}gT&Ek2m6D!X&Ft=yZwnQBurA+?PMS4eNW+g^oc`v+AzB>$hc2wO>B>q z(JT=}jLo((%=kOr;9doKvo+{%?fM0hknK*xezJ(Cy19mPh zNO1WASAM}6w_UA?9-+Bd1%;dGa>QDRDOvhT!ruXAC2Ii8?AQojR$9uvM`6!%hJR*O zy_S7Q7$|J~)%0W{oA`KR(a1obd&E7>W6^qaN?qx6)I!WWU5v_%Qf;q!k{LFxY49ht7q#%S!?6b+aKQ>|LA^gwhVFFdAL&> zSv^cbLy3880QF95<3PXZn5q2)luONJCZv+gxmUwXHa1ld=0`qLoJ*Ijie%eFpbh&ASPQoD`vt%+gT33aBx&?O96 z`~&$dTxf{GTB2UzBXM^+1SQM0!Ml&E%VP}ZI-6Dophg@bBslkaG$NOWiWm_VvEp(5fgS^hk^ZP@Q+j~9c8$;iM}Dl5V!5X`Jb zW|dBZF46(E`g71cI9v_?8kX!X$d?T{@#Y<$Qu6MOkAS%>NibP%e^XE2BL!6Q*g|Ha zL_Z^EvTMXF(ueq^$!zte8yaTJcN@-FF7%mP=I(@4UpGirAZsH&es&T1>zOmah1?52 zPQ&fH7&&bba-lTI6p$1pZp2gyuk=Q^>>ymYxDA5;EF7o1Ft96lc}g5O^T*P5Qn!f! zyyM;pvpQSC2B_OMLxtiIUB_*hCm(vZDFimq`LLDbI0j{THahRX9U)5#+*B9Fje^HV zIm;cBl_CZ8hCid>6RJ6%iu1safVXx>z!jaU*=u10C0W}&gn^EDUl@nf{Qf#+8zDPg z4228}3cBOkbegNp#eH-_H!7>Dg|~u@ltXe%y0aj;TQIqrH1I;uMsB1pc6%~A%p4UF z53~F!4G3JN7Kf9251drfi`FkzXj4W>hTTh%*+e8fvtq6xv)ijSG&*N^N%V)Tu5?Fr z<><+fONIejZXSG!zwNI>E*|`oQ(_dukP-^;a0UoJ5XeWAO&hF%yDp9$;7cr6g@E3b z^~xjfto}rGJ6FP#j?e-TUUWN%2f~`&?eB049rieGiKVq+#37F&zT)uSPqKWa!!``- zZ7_BO*_E*zac5gXI!7qE0lV>fZFl`#;sYtNn zXuXDl)fm`Is!0`tFy_y=&C%JXood`a8&tq`w;W;^qZQVSVpx4wv$d0(#y=!~4n~kh z&9JBKx7(A~Ed6b(|GhW*+^+Bn5%A~dI_}jlIH`HxI68BK9Ro4J+(R z+~n1jCLu_Zl~3@z9i5BAa_(Jr%QbD^Y{`o$7W_{`)m~Frl1q?8>o9*{Xz*tVa|n6x zYwFktDy|^u(8K~A32r!M>hc9U7yLUo)~7;rFfycbio#kmt08J_C%UQ1YWpsRO`M*6 zz%kU&@7vC#sRb$7&j>K`7&5iY9ZQN7vzde~Od%qJq?vzHj6hjJ?QT!8=yexL4s_>F zg07DJo6kEaQa8zsQhq2!NyskW;W9KFVhK&Sd2X)*n4EX-7pXhn@*GzDtHwA|Ax1YO zP)(uy>n0YWzx-&e?p$G&E< zT$a3%+LIxW@$}e0pP?+)1h-o$RGcSvf~IVqgyXQ}b-+-c#T!!acGMm_{W(e}eRgo% zb!CFue-)Rp&nF^HVKXKTw|b|R8wVo}Ef?uVie8VG8*{+9A{Ca&+6E-DXvNp#l}+i9De`jM@OIiII}4oxLO(=YoLU6+1%}A8>T#NCQD~wqg#0zN~pMEzYZe*@$31&Ek`rRk7x+&&F`lludXl3ejG`9cUuO0Ekz?wQTzd%U#9^Y#btR@ zo-769Y<|mX-66U>M-)!zr|3_nf%y3KXc(ybVF6Xb4C2vGfW#LI{!S|XTLwT&n+Z)> zUuV?sckG6{oll_k4Gq4B;Y^mLSXFM$hz5E^WS|78c(l{hY!wvB-0R@>ys@nTkOw^A z@dp1Qhfk9(`T%>8;PUfZ@mHd!1>n*T!8%PhfdDRE7J`6s(RAzXFZ!)5s*g(FGk3+F zviEu@Z>DP`b}v^*(-pBPXvq*=ZuQNeKLCbRX$Mgh&v5Uw1#&#J^t}!rO4xyz#2Xyx z{jgeZoOAoj=nNRP-Kl;@f)DbVv5qus6V5ZQr9^N)L+%p8Iy?cOgh>kTPlIs(Eg}07aTz$L=VjsDh*r?&c zaf2){lzKH(N92i~H|18MnS4w_Nz-G_oDQ3nt|Tkw07SCIDpztLBpK#v7JwnJbDX=@ zZx*J{&!FJAg^4@$x0Y_3BDC-PG3 z7vJFXHGB#Y0RsuHhCR;09QdZ##f`8nval$e>^p1Hm<#|{QdFT5OC1pQo)-3Kc5c;7 z=W(`qnbo^(7Pr5}S}5bNxn{DyKZ05I%f9WcW-rt7u(jUr|77rn!g4QwE(c)~sck7o zHSy=?CH_Z6%N8jlH~*4;(d$cMc)O+h+WMz!B{ONM?ywVVD zHDsaL{`ivN>597GtA?%efI=*gohOTF+JfcO^ny3R@v*F*2hTGfcku!~nh{p{o{`1y zui5XHDH>Sv!FfS1KT}lhtm;OhqH}ADVj6a9>Ej1=0)+2-w3F}Ke|M(5CBPlcsI=k8 z>>O}sDpF!OT2&1gW8r|Fu!7&9krVb=UP4jDxGr0^hnv-(>VX=6n{!IiqE`g~%*IDu zD0Yq$t22ruW@au2j!1$=<$i7!>M)%h2%RWX+LjnJ(h}4Q!EgQj)37CaNvJajsbs!V zxP;vgETt44Ncy^4gQI~8LMavlxXl;Nx{PGcTh9LAypCqE zsD)0d-4B^w;;6P;=uik%_N}(AUNj<|Sx#g_Y_o@d4)P#y&mKc;cYx_z?3S*Cqaq*| zI#0R1yaa#Kq(R!>L6epR`|EenIez|E7k^v*Hw%(4RrXH4#}oHTeXoXBjZ-ZDdogK1 z>GRh`byi#FU~@8C?;7j?T1`7)5T3rZUca#W>0M!XyRu=VXwAWGaoe`B5+|@D_Vpb) z#TJFiKtiuI#mapRC1dj+)+Td)hVnfoV12BxA=`cSvd`+d42kk~hyV5Gm)L1!Yw{Wz2)Qo3C+wq?!Z)mb z?_tjtSLZh|9U1(7Tl=5A+Ad47<@_IFQ#vqxAmPdTp?9kv2S&vS9o5=qBTzWLhdmtb zTUM@c7!Y`1C{Dzr09ZL6eDX;|&S7g~gYkmQW*jGinu?D?Mm~rB!gD#^>tij(+As#y*z%mWCuVy{A)md~ox+f!3i=d3k1gF^l=jl4Y?L57Bf}?|%qtiNf2`aTU<2Qh6cs*~ z*{oa<0aQ>?oT_B?&Y>UDPSSXNB96f89(X7H+Ss69=1o-YlX}wNFrUSOxL_0iKA~_$FG1(Kv(&rF@hC}4We}Q2UqARqkwqnn z6c+svukhd#7+|Gme&uM8cI4QfERe;1n5(!6zfDB)!(td=00pfHH7lDJ+*$6AhuIUT z4rmt|49(ofM!&jn#V}BDLt=~2=Xqy4tygbd8VRT?VF@-RU?}rdAU@k5XdPN;z@YC* zk%Mp{x01h1wILm>e4}~r$y)0Vr8^!rTm~?;Vy^3m1>%GHTRxRsx;9*fTZ>ag{=1S zwZFXMf0#CXBX-}o0=y7iuH>B~Ww81dy4l0+>fx3^VlO!43}YRCLEvO};SlHv(@qPg z8jQNr+NB8WTYau)+*btJ9P_1agNGW-%obOYY%c3dS*Ds!IIMJ14Pz`LV0VrJj|J#` zW?7waK?Zs+*#~7cLEEP00QK5qBd_|2_XaI)>UP}p%$*p}j%UArzIKV=?G^O<;9c6?R(xm;i;bj${ayz}#;pl1FT64_g&k}Et=~Y+42$j% z0f0kIZHNLC)$t0p8!Gbq=fxEvxaEmt>Zume;!UUN8>%kII`8ZaXG-D2_t`;<^*+Y_ zrN;mUYULIw384YrXzj)gUOs*E76t6P2NGV+QBG73pm?f)mfTx)1ZcaT?&vgEtq-8`7E~d#)yH0IL)qCLV7`!F0(D zvV8~a)6~R*Kh%pNq32mHV%+fIm3G)j%LA+;XJAVa^=!sKII=`2=w>HuyFB5GS0Gx{wtG(g4OWW3 zXjw&c2TKD~3|}=>{#X9yT7ZnSK&8cvs^=%=p%!e!ue^sP8E(I5fp|83AgKVI1>*bM z)52#sn!2J!7~Z7f!%>NEt?r3rqa=yGfF9;2$DG%H7A)`G$`M61`SXhq?onjRb@`G6 zN2_^M2h>%NyyvfSK%WiB|1iIR$=lD$LF76OaxeZ5}X4tk zY2G{sIH-VQjgnzXB-JV=n{}*Cfz2&GQmc`yxacA#%3o{;{oZ9b9`!{j@1jP>Im9BQ zns{>BKe;n?g+B{coU?Ba05KO5mf4E50#>tUcac+Vs;H@D$*dSLX)+huCXtl#^GO#Z zFrq5E!s1qWmOwOz{W%sL3k>-YOzG**B>^kx{YB_p&i8q@cJ~W2wZ{v=*0z}o z+DsNe$Vudtt_toOqxgdyGj>^xr6L$su}%NSG$I@=0o?U^taw<>RI%Qwu7Zlt*TE^u z`7J8R?b}Z%knu3r5KqIha)KOtyP<^p@>~36Ar&mxBmbH0>@CA9^NZJMpoxoS^|$3D z_6b|7!DXj2fQXB5)wSv{l9FM=q|tt7`RM*mMAW$w{Ej<``AGlQc;{>HemBF5>M=$z zX%4v91T3M|Fl1(}NA4!bh-F9fkQJLuA5zeF7}MfU8pv|Z2)oAWwV_Q(24LE(Sm|K z`_i1fY3G}X<6woLWeYivDH1CXGJ=qPWe4i}?g~5$^zIvW^mCCsV~n78kkpr}5zJ1! z;3_zCOn~OTOebC#V-i6V6b+y$f%CjyO5bCTTb+cV-z|P$z%CHj_=UjzLD<$;YiCqM zL;oWaqz>EQD7)K>B}Bo zrXu`fBOBCxIpn>gqvep%X4}-EI-RQ$oB#mX2aP9>UiLZWb;(3hf5e@3-Uj_hB4aaW zRzYOZvvI=dqXSDCh|4!EYJLPc&@isrKpoz>kPkw2cfR^0{KYCN!iPIJK1pf-57+vY zUEBF%8{k_a=5M5wQENplis3#Zzj7gt)k+g^6SitH!|X_omwppuSdjyVvH=G%3+mq@ z%A2CbcPd+?w|}{MZ%3fupqgXdtfm?1Ek%Yhb$i`{I&bXbzNbK6Td5I~&mw~mU-Qgj zlNQD2-ABA(S4!IRsEQes=ukUhbXBZN&u(#TxNV-*zQ0aH;8*EiZ2v;^t6gz{-7F1| w?2HsR@@9eAM&yeHH48QB|9{?0eGKQIhqAiSo@hlu0O_Tns`I4$vE{q}2hzn>CIA2c literal 0 HcmV?d00001 diff --git a/packages/desktop-electron/icons/dev/dock.png b/packages/desktop-electron/icons/dev/dock.png new file mode 100644 index 0000000000000000000000000000000000000000..4953d5531e751bac6b2ec3985a0f2a4b20e0cc86 GIT binary patch literal 50483 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 z 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

#0oVu`Pdp8_ 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 P1|aZs^>bP0l+XkKisKE- literal 0 HcmV?d00001 diff --git a/packages/desktop-electron/icons/prod/dock.png b/packages/desktop-electron/icons/prod/dock.png new file mode 100644 index 0000000000000000000000000000000000000000..f2ab694e97d9556e306bec4c76f54e8cdb83a938 GIT binary patch literal 38916 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 TpPc;$`gQiyxs#Q@U%mH#q;zt8 literal 0 HcmV?d00001 diff --git a/packages/desktop-electron/src/main/windows.ts b/packages/desktop-electron/src/main/windows.ts index 0b7783f289..170cd877cd 100644 --- a/packages/desktop-electron/src/main/windows.ts +++ b/packages/desktop-electron/src/main/windows.ts @@ -50,7 +50,8 @@ export function setTitlebar(win: BrowserWindow, theme: Partial = export function setDockIcon() { if (process.platform !== "darwin") return - app.dock?.setIcon(nativeImage.createFromPath(join(iconsDir(), "128x128@2x.png"))) + const icon = nativeImage.createFromPath(join(iconsDir(), "dock.png")) + if (!icon.isEmpty()) app.dock?.setIcon(icon) } export function createMainWindow(globals: Globals) { From e973bbf54a519566bfdccce3474178b26b163a6d Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 27 Mar 2026 14:11:50 -0400 Subject: [PATCH 008/138] fix(app): default file tree to closed with minimum width (#19426) --- packages/app/src/context/layout.tsx | 23 ++++++++++++----------- packages/app/src/pages/session.tsx | 9 +++++++++ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 78928118d7..640d5e02eb 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -13,7 +13,8 @@ import { createScrollPersistence, type SessionScroll } from "./layout-scroll" import { createPathHelpers } from "./file/path" const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const -const DEFAULT_PANEL_WIDTH = 344 +const DEFAULT_SIDEBAR_WIDTH = 344 +const DEFAULT_FILE_TREE_WIDTH = 200 const DEFAULT_SESSION_WIDTH = 600 const DEFAULT_TERMINAL_HEIGHT = 280 export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number] @@ -161,11 +162,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( if (!isRecord(fileTree)) return fileTree if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree - const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_PANEL_WIDTH + const width = typeof fileTree.width === "number" ? fileTree.width : DEFAULT_FILE_TREE_WIDTH return { ...fileTree, opened: true, - width: width === 260 ? DEFAULT_PANEL_WIDTH : width, + width: width === 260 ? DEFAULT_FILE_TREE_WIDTH : width, tab: "changes", } })() @@ -230,7 +231,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( createStore({ sidebar: { opened: false, - width: DEFAULT_PANEL_WIDTH, + width: DEFAULT_SIDEBAR_WIDTH, workspaces: {} as Record, workspacesDefault: false, }, @@ -243,8 +244,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( panelOpened: true, }, fileTree: { - opened: true, - width: DEFAULT_PANEL_WIDTH, + opened: false, + width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" as "changes" | "all", }, session: { @@ -628,32 +629,32 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, fileTree: { opened: createMemo(() => store.fileTree?.opened ?? true), - width: createMemo(() => store.fileTree?.width ?? DEFAULT_PANEL_WIDTH), + width: createMemo(() => store.fileTree?.width ?? DEFAULT_FILE_TREE_WIDTH), tab: createMemo(() => store.fileTree?.tab ?? "changes"), setTab(tab: "changes" | "all") { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab }) + setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab }) return } setStore("fileTree", "tab", tab) }, open() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) + setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", true) }, close() { if (!store.fileTree) { - setStore("fileTree", { opened: false, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) + setStore("fileTree", { opened: false, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", false) }, toggle() { if (!store.fileTree) { - setStore("fileTree", { opened: true, width: DEFAULT_PANEL_WIDTH, tab: "changes" }) + setStore("fileTree", { opened: true, width: DEFAULT_FILE_TREE_WIDTH, tab: "changes" }) return } setStore("fileTree", "opened", (x) => !x) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 752b549b86..11e6375b3b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1640,6 +1640,15 @@ export default function Page() { consumePendingMessage: layout.pendingMessage.consume, }) + createEffect( + on( + () => params.id, + (id) => { + if (!id) requestAnimationFrame(() => inputRef?.focus()) + }, + ), + ) + onMount(() => { document.addEventListener("keydown", handleKeyDown) }) From ff13524a531ebd196224551199c3cb0833c44c3c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 27 Mar 2026 20:55:03 +0100 Subject: [PATCH 009/138] fix flaky plugin tests (no mock.module for bun) (#19445) --- packages/opencode/test/cli/tui/thread.test.ts | 111 +++++++----------- packages/opencode/test/config/config.test.ts | 29 +++-- 2 files changed, 60 insertions(+), 80 deletions(-) diff --git a/packages/opencode/test/cli/tui/thread.test.ts b/packages/opencode/test/cli/tui/thread.test.ts index d3de7c3183..176c2575a3 100644 --- a/packages/opencode/test/cli/tui/thread.test.ts +++ b/packages/opencode/test/cli/tui/thread.test.ts @@ -1,7 +1,15 @@ -import { describe, expect, mock, test } from "bun:test" +import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" import fs from "fs/promises" import path from "path" import { tmpdir } from "../../fixture/fixture" +import * as App from "../../../src/cli/cmd/tui/app" +import { Rpc } from "../../../src/util/rpc" +import { UI } from "../../../src/cli/ui" +import * as Timeout from "../../../src/util/timeout" +import * as Network from "../../../src/cli/network" +import * as Win32 from "../../../src/cli/cmd/tui/win32" +import { TuiConfig } from "../../../src/config/tui" +import { Instance } from "../../../src/project/instance" const stop = new Error("stop") const seen = { @@ -9,81 +17,43 @@ const seen = { inst: [] as string[], } -mock.module("../../../src/cli/cmd/tui/app", () => ({ - tui: async (input: { directory: string }) => { - seen.tui.push(input.directory) +function setup() { + // Intentionally avoid mock.module() here: Bun keeps module overrides in cache + // and mock.restore() does not reset mock.module values. If this switches back + // to module mocks, later suites can see mocked @/config/tui and fail (e.g. + // plugin-loader tests expecting real TuiConfig.waitForDependencies). See: + // https://github.com/oven-sh/bun/issues/7823 and #12823. + spyOn(App, "tui").mockImplementation(async (input) => { + if (input.directory) seen.tui.push(input.directory) throw stop - }, -})) - -mock.module("@/util/rpc", () => ({ - Rpc: { - client: () => ({ - call: async () => ({ url: "http://127.0.0.1" }), - on: () => {}, - }), - }, -})) - -mock.module("@/cli/ui", () => ({ - UI: { - error: () => {}, - }, -})) - -mock.module("@/util/log", () => ({ - Log: { - init: async () => {}, - create: () => ({ - error: () => {}, - info: () => {}, - warn: () => {}, - debug: () => {}, - time: () => ({ stop: () => {} }), - }), - Default: { - error: () => {}, - info: () => {}, - warn: () => {}, - debug: () => {}, - }, - }, -})) - -mock.module("@/util/timeout", () => ({ - withTimeout: (input: Promise) => input, -})) - -mock.module("@/cli/network", () => ({ - withNetworkOptions: (input: T) => input, - resolveNetworkOptions: async () => ({ + }) + spyOn(Rpc, "client").mockImplementation(() => ({ + call: async () => ({ url: "http://127.0.0.1" }) as never, + on: () => () => {}, + })) + spyOn(UI, "error").mockImplementation(() => {}) + spyOn(Timeout, "withTimeout").mockImplementation((input) => input) + spyOn(Network, "resolveNetworkOptions").mockResolvedValue({ mdns: false, port: 0, hostname: "127.0.0.1", - }), -})) - -mock.module("../../../src/cli/cmd/tui/win32", () => ({ - win32DisableProcessedInput: () => {}, - win32InstallCtrlCGuard: () => undefined, -})) - -mock.module("@/config/tui", () => ({ - TuiConfig: { - get: () => ({}), - }, -})) - -mock.module("@/project/instance", () => ({ - Instance: { - provide: async (input: { directory: string; fn: () => Promise | unknown }) => { - seen.inst.push(input.directory) - return input.fn() - }, - }, -})) + mdnsDomain: "opencode.local", + cors: [], + }) + spyOn(Win32, "win32DisableProcessedInput").mockImplementation(() => {}) + spyOn(Win32, "win32InstallCtrlCGuard").mockReturnValue(undefined) + spyOn(TuiConfig, "get").mockResolvedValue({}) + spyOn(Instance, "provide").mockImplementation(async (input) => { + seen.inst.push(input.directory) + return input.fn() + }) +} describe("tui thread", () => { + afterEach(() => { + mock.restore() + }) + async function call(project?: string) { const { TuiThreadCommand } = await import("../../../src/cli/cmd/tui/thread") const args: Parameters>[0] = { @@ -107,6 +77,7 @@ describe("tui thread", () => { } async function check(project?: string) { + setup() await using tmp = await tmpdir({ git: true }) const cwd = process.cwd() const pwd = process.env.PWD diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index aa49aa4bd5..ea0a545200 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -821,9 +821,12 @@ test("dedupes concurrent config dependency installs for the same dir", async () }) const online = spyOn(Network, "online").mockReturnValue(false) const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => { - calls += 1 - start() - await gate + const hit = path.normalize(opts?.cwd ?? "") === path.normalize(dir) + if (hit) { + calls += 1 + start() + await gate + } const mod = path.join(opts?.cwd ?? "", "node_modules", "@opencode-ai", "plugin") await fs.mkdir(mod, { recursive: true }) await Filesystem.write( @@ -883,12 +886,16 @@ test("serializes config dependency installs across dirs", async () => { const online = spyOn(Network, "online").mockReturnValue(false) const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => { - calls += 1 - open += 1 - peak = Math.max(peak, open) - if (calls === 1) { - start() - await gate + const cwd = path.normalize(opts?.cwd ?? "") + const hit = cwd === path.normalize(a) || cwd === path.normalize(b) + if (hit) { + calls += 1 + open += 1 + peak = Math.max(peak, open) + if (calls === 1) { + start() + await gate + } } const mod = path.join(opts?.cwd ?? "", "node_modules", "@opencode-ai", "plugin") await fs.mkdir(mod, { recursive: true }) @@ -896,7 +903,9 @@ test("serializes config dependency installs across dirs", async () => { path.join(mod, "package.json"), JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }), ) - open -= 1 + if (hit) { + open -= 1 + } return { code: 0, stdout: Buffer.alloc(0), From 6f5b70e681b3a257c01fae1df4dbfe555cd216ef Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:19:51 -0500 Subject: [PATCH 010/138] tweak: add additional overflow error patterns (#19446) --- packages/opencode/src/provider/error.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opencode/src/provider/error.ts b/packages/opencode/src/provider/error.ts index 7a171f4dbb..52e525177a 100644 --- a/packages/opencode/src/provider/error.ts +++ b/packages/opencode/src/provider/error.ts @@ -23,6 +23,9 @@ export namespace ProviderError { /request entity too large/i, // HTTP 413 /context length is only \d+ tokens/i, // vLLM /input length.*exceeds.*context length/i, // vLLM + /prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error + /too large for model with \d+ maximum context length/i, // Mistral + /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text ] function isOpenAiErrorRetryable(e: APICallError) { From 7a7643c86a69edbd79f99b0c4f613463627f2428 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 27 Mar 2026 21:21:15 +0100 Subject: [PATCH 011/138] no theme override in dev (#19456) --- .opencode/tui.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.opencode/tui.json b/.opencode/tui.json index f228c20886..1eee01b302 100644 --- a/.opencode/tui.json +++ b/.opencode/tui.json @@ -1,6 +1,5 @@ { "$schema": "https://opencode.ai/tui.json", - "theme": "smoke-theme", "plugin": [ [ "./plugins/tui-smoke.tsx", From c33d9996f0e630d15b6e40b9a1feb578e991561a Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:24:30 -0500 Subject: [PATCH 012/138] feat: AI SDK v6 support (#18433) --- bun.lock | 246 ++---- package.json | 8 +- packages/console/function/package.json | 6 +- packages/opencode/package.json | 44 +- packages/opencode/src/provider/provider.ts | 73 +- ...vert-to-openai-compatible-chat-messages.ts | 14 +- .../map-openai-compatible-finish-reason.ts | 8 +- .../openai-compatible-chat-language-model.ts | 113 ++- .../openai-compatible-metadata-extractor.ts | 6 +- .../chat/openai-compatible-prepare-tools.ts | 18 +- .../provider/sdk/copilot/copilot-provider.ts | 10 +- .../convert-to-openai-responses-input.ts | 46 +- .../map-openai-responses-finish-reason.ts | 6 +- .../responses/openai-responses-api-types.ts | 7 + .../openai-responses-language-model.ts | 187 +++-- .../openai-responses-prepare-tools.ts | 18 +- .../responses/tool/code-interpreter.ts | 5 +- .../sdk/copilot/responses/tool/file-search.ts | 5 +- .../responses/tool/image-generation.ts | 5 +- .../sdk/copilot/responses/tool/local-shell.ts | 5 +- .../responses/tool/web-search-preview.ts | 5 +- .../sdk/copilot/responses/tool/web-search.ts | 5 +- packages/opencode/src/provider/transform.ts | 78 +- packages/opencode/src/session/compaction.ts | 2 +- packages/opencode/src/session/llm.ts | 17 +- packages/opencode/src/session/message-v2.ts | 17 +- packages/opencode/src/session/prompt.ts | 67 +- .../copilot/copilot-chat-model.test.ts | 28 +- .../opencode/test/provider/gitlab-duo.test.ts | 784 +++++++++--------- packages/opencode/test/session/llm.test.ts | 127 ++- .../opencode/test/session/message-v2.test.ts | 52 +- .../test/session/structured-output.test.ts | 17 +- patches/@ai-sdk%2Fanthropic@3.0.64.patch | 119 +++ patches/@ai-sdk%2Fprovider-utils@4.0.21.patch | 61 ++ patches/@ai-sdk%2Fxai@2.0.51.patch | 108 --- .../@openrouter%2Fai-sdk-provider@1.5.4.patch | 128 --- 36 files changed, 1290 insertions(+), 1155 deletions(-) create mode 100644 patches/@ai-sdk%2Fanthropic@3.0.64.patch create mode 100644 patches/@ai-sdk%2Fprovider-utils@4.0.21.patch delete mode 100644 patches/@ai-sdk%2Fxai@2.0.51.patch delete mode 100644 patches/@openrouter%2Fai-sdk-provider@1.5.4.patch diff --git a/bun.lock b/bun.lock index 1ff4b4f728..783abe2893 100644 --- a/bun.lock +++ b/bun.lock @@ -142,9 +142,9 @@ "name": "@opencode-ai/console-function", "version": "1.3.3", "dependencies": { - "@ai-sdk/anthropic": "2.0.0", - "@ai-sdk/openai": "2.0.2", - "@ai-sdk/openai-compatible": "1.0.1", + "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/openai": "3.0.48", + "@ai-sdk/openai-compatible": "2.0.37", "@hono/zod-validator": "catalog:", "@openauthjs/openauth": "0.0.0-20250322224806", "@opencode-ai/console-core": "workspace:*", @@ -305,25 +305,25 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.82", - "@ai-sdk/anthropic": "2.0.65", - "@ai-sdk/azure": "2.0.91", - "@ai-sdk/cerebras": "1.0.36", - "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.36", - "@ai-sdk/gateway": "2.0.30", - "@ai-sdk/google": "2.0.54", - "@ai-sdk/google-vertex": "3.0.106", - "@ai-sdk/groq": "2.0.34", - "@ai-sdk/mistral": "2.0.27", - "@ai-sdk/openai": "2.0.89", - "@ai-sdk/openai-compatible": "1.0.32", - "@ai-sdk/perplexity": "2.0.23", - "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.21", - "@ai-sdk/togetherai": "1.0.34", - "@ai-sdk/vercel": "1.0.33", - "@ai-sdk/xai": "2.0.51", + "@ai-sdk/amazon-bedrock": "4.0.83", + "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/azure": "3.0.49", + "@ai-sdk/cerebras": "2.0.41", + "@ai-sdk/cohere": "3.0.27", + "@ai-sdk/deepinfra": "2.0.41", + "@ai-sdk/gateway": "3.0.80", + "@ai-sdk/google": "3.0.53", + "@ai-sdk/google-vertex": "4.0.95", + "@ai-sdk/groq": "3.0.31", + "@ai-sdk/mistral": "3.0.27", + "@ai-sdk/openai": "3.0.48", + "@ai-sdk/openai-compatible": "2.0.37", + "@ai-sdk/perplexity": "3.0.26", + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.21", + "@ai-sdk/togetherai": "2.0.41", + "@ai-sdk/vercel": "2.0.39", + "@ai-sdk/xai": "3.0.74", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/platform-node": "catalog:", @@ -337,7 +337,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "1.5.4", + "@openrouter/ai-sdk-provider": "2.3.3", "@opentui/core": "0.1.90", "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", @@ -347,7 +347,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "ai-gateway-provider": "2.3.1", + "ai-gateway-provider": "3.1.2", "bonjour-service": "1.3.0", "bun-pty": "0.4.8", "chokidar": "4.0.3", @@ -358,7 +358,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "5.3.3", + "gitlab-ai-provider": "6.0.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -599,10 +599,10 @@ "tree-sitter-bash", ], "patchedDependencies": { - "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", - "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch", + "@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch", }, "overrides": { "@types/bun": "catalog:", @@ -629,7 +629,7 @@ "@types/node": "22.13.9", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20251207.1", - "ai": "5.0.124", + "ai": "6.0.138", "diff": "8.0.2", "dompurify": "3.3.1", "drizzle-kit": "1.0.0-beta.19-d95b7a4", @@ -673,51 +673,51 @@ "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.14.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-b6r3PS3Nly+Wyw9U+0nOr47bV8tfS476EgyEMhoKvJCZLbgqoDFN7DJwkxL88RR0aiOqOYV1ZnESHqb+RmdH8w=="], - "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.82", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yb1EkRCMWex0tnpHPLGQxoJEiJvMGOizuxzlXFOpuGFiYgE679NsWE/F8pHwtoAWsqLlylgGAJvJDIJ8us8LEw=="], + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], - "@ai-sdk/azure": ["@ai-sdk/azure@2.0.91", "", { "dependencies": { "@ai-sdk/openai": "2.0.89", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9tznVSs6LGQNKKxb8pKd7CkBV9yk+a/ENpFicHCj2CmBUKefxzwJ9JbUqrlK3VF6dGZw3LXq0dWxt7/Yekaj1w=="], + "@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], - "@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zoJYL33+ieyd86FSP0Whm86D79d1lKPR7wUzh1SZ1oTxwYmsGyvIrmMf2Ll0JA9Ds2Es6qik4VaFCrjwGYRTIQ=="], + "@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kDMEpjaRdRXIUi1EH8WHwLRahyDTYv9SAJnP6VCCeq8X+tVqZbMLCqqxSG5dRknrI65ucjvzQt+FiDKTAa7AHg=="], - "@ai-sdk/cohere": ["@ai-sdk/cohere@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yJ9kP5cEDJwo8qpITq5TQFD8YNfNtW+HbyvWwrKMbFzmiMvIZuk95HIaFXE7PCTuZsqMA05yYu+qX/vQ3rNKjA=="], + "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OqcCq2PiFY1dbK/0Ck45KuvE8jfdxRuuAE9Y5w46dAk6U+9vPOeg1CDcmR+ncqmrYrhRl3nmyDttyDahyjCzAw=="], - "@ai-sdk/deepgram": ["@ai-sdk/deepgram@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-E+wzGPSa/XHmajO3WtX8mtq0ewy04tsHSpU6/SGwqbiykwWba/emi7ayZ4ir89s5OzbAen2g7T9zZiEchMfkHQ=="], + "@ai-sdk/deepgram": ["@ai-sdk/deepgram@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-htT1Y7vBN0cRu/1pGnhx6DNH3xaNr0o0MjDkmii48X2+6S/WkOzVNtMjn7V3vLWEQIWNio5vw1hG/F43K8WLHA=="], - "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@1.0.36", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.33", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LndvRktEgY2IFu4peDJMEXcjhHEEFtM0upLx/J64kCpFHCifalXpK4PPSX3PVndnn0bJzvamO5+fc0z2ooqBZw=="], + "@ai-sdk/deepinfra": ["@ai-sdk/deepinfra@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-y6RoOP7DGWmDSiSxrUSt5p18sbz+Ixe5lMVPmdE7x+Tr5rlrzvftyHhjWHfqlAtoYERZTGFbP6tPW1OfQcrb4A=="], - "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.35", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qvh2yxL5zJS9RO/Bf12pyYBIDmn+9GR1hT6e28IYWQWnt2Xq0h9XGps6XagLAv3VYYFg8c/ozkWVd4kXLZ25HA=="], + "@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4vOEekW4TAYVHN0qgiwoUOQZhguGwZBiEw8LDeUmpWBm07QkLRAtxYCaSoMiA4hZZojao5mj6NRGEBW1CnDPtg=="], - "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@1.0.24", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ee2At5jgV+SqC6nrtPq20iH7N/aN+O36LrA4gkzVM4cmhM7bvQKVkOXhC1XxG+wsYG6UZi3Nekoi8MEjNWuRrw=="], + "@ai-sdk/elevenlabs": ["@ai-sdk/elevenlabs@2.0.24", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K+1YprVMO8R6vTcNhqTqUWhOzX5V/hEY0pFx9KQL0/+MJjOgRi6DcOLoNBd7ONcjxYTyiFLRfk/0a/pHTtSgFA=="], - "@ai-sdk/fireworks": ["@ai-sdk/fireworks@1.0.35", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.34", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-inUq29XvSVDer6JIeOkwAmCFxOtHPU0OZEhwaWoe3PI59naHIW4RIFA9wppLLV5fJI9WQcAfDKy0ZHW9nV3UJw=="], + "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.40", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ARjygiBQtVSgNBp3Sag+Bkwn68ub+cZPC05UpRGG+VY8/Q896K2yU1j4I0+S1eU0BQW/9DKbRG04d9Ayi2DUmA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5Nrkj8B4MzkkOfjjA+Cs5pamkbkK4lI11bx80QV7TFcen/hWA8wEC+UVzwuM5H2zpekoNMjvl6GonHnR62XIZw=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.80", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uM7kpZB5l977lW7+2X1+klBUxIZQ78+1a9jHlaHFEzcOcmmslTl3sdP0QqfuuBcO0YBM2gwOiqVdp8i4TRQYcw=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VKguP0x/PUYpdQyuA/uy5pDGJy6reL0X/yDKxHfL207aCUXpFIBmyMhVs4US39dkEVhtmIFSwXauY0Pt170JRw=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uz8tIlkDgQJG9Js2Wh9JHzd4kI9+hYJqf9XXJLx60vyN5mRIqhr49iwR5zGP5Gl8odp2PeR3Gh2k+5bh3Z1HHw=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.106", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/google": "2.0.54", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-f9sA66bmhgJoTwa+pHWFSdYxPa0lgdQ/MgYNxZptzVyGptoziTf1a9EIXEL3jiCD0qIBAg+IhDAaYalbvZaDqQ=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.95", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/google": "3.0.53", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xL44fHlTtDM7RLkMTgyqMfkfthA38JS91bbMaHItObIhte1PAIY936ZV1PLl/Z9A/oBAXjHWbXo5xDoHzB7LEg=="], - "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], + "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gaptHgaXjMw3+eA0Q4FABcsj5nQNP6EpFaGUR+Pj5WJy7Kn6mApl975/x57224MfeJIShNpt8wFKK3tvh5ewKg=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], - "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-luHVcU+yKzwv3ekKgbP3v+elUVxb2Rt+8c6w9qi7g2NYG2/pEL21oIrnaEnc6UtTZLLZX9EFBcpq2N1FQKDIMw=="], + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw=="], - "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aiaRvnc6mhQZKhTTSXPCjPH8Iqr5D/PfCN1hgVP/3RGTBbJtsd9HemIBSABeSdAKbsMH/PwJxgnqH75HEamcBA=="], + "@ai-sdk/perplexity": ["@ai-sdk/perplexity@3.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dXzrVsLR5f6tr+U04jq4AXoRroGFBTvODnLgss0SWbzNjGGQg3XqtQ9j7rCLo6o8qbYGuAHvqUrIpUCuiscuFg=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.34", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jjJmJms6kdEc4nC3MDGFJfhV8F1ifY4nolV2dbnT7BM4ab+Wkskc0GwCsJ7G7WdRMk7xDbFh4he3DPL8KJ/cyA=="], + "@ai-sdk/togetherai": ["@ai-sdk/togetherai@2.0.41", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-k3p9e3k0/gpDDyTtvafsK4HYR4D/aUQW/kzCwWo1+CzdBU84i4L14gWISC/mv6tgSicMXHcEUd521fPufQwNlg=="], - "@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.32", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qwjm+HdwKasu7L9bDUryBMGKDMscIEzMUkjw/33uGdJpktzyNW13YaNIObOZ2HkskqDMIQJSd4Ao2BBT8fEYLw=="], + "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], - "@ai-sdk/xai": ["@ai-sdk/xai@2.0.51", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AI3le03qiegkZvn9hpnpDwez49lOvQLj4QUBT8H41SMbrdTYOxn3ktTwrsSu90cNDdzKGMvoH0u2GHju1EdnCg=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.74", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HDDLsT+QrzE3c2QZLRV/HKAwMtXDb0PMDdk1PYUXLJ3r9Qv76zGKGyvJLX7Pu6c8TOHD1mwLrOVYrsTpC/eTMw=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -1455,9 +1455,7 @@ "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.4", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-xrSQPUIH8n9zuyYZR0XK7Ba0h2KsjJcMkxnwaYfmv13pKs3sDkjPzVPPhlhzqBGddHb5cFEwJ9VFuFeDcxCDSw=="], - - "@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -2271,9 +2269,9 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@5.0.124", "", { "dependencies": { "@ai-sdk/gateway": "2.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Li6Jw9F9qsvFJXZPBfxj38ddP2iURCnMs96f9Q3OeQzrDVcl1hvtwSEAuxA/qmfh6SDV2ERqFUOFzigvr0697g=="], + "ai": ["ai@6.0.138", "", { "dependencies": { "@ai-sdk/gateway": "3.0.80", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-49OfPe0f5uxJ6jUdA5BBXjIinP6+ZdYfAtpF2aEH64GA5wPcxH2rf/TBUQQ0bbamBz/D+TLMV18xilZqOC+zaA=="], - "ai-gateway-provider": ["ai-gateway-provider@2.3.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.19", "ai": "^5.0.116" }, "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^3.0.71", "@ai-sdk/anthropic": "^2.0.56", "@ai-sdk/azure": "^2.0.90", "@ai-sdk/cerebras": "^1.0.33", "@ai-sdk/cohere": "^2.0.21", "@ai-sdk/deepgram": "^1.0.21", "@ai-sdk/deepseek": "^1.0.32", "@ai-sdk/elevenlabs": "^1.0.21", "@ai-sdk/fireworks": "^1.0.30", "@ai-sdk/google": "^2.0.51", "@ai-sdk/google-vertex": "3.0.90", "@ai-sdk/groq": "^2.0.33", "@ai-sdk/mistral": "^2.0.26", "@ai-sdk/openai": "^2.0.88", "@ai-sdk/perplexity": "^2.0.22", "@ai-sdk/xai": "^2.0.42", "@openrouter/ai-sdk-provider": "^1.5.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^1.0.29" } }, "sha512-PqI6TVNEDNwr7kOhy7XUGnA8XJB1SpeA9aLqGjr0CyWkKgH+y+ofPm8MZGZ74DOwVejDF+POZq0Qs9jKEKUeYg=="], + "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -3049,7 +3047,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@5.3.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-k0kRUoAhDvoRC28hQW4sPp+A3cfpT5c/oL9Ng10S0oBiF2Tci1AtsX1iclJM5Os8C1nIIAXBW8LMr0GY7rwcGA=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.0.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-683GcJdrer/GhnljkbVcGsndCEhvGB8f9fUdCxQBlkuyt8rzf0G9DpSh+iMBYp9HpcSvYmYG0Qv5ks9dLrNxwQ=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -4799,63 +4797,21 @@ "@actions/http-client/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], + "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], - "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], + "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], + "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], + "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="], - "@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/deepgram/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - - "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2KMcR2xAul3u5dGZD7gONgbIki3Hg7Ey+sFu7gsiJ4U2iRU0GDV3ccNq79dTuAEXPDFcOWCUpW8A8jXc0kxJxQ=="], - - "@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - - "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="], - - "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - - "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], - - "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], - - "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - - "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], + "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -5329,16 +5285,6 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "ai-gateway-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], - - "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.90", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.56", "@ai-sdk/google": "2.0.46", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-C9MLe1KZGg1ZbupV2osygHtL5qngyCDA6ATatunyfTbIe8TXKG8HGni/3O6ifbnI5qxTidIn150Ox7eIFZVMYg=="], - - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - - "ai-gateway-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AnGoxVNZ/E3EU4lW12rrufI6riqL2cEv4jk3OrjJ/i54XwR0CJU1V26jXAwxb+Pc+uZmYG++HM+gzXxPQZkMNQ=="], - "ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -5557,12 +5503,6 @@ "nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.65", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-HqTPP59mLQ9U6jXQcx6EORkdc5FyZu34Sitkg6jNpyMYcRjStvfx4+NWq/qaR+OTwBFcccv8hvVii0CYkH2Lag=="], - - "opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4+qWkBCbL9HPKbgrUO/F2uXZ8GqrYxHa8SWEYIzxEJ9zvWw3ISr3t1/27O1i8MGSym+PzEyHBT48EV4LAwWaEw=="], - - "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-YspqqyJPzHjqWrjt4y/Wgc2aJgCcQj5uIJgZpq2Ar/lH30cEVhgE+keePDbjKpetD9UwNggCj7u6kO3unS23OQ=="], - "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "opencode-poe-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -5739,16 +5679,6 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/anthropic/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - - "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -5757,28 +5687,6 @@ "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - - "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -6211,20 +6119,6 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.46", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8PK6u4sGE/kXebd7ZkTp+0aya4kNqzoqpS5m7cHY2NfTK6fhPc6GNvE+MZIZIoHQTp5ed86wGBdeBPpFaaUtyg=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="], - - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], - - "ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -6321,10 +6215,6 @@ "opencode-poe-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - - "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], - "opencontrol/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "opencontrol/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -6581,12 +6471,6 @@ "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="], - "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], @@ -6639,10 +6523,6 @@ "js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "opencode/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], diff --git a/package.json b/package.json index 40ab8ceaf6..2b1c15fb69 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", "effect": "4.0.0-beta.37", - "ai": "5.0.124", + "ai": "6.0.138", "hono": "4.10.7", "hono-openapi": "1.1.2", "fuzzysort": "3.1.0", @@ -113,8 +113,8 @@ }, "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", - "@ai-sdk/xai@2.0.51": "patches/@ai-sdk%2Fxai@2.0.51.patch", - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" + "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@ai-sdk/provider-utils@4.0.21": "patches/@ai-sdk%2Fprovider-utils@4.0.21.patch", + "@ai-sdk/anthropic@3.0.64": "patches/@ai-sdk%2Fanthropic@3.0.64.patch" } } diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 4fa2d2a2d2..389583edba 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -17,9 +17,9 @@ "@typescript/native-preview": "catalog:" }, "dependencies": { - "@ai-sdk/anthropic": "2.0.0", - "@ai-sdk/openai": "2.0.2", - "@ai-sdk/openai-compatible": "1.0.1", + "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/openai": "3.0.48", + "@ai-sdk/openai-compatible": "2.0.37", "@hono/zod-validator": "catalog:", "@opencode-ai/console-core": "workspace:*", "@opencode-ai/console-resource": "workspace:*", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0f3d7d4deb..b6226bac51 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -68,25 +68,25 @@ "@actions/core": "1.11.1", "@actions/github": "6.0.1", "@agentclientprotocol/sdk": "0.14.1", - "@ai-sdk/amazon-bedrock": "3.0.82", - "@ai-sdk/anthropic": "2.0.65", - "@ai-sdk/azure": "2.0.91", - "@ai-sdk/cerebras": "1.0.36", - "@ai-sdk/cohere": "2.0.22", - "@ai-sdk/deepinfra": "1.0.36", - "@ai-sdk/gateway": "2.0.30", - "@ai-sdk/google": "2.0.54", - "@ai-sdk/google-vertex": "3.0.106", - "@ai-sdk/groq": "2.0.34", - "@ai-sdk/mistral": "2.0.27", - "@ai-sdk/openai": "2.0.89", - "@ai-sdk/openai-compatible": "1.0.32", - "@ai-sdk/perplexity": "2.0.23", - "@ai-sdk/provider": "2.0.1", - "@ai-sdk/provider-utils": "3.0.21", - "@ai-sdk/togetherai": "1.0.34", - "@ai-sdk/vercel": "1.0.33", - "@ai-sdk/xai": "2.0.51", + "@ai-sdk/amazon-bedrock": "4.0.83", + "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/azure": "3.0.49", + "@ai-sdk/cerebras": "2.0.41", + "@ai-sdk/cohere": "3.0.27", + "@ai-sdk/deepinfra": "2.0.41", + "@ai-sdk/gateway": "3.0.80", + "@ai-sdk/google": "3.0.53", + "@ai-sdk/google-vertex": "4.0.95", + "@ai-sdk/groq": "3.0.31", + "@ai-sdk/mistral": "3.0.27", + "@ai-sdk/openai": "3.0.48", + "@ai-sdk/openai-compatible": "2.0.37", + "@ai-sdk/perplexity": "3.0.26", + "@ai-sdk/provider": "3.0.8", + "@ai-sdk/provider-utils": "4.0.21", + "@ai-sdk/togetherai": "2.0.41", + "@ai-sdk/vercel": "2.0.39", + "@ai-sdk/xai": "3.0.74", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/platform-node": "catalog:", @@ -100,7 +100,7 @@ "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", - "@openrouter/ai-sdk-provider": "1.5.4", + "@openrouter/ai-sdk-provider": "2.3.3", "@opentui/core": "0.1.90", "@opentui/solid": "0.1.90", "@parcel/watcher": "2.5.1", @@ -110,7 +110,7 @@ "@standard-schema/spec": "1.0.0", "@zip.js/zip.js": "2.7.62", "ai": "catalog:", - "ai-gateway-provider": "2.3.1", + "ai-gateway-provider": "3.1.2", "bonjour-service": "1.3.0", "bun-pty": "0.4.8", "chokidar": "4.0.3", @@ -121,7 +121,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "5.3.3", + "gitlab-ai-provider": "6.0.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6ab45d028b..7fb3166284 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -9,6 +9,7 @@ import { BunProc } from "../bun" import { Hash } from "../util/hash" import { Plugin } from "../plugin" import { NamedError } from "@opencode-ai/util/error" +import { type LanguageModelV3 } from "@ai-sdk/provider" import { ModelsDev } from "./models" import { Auth } from "../auth" import { Env } from "../env" @@ -28,7 +29,7 @@ import { createVertex } from "@ai-sdk/google-vertex" import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic" import { createOpenAI } from "@ai-sdk/openai" import { createOpenAICompatible } from "@ai-sdk/openai-compatible" -import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider" +import { createOpenRouter } from "@openrouter/ai-sdk-provider" import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/copilot" import { createXai } from "@ai-sdk/xai" import { createMistral } from "@ai-sdk/mistral" @@ -109,7 +110,11 @@ export namespace Provider { }) } - const BUNDLED_PROVIDERS: Record SDK> = { + type BundledSDK = { + languageModel(modelId: string): LanguageModelV3 + } + + const BUNDLED_PROVIDERS: Record BundledSDK> = { "@ai-sdk/amazon-bedrock": createAmazonBedrock, "@ai-sdk/anthropic": createAnthropic, "@ai-sdk/azure": createAzure, @@ -130,7 +135,6 @@ export namespace Provider { "@ai-sdk/perplexity": createPerplexity, "@ai-sdk/vercel": createVercel, "gitlab-ai-provider": createGitLab, - // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } @@ -591,7 +595,12 @@ export namespace Provider { if (!result.models.length) { log.info("gitlab model discovery skipped: no models found", { - project: result.project ? { id: result.project.id, path: result.project.pathWithNamespace } : null, + project: result.project + ? { + id: result.project.id, + path: result.project.pathWithNamespace, + } + : null, }) return {} } @@ -619,8 +628,20 @@ export namespace Provider { 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 }, + input: { + text: true, + audio: false, + image: true, + video: false, + pdf: true, + }, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, interleaved: false, }, release_date: "", @@ -930,17 +951,17 @@ export namespace Provider { } const providers: Record = {} as Record - const languages = new Map() + const languages = new Map() const modelLoaders: { [providerID: string]: CustomModelLoader } = {} const varsLoaders: { [providerID: string]: CustomVarsLoader } = {} + const sdk = new Map() const discoveryLoaders: { [providerID: string]: CustomDiscoverModels } = {} - const sdk = new Map() log.info("init") @@ -1232,7 +1253,13 @@ export namespace Provider { ...model.headers, } - const key = Hash.fast(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) + const key = Hash.fast( + JSON.stringify({ + providerID: model.providerID, + npm: model.api.npm, + options, + }), + ) const existing = s.sdk.get(key) if (existing) return existing @@ -1285,7 +1312,10 @@ export namespace Provider { const bundledFn = BUNDLED_PROVIDERS[model.api.npm] if (bundledFn) { - log.info("using bundled provider", { providerID: model.providerID, pkg: model.api.npm }) + log.info("using bundled provider", { + providerID: model.providerID, + pkg: model.api.npm, + }) const loaded = bundledFn({ name: model.providerID, ...options, @@ -1325,7 +1355,10 @@ export namespace Provider { const provider = s.providers[providerID] if (!provider) { const availableProviders = Object.keys(s.providers) - const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 }) + const matches = fuzzysort.go(providerID, availableProviders, { + limit: 3, + threshold: -10000, + }) const suggestions = matches.map((m) => m.target) throw new ModelNotFoundError({ providerID, modelID, suggestions }) } @@ -1333,14 +1366,17 @@ export namespace Provider { const info = provider.models[modelID] if (!info) { const availableModels = Object.keys(provider.models) - const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 }) + const matches = fuzzysort.go(modelID, availableModels, { + limit: 3, + threshold: -10000, + }) const suggestions = matches.map((m) => m.target) throw new ModelNotFoundError({ providerID, modelID, suggestions }) } return info } - export async function getLanguage(model: Model): Promise { + export async function getLanguage(model: Model): Promise { const s = await state() const key = `${model.providerID}/${model.id}` if (s.models.has(key)) return s.models.get(key)! @@ -1350,7 +1386,10 @@ export namespace Provider { try { const language = s.modelLoaders[model.providerID] - ? await s.modelLoaders[model.providerID](sdk, model.api.id, { ...provider.options, ...model.options }) + ? await s.modelLoaders[model.providerID](sdk, model.api.id, { + ...provider.options, + ...model.options, + }) : sdk.languageModel(model.api.id) s.models.set(key, language) return language @@ -1457,9 +1496,9 @@ export namespace Provider { if (cfg.model) return parseModel(cfg.model) const providers = await list() - const recent = (await Filesystem.readJson<{ recent?: { providerID: ProviderID; modelID: ModelID }[] }>( - path.join(Global.Path.state, "model.json"), - ) + const recent = (await Filesystem.readJson<{ + recent?: { providerID: ProviderID; modelID: ModelID }[] + }>(path.join(Global.Path.state, "model.json")) .then((x) => (Array.isArray(x.recent) ? x.recent : [])) .catch(() => [])) as { providerID: ProviderID; modelID: ModelID }[] for (const entry of recent) { diff --git a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts index e1e3ed4c20..c4e15e0b4f 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts @@ -1,16 +1,16 @@ import { - type LanguageModelV2Prompt, - type SharedV2ProviderMetadata, + type LanguageModelV3Prompt, + type SharedV3ProviderOptions, UnsupportedFunctionalityError, } from "@ai-sdk/provider" import type { OpenAICompatibleChatPrompt } from "./openai-compatible-api-types" import { convertToBase64 } from "@ai-sdk/provider-utils" -function getOpenAIMetadata(message: { providerOptions?: SharedV2ProviderMetadata }) { +function getOpenAIMetadata(message: { providerOptions?: SharedV3ProviderOptions }) { return message?.providerOptions?.copilot ?? {} } -export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Prompt): OpenAICompatibleChatPrompt { +export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV3Prompt): OpenAICompatibleChatPrompt { const messages: OpenAICompatibleChatPrompt = [] for (const { role, content, ...message } of prompt) { const metadata = getOpenAIMetadata({ ...message }) @@ -127,6 +127,9 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro case "tool": { for (const toolResponse of content) { + if (toolResponse.type === "tool-approval-response") { + continue + } const output = toolResponse.output let contentValue: string @@ -135,6 +138,9 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro case "error-text": contentValue = output.value break + case "execution-denied": + contentValue = output.reason ?? "Tool execution denied." + break case "content": case "json": case "error-json": diff --git a/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts b/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts index 82e2ca02e9..7186b62af9 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts @@ -1,6 +1,8 @@ -import type { LanguageModelV2FinishReason } from "@ai-sdk/provider" +import type { LanguageModelV3FinishReason } from "@ai-sdk/provider" -export function mapOpenAICompatibleFinishReason(finishReason: string | null | undefined): LanguageModelV2FinishReason { +export function mapOpenAICompatibleFinishReason( + finishReason: string | null | undefined, +): LanguageModelV3FinishReason["unified"] { switch (finishReason) { case "stop": return "stop" @@ -12,6 +14,6 @@ export function mapOpenAICompatibleFinishReason(finishReason: string | null | un case "tool_calls": return "tool-calls" default: - return "unknown" + return "other" } } diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts index c85d3f3d17..280970c41b 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts @@ -1,12 +1,12 @@ import { APICallError, InvalidResponseDataError, - type LanguageModelV2, - type LanguageModelV2CallWarning, - type LanguageModelV2Content, - type LanguageModelV2FinishReason, - type LanguageModelV2StreamPart, - type SharedV2ProviderMetadata, + type LanguageModelV3, + type LanguageModelV3CallOptions, + type LanguageModelV3Content, + type LanguageModelV3StreamPart, + type SharedV3ProviderMetadata, + type SharedV3Warning, } from "@ai-sdk/provider" import { combineHeaders, @@ -47,11 +47,11 @@ export type OpenAICompatibleChatConfig = { /** * The supported URLs for the model. */ - supportedUrls?: () => LanguageModelV2["supportedUrls"] + supportedUrls?: () => LanguageModelV3["supportedUrls"] } -export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { - readonly specificationVersion = "v2" +export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { + readonly specificationVersion = "v3" readonly supportsStructuredOutputs: boolean @@ -98,8 +98,8 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { seed, toolChoice, tools, - }: Parameters[0]) { - const warnings: LanguageModelV2CallWarning[] = [] + }: LanguageModelV3CallOptions) { + const warnings: SharedV3Warning[] = [] // Parse provider options const compatibleOptions = Object.assign( @@ -116,13 +116,13 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { ) if (topK != null) { - warnings.push({ type: "unsupported-setting", setting: "topK" }) + warnings.push({ type: "unsupported", feature: "topK" }) } if (responseFormat?.type === "json" && responseFormat.schema != null && !this.supportsStructuredOutputs) { warnings.push({ - type: "unsupported-setting", - setting: "responseFormat", + type: "unsupported", + feature: "responseFormat", details: "JSON response format schema is only supported with structuredOutputs", }) } @@ -189,9 +189,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { } } - async doGenerate( - options: Parameters[0], - ): Promise>> { + async doGenerate(options: LanguageModelV3CallOptions) { const { args, warnings } = await this.getArgs({ ...options }) const body = JSON.stringify(args) @@ -214,7 +212,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { }) const choice = responseBody.choices[0] - const content: Array = [] + const content: Array = [] // text content: const text = choice.message.content @@ -257,7 +255,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { } // provider metadata: - const providerMetadata: SharedV2ProviderMetadata = { + const providerMetadata: SharedV3ProviderMetadata = { [this.providerOptionsName]: {}, ...(await this.config.metadataExtractor?.extractMetadata?.({ parsedBody: rawResponse, @@ -275,13 +273,23 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { return { content, - finishReason: mapOpenAICompatibleFinishReason(choice.finish_reason), + finishReason: { + unified: mapOpenAICompatibleFinishReason(choice.finish_reason), + raw: choice.finish_reason ?? undefined, + }, usage: { - inputTokens: responseBody.usage?.prompt_tokens ?? undefined, - outputTokens: responseBody.usage?.completion_tokens ?? undefined, - totalTokens: responseBody.usage?.total_tokens ?? undefined, - reasoningTokens: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined, - cachedInputTokens: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined, + inputTokens: { + total: responseBody.usage?.prompt_tokens ?? undefined, + noCache: undefined, + cacheRead: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: responseBody.usage?.completion_tokens ?? undefined, + text: undefined, + reasoning: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined, + }, + raw: responseBody.usage ?? undefined, }, providerMetadata, request: { body }, @@ -294,9 +302,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { } } - async doStream( - options: Parameters[0], - ): Promise>> { + async doStream(options: LanguageModelV3CallOptions) { const { args, warnings } = await this.getArgs({ ...options }) const body = { @@ -332,7 +338,13 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { hasFinished: boolean }> = [] - let finishReason: LanguageModelV2FinishReason = "unknown" + let finishReason: { + unified: ReturnType + raw: string | undefined + } = { + unified: "other", + raw: undefined, + } const usage: { completionTokens: number | undefined completionTokensDetails: { @@ -366,7 +378,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { return { stream: response.pipeThrough( - new TransformStream>, LanguageModelV2StreamPart>({ + new TransformStream>, LanguageModelV3StreamPart>({ start(controller) { controller.enqueue({ type: "stream-start", warnings }) }, @@ -380,7 +392,10 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { // handle failed chunk parsing / validation: if (!chunk.success) { - finishReason = "error" + finishReason = { + unified: "error", + raw: undefined, + } controller.enqueue({ type: "error", error: chunk.error }) return } @@ -390,7 +405,10 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { // handle error chunks: if ("error" in value) { - finishReason = "error" + finishReason = { + unified: "error", + raw: undefined, + } controller.enqueue({ type: "error", error: value.error.message }) return } @@ -435,7 +453,10 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { const choice = value.choices[0] if (choice?.finish_reason != null) { - finishReason = mapOpenAICompatibleFinishReason(choice.finish_reason) + finishReason = { + unified: mapOpenAICompatibleFinishReason(choice.finish_reason), + raw: choice.finish_reason ?? undefined, + } } if (choice?.delta == null) { @@ -652,7 +673,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { }) } - const providerMetadata: SharedV2ProviderMetadata = { + const providerMetadata: SharedV3ProviderMetadata = { [providerOptionsName]: {}, // Include reasoning_opaque for Copilot multi-turn reasoning ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}), @@ -671,11 +692,25 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 { type: "finish", finishReason, usage: { - inputTokens: usage.promptTokens ?? undefined, - outputTokens: usage.completionTokens ?? undefined, - totalTokens: usage.totalTokens ?? undefined, - reasoningTokens: usage.completionTokensDetails.reasoningTokens ?? undefined, - cachedInputTokens: usage.promptTokensDetails.cachedTokens ?? undefined, + inputTokens: { + total: usage.promptTokens, + noCache: + usage.promptTokens != undefined && usage.promptTokensDetails.cachedTokens != undefined + ? usage.promptTokens - usage.promptTokensDetails.cachedTokens + : undefined, + cacheRead: usage.promptTokensDetails.cachedTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.completionTokens, + text: undefined, + reasoning: usage.completionTokensDetails.reasoningTokens, + }, + raw: { + prompt_tokens: usage.promptTokens ?? null, + completion_tokens: usage.completionTokens ?? null, + total_tokens: usage.totalTokens ?? null, + }, }, providerMetadata, }) diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts index ba233fbc1b..40335f87f6 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts @@ -1,4 +1,4 @@ -import type { SharedV2ProviderMetadata } from "@ai-sdk/provider" +import type { SharedV3ProviderMetadata } from "@ai-sdk/provider" /** Extracts provider-specific metadata from API responses. @@ -14,7 +14,7 @@ export type MetadataExtractor = { * @returns Provider-specific metadata or undefined if no metadata is available. * The metadata should be under a key indicating the provider id. */ - extractMetadata: ({ parsedBody }: { parsedBody: unknown }) => Promise + extractMetadata: ({ parsedBody }: { parsedBody: unknown }) => Promise /** * Creates an extractor for handling streaming responses. The returned object provides @@ -39,6 +39,6 @@ export type MetadataExtractor = { * @returns Provider-specific metadata or undefined if no metadata is available. * The metadata should be under a key indicating the provider id. */ - buildMetadata(): SharedV2ProviderMetadata | undefined + buildMetadata(): SharedV3ProviderMetadata | undefined } } diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts index 8879d6481b..ac907f5254 100644 --- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts @@ -1,15 +1,11 @@ -import { - type LanguageModelV2CallOptions, - type LanguageModelV2CallWarning, - UnsupportedFunctionalityError, -} from "@ai-sdk/provider" +import { type LanguageModelV3CallOptions, type SharedV3Warning, UnsupportedFunctionalityError } from "@ai-sdk/provider" export function prepareTools({ tools, toolChoice, }: { - tools: LanguageModelV2CallOptions["tools"] - toolChoice?: LanguageModelV2CallOptions["toolChoice"] + tools: LanguageModelV3CallOptions["tools"] + toolChoice?: LanguageModelV3CallOptions["toolChoice"] }): { tools: | undefined @@ -22,12 +18,12 @@ export function prepareTools({ } }> toolChoice: { type: "function"; function: { name: string } } | "auto" | "none" | "required" | undefined - toolWarnings: LanguageModelV2CallWarning[] + toolWarnings: SharedV3Warning[] } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined - const toolWarnings: LanguageModelV2CallWarning[] = [] + const toolWarnings: SharedV3Warning[] = [] if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings } @@ -43,8 +39,8 @@ export function prepareTools({ }> = [] for (const tool of tools) { - if (tool.type === "provider-defined") { - toolWarnings.push({ type: "unsupported-tool", tool }) + if (tool.type === "provider") { + toolWarnings.push({ type: "unsupported", feature: `tool type: ${tool.type}` }) } else { openaiCompatTools.push({ type: "function", diff --git a/packages/opencode/src/provider/sdk/copilot/copilot-provider.ts b/packages/opencode/src/provider/sdk/copilot/copilot-provider.ts index 1dc373ff3c..b9cbb6c7cc 100644 --- a/packages/opencode/src/provider/sdk/copilot/copilot-provider.ts +++ b/packages/opencode/src/provider/sdk/copilot/copilot-provider.ts @@ -1,4 +1,4 @@ -import type { LanguageModelV2 } from "@ai-sdk/provider" +import type { LanguageModelV3 } from "@ai-sdk/provider" import { type FetchFunction, withoutTrailingSlash, withUserAgentSuffix } from "@ai-sdk/provider-utils" import { OpenAICompatibleChatLanguageModel } from "./chat/openai-compatible-chat-language-model" import { OpenAIResponsesLanguageModel } from "./responses/openai-responses-language-model" @@ -36,10 +36,10 @@ export interface OpenaiCompatibleProviderSettings { } export interface OpenaiCompatibleProvider { - (modelId: OpenaiCompatibleModelId): LanguageModelV2 - chat(modelId: OpenaiCompatibleModelId): LanguageModelV2 - responses(modelId: OpenaiCompatibleModelId): LanguageModelV2 - languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV2 + (modelId: OpenaiCompatibleModelId): LanguageModelV3 + chat(modelId: OpenaiCompatibleModelId): LanguageModelV3 + responses(modelId: OpenaiCompatibleModelId): LanguageModelV3 + languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV3 // embeddingModel(modelId: any): EmbeddingModelV2 diff --git a/packages/opencode/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts b/packages/opencode/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts index 807f6ea57c..83e46015dd 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts @@ -1,7 +1,7 @@ import { - type LanguageModelV2CallWarning, - type LanguageModelV2Prompt, - type LanguageModelV2ToolCallPart, + type LanguageModelV3Prompt, + type LanguageModelV3ToolCallPart, + type SharedV3Warning, UnsupportedFunctionalityError, } from "@ai-sdk/provider" import { convertToBase64, parseProviderOptions } from "@ai-sdk/provider-utils" @@ -25,17 +25,18 @@ export async function convertToOpenAIResponsesInput({ store, hasLocalShellTool = false, }: { - prompt: LanguageModelV2Prompt + prompt: LanguageModelV3Prompt systemMessageMode: "system" | "developer" | "remove" fileIdPrefixes?: readonly string[] store: boolean hasLocalShellTool?: boolean }): Promise<{ input: OpenAIResponsesInput - warnings: Array + warnings: Array }> { const input: OpenAIResponsesInput = [] - const warnings: Array = [] + const warnings: Array = [] + const processedApprovalIds = new Set() for (const { role, content } of prompt) { switch (role) { @@ -118,7 +119,7 @@ export async function convertToOpenAIResponsesInput({ case "assistant": { const reasoningMessages: Record = {} - const toolCallParts: Record = {} + const toolCallParts: Record = {} for (const part of content) { switch (part.type) { @@ -251,8 +252,36 @@ export async function convertToOpenAIResponsesInput({ case "tool": { for (const part of content) { + if (part.type === "tool-approval-response") { + if (processedApprovalIds.has(part.approvalId)) { + continue + } + processedApprovalIds.add(part.approvalId) + + if (store) { + input.push({ + type: "item_reference", + id: part.approvalId, + }) + } + + input.push({ + type: "mcp_approval_response", + approval_request_id: part.approvalId, + approve: part.approved, + }) + continue + } const output = part.output + if (output.type === "execution-denied") { + const approvalId = (output.providerOptions?.openai as { approvalId?: string } | undefined)?.approvalId + + if (approvalId) { + continue + } + } + if (hasLocalShellTool && part.toolName === "local_shell" && output.type === "json") { input.push({ type: "local_shell_call_output", @@ -268,6 +297,9 @@ export async function convertToOpenAIResponsesInput({ case "error-text": contentValue = output.value break + case "execution-denied": + contentValue = output.reason ?? "Tool execution denied." + break case "content": case "json": case "error-json": diff --git a/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts b/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts index 54bb9056d7..4f443b511b 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts @@ -1,4 +1,4 @@ -import type { LanguageModelV2FinishReason } from "@ai-sdk/provider" +import type { LanguageModelV3FinishReason } from "@ai-sdk/provider" export function mapOpenAIResponseFinishReason({ finishReason, @@ -7,7 +7,7 @@ export function mapOpenAIResponseFinishReason({ finishReason: string | null | undefined // flag that checks if there have been client-side tool calls (not executed by openai) hasFunctionCall: boolean -}): LanguageModelV2FinishReason { +}): LanguageModelV3FinishReason["unified"] { switch (finishReason) { case undefined: case null: @@ -17,6 +17,6 @@ export function mapOpenAIResponseFinishReason({ case "content_filter": return "content-filter" default: - return hasFunctionCall ? "tool-calls" : "unknown" + return hasFunctionCall ? "tool-calls" : "other" } } diff --git a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-api-types.ts b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-api-types.ts index cf1a3ba2fb..dfdd066750 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-api-types.ts @@ -13,6 +13,7 @@ export type OpenAIResponsesInputItem = | OpenAIResponsesLocalShellCallOutput | OpenAIResponsesReasoning | OpenAIResponsesItemReference + | OpenAIResponsesMcpApprovalResponse export type OpenAIResponsesIncludeValue = | "web_search_call.action.sources" @@ -93,6 +94,12 @@ export type OpenAIResponsesItemReference = { id: string } +export type OpenAIResponsesMcpApprovalResponse = { + type: "mcp_approval_response" + approval_request_id: string + approve: boolean +} + /** * A filter used to compare a specified attribute key to a given value using a defined comparison operation. */ diff --git a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts index 0a575bc02b..4606af7a15 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-language-model.ts @@ -1,13 +1,13 @@ import { APICallError, - type LanguageModelV2, - type LanguageModelV2CallWarning, - type LanguageModelV2Content, - type LanguageModelV2FinishReason, - type LanguageModelV2ProviderDefinedTool, - type LanguageModelV2StreamPart, - type LanguageModelV2Usage, - type SharedV2ProviderMetadata, + type JSONValue, + type LanguageModelV3, + type LanguageModelV3CallOptions, + type LanguageModelV3Content, + type LanguageModelV3ProviderTool, + type LanguageModelV3StreamPart, + type SharedV3ProviderMetadata, + type SharedV3Warning, } from "@ai-sdk/provider" import { combineHeaders, @@ -128,8 +128,8 @@ const LOGPROBS_SCHEMA = z.array( }), ) -export class OpenAIResponsesLanguageModel implements LanguageModelV2 { - readonly specificationVersion = "v2" +export class OpenAIResponsesLanguageModel implements LanguageModelV3 { + readonly specificationVersion = "v3" readonly modelId: OpenAIResponsesModelId @@ -163,34 +163,34 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { tools, toolChoice, responseFormat, - }: Parameters[0]) { - const warnings: LanguageModelV2CallWarning[] = [] + }: LanguageModelV3CallOptions) { + const warnings: SharedV3Warning[] = [] const modelConfig = getResponsesModelConfig(this.modelId) if (topK != null) { - warnings.push({ type: "unsupported-setting", setting: "topK" }) + warnings.push({ type: "unsupported", feature: "topK" }) } if (seed != null) { - warnings.push({ type: "unsupported-setting", setting: "seed" }) + warnings.push({ type: "unsupported", feature: "seed" }) } if (presencePenalty != null) { warnings.push({ - type: "unsupported-setting", - setting: "presencePenalty", + type: "unsupported", + feature: "presencePenalty", }) } if (frequencyPenalty != null) { warnings.push({ - type: "unsupported-setting", - setting: "frequencyPenalty", + type: "unsupported", + feature: "frequencyPenalty", }) } if (stopSequences != null) { - warnings.push({ type: "unsupported-setting", setting: "stopSequences" }) + warnings.push({ type: "unsupported", feature: "stopSequences" }) } const openaiOptions = await parseProviderOptions({ @@ -218,7 +218,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { } function hasOpenAITool(id: string) { - return tools?.find((tool) => tool.type === "provider-defined" && tool.id === id) != null + return tools?.find((tool) => tool.type === "provider" && tool.id === id) != null } // when logprobs are requested, automatically include them: @@ -237,9 +237,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { const webSearchToolName = ( tools?.find( (tool) => - tool.type === "provider-defined" && - (tool.id === "openai.web_search" || tool.id === "openai.web_search_preview"), - ) as LanguageModelV2ProviderDefinedTool | undefined + tool.type === "provider" && (tool.id === "openai.web_search" || tool.id === "openai.web_search_preview"), + ) as LanguageModelV3ProviderTool | undefined )?.name if (webSearchToolName) { @@ -315,8 +314,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { if (baseArgs.temperature != null) { baseArgs.temperature = undefined warnings.push({ - type: "unsupported-setting", - setting: "temperature", + type: "unsupported", + feature: "temperature", details: "temperature is not supported for reasoning models", }) } @@ -324,24 +323,24 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { if (baseArgs.top_p != null) { baseArgs.top_p = undefined warnings.push({ - type: "unsupported-setting", - setting: "topP", + type: "unsupported", + feature: "topP", details: "topP is not supported for reasoning models", }) } } else { if (openaiOptions?.reasoningEffort != null) { warnings.push({ - type: "unsupported-setting", - setting: "reasoningEffort", + type: "unsupported", + feature: "reasoningEffort", details: "reasoningEffort is not supported for non-reasoning models", }) } if (openaiOptions?.reasoningSummary != null) { warnings.push({ - type: "unsupported-setting", - setting: "reasoningSummary", + type: "unsupported", + feature: "reasoningSummary", details: "reasoningSummary is not supported for non-reasoning models", }) } @@ -350,8 +349,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // Validate flex processing support if (openaiOptions?.serviceTier === "flex" && !modelConfig.supportsFlexProcessing) { warnings.push({ - type: "unsupported-setting", - setting: "serviceTier", + type: "unsupported", + feature: "serviceTier", details: "flex processing is only available for o3, o4-mini, and gpt-5 models", }) // Remove from args if not supported @@ -361,8 +360,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // Validate priority processing support if (openaiOptions?.serviceTier === "priority" && !modelConfig.supportsPriorityProcessing) { warnings.push({ - type: "unsupported-setting", - setting: "serviceTier", + type: "unsupported", + feature: "serviceTier", details: "priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported", }) @@ -391,9 +390,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { } } - async doGenerate( - options: Parameters[0], - ): Promise>> { + async doGenerate(options: LanguageModelV3CallOptions) { const { args: body, warnings, webSearchToolName } = await this.getArgs(options) const url = this.config.url({ path: "/responses", @@ -508,7 +505,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { }) } - const content: Array = [] + const content: Array = [] const logprobs: Array> = [] // flag that checks if there have been client-side tool calls (not executed by openai) @@ -554,7 +551,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { result: { result: part.result, } satisfies z.infer, - providerExecuted: true, }) break @@ -648,7 +644,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { toolCallId: part.id, toolName: webSearchToolName ?? "web_search", result: { status: part.status }, - providerExecuted: true, }) break @@ -671,7 +666,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { type: "computer_use_tool_result", status: part.status || "completed", }, - providerExecuted: true, }) break } @@ -693,14 +687,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { queries: part.queries, results: part.results?.map((result) => ({ - attributes: result.attributes, + attributes: result.attributes as Record, fileId: result.file_id, filename: result.filename, score: result.score, text: result.text, })) ?? null, } satisfies z.infer, - providerExecuted: true, }) break } @@ -724,14 +717,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { result: { outputs: part.outputs, } satisfies z.infer, - providerExecuted: true, }) break } } } - const providerMetadata: SharedV2ProviderMetadata = { + const providerMetadata: SharedV3ProviderMetadata = { openai: { responseId: response.id }, } @@ -745,16 +737,29 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { return { content, - finishReason: mapOpenAIResponseFinishReason({ - finishReason: response.incomplete_details?.reason, - hasFunctionCall, - }), + finishReason: { + unified: mapOpenAIResponseFinishReason({ + finishReason: response.incomplete_details?.reason, + hasFunctionCall, + }), + raw: response.incomplete_details?.reason, + }, usage: { - inputTokens: response.usage.input_tokens, - outputTokens: response.usage.output_tokens, - totalTokens: response.usage.input_tokens + response.usage.output_tokens, - reasoningTokens: response.usage.output_tokens_details?.reasoning_tokens ?? undefined, - cachedInputTokens: response.usage.input_tokens_details?.cached_tokens ?? undefined, + inputTokens: { + total: response.usage.input_tokens, + noCache: + response.usage.input_tokens_details?.cached_tokens != null + ? response.usage.input_tokens - response.usage.input_tokens_details.cached_tokens + : undefined, + cacheRead: response.usage.input_tokens_details?.cached_tokens ?? undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: response.usage.output_tokens, + text: undefined, + reasoning: response.usage.output_tokens_details?.reasoning_tokens ?? undefined, + }, + raw: response.usage, }, request: { body }, response: { @@ -769,9 +774,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { } } - async doStream( - options: Parameters[0], - ): Promise>> { + async doStream(options: LanguageModelV3CallOptions) { const { args: body, warnings, webSearchToolName } = await this.getArgs(options) const { responseHeaders, value: response } = await postJsonToApi({ @@ -792,11 +795,25 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { const self = this - let finishReason: LanguageModelV2FinishReason = "unknown" - const usage: LanguageModelV2Usage = { + let finishReason: { + unified: ReturnType + raw: string | undefined + } = { + unified: "other", + raw: undefined, + } + const usage: { + inputTokens: number | undefined + outputTokens: number | undefined + totalTokens: number | undefined + reasoningTokens: number | undefined + cachedInputTokens: number | undefined + } = { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined, + reasoningTokens: undefined, + cachedInputTokens: undefined, } const logprobs: Array> = [] let responseId: string | null = null @@ -837,7 +854,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { return { stream: response.pipeThrough( - new TransformStream>, LanguageModelV2StreamPart>({ + new TransformStream>, LanguageModelV3StreamPart>({ start(controller) { controller.enqueue({ type: "stream-start", warnings }) }, @@ -849,7 +866,10 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // handle failed chunk parsing / validation: if (!chunk.success) { - finishReason = "error" + finishReason = { + unified: "error", + raw: undefined, + } controller.enqueue({ type: "error", error: chunk.error }) return } @@ -999,7 +1019,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { toolCallId: value.item.id, toolName: "web_search", result: { status: value.item.status }, - providerExecuted: true, }) } else if (value.item.type === "computer_call") { ongoingToolCalls[value.output_index] = undefined @@ -1025,7 +1044,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { type: "computer_use_tool_result", status: value.item.status || "completed", }, - providerExecuted: true, }) } else if (value.item.type === "file_search_call") { ongoingToolCalls[value.output_index] = undefined @@ -1038,14 +1056,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { queries: value.item.queries, results: value.item.results?.map((result) => ({ - attributes: result.attributes, + attributes: result.attributes as Record, fileId: result.file_id, filename: result.filename, score: result.score, text: result.text, })) ?? null, } satisfies z.infer, - providerExecuted: true, }) } else if (value.item.type === "code_interpreter_call") { ongoingToolCalls[value.output_index] = undefined @@ -1057,7 +1074,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { result: { outputs: value.item.outputs, } satisfies z.infer, - providerExecuted: true, }) } else if (value.item.type === "image_generation_call") { controller.enqueue({ @@ -1067,7 +1083,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { result: { result: value.item.result, } satisfies z.infer, - providerExecuted: true, }) } else if (value.item.type === "local_shell_call") { ongoingToolCalls[value.output_index] = undefined @@ -1137,7 +1152,6 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { result: { result: value.partial_image_b64, } satisfies z.infer, - providerExecuted: true, }) } else if (isResponseCodeInterpreterCallCodeDeltaChunk(value)) { const toolCall = ongoingToolCalls[value.output_index] @@ -1244,10 +1258,13 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { }) } } else if (isResponseFinishedChunk(value)) { - finishReason = mapOpenAIResponseFinishReason({ - finishReason: value.response.incomplete_details?.reason, - hasFunctionCall, - }) + finishReason = { + unified: mapOpenAIResponseFinishReason({ + finishReason: value.response.incomplete_details?.reason, + hasFunctionCall, + }), + raw: value.response.incomplete_details?.reason ?? undefined, + } usage.inputTokens = value.response.usage.input_tokens usage.outputTokens = value.response.usage.output_tokens usage.totalTokens = value.response.usage.input_tokens + value.response.usage.output_tokens @@ -1287,7 +1304,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { currentTextId = null } - const providerMetadata: SharedV2ProviderMetadata = { + const providerMetadata: SharedV3ProviderMetadata = { openai: { responseId, }, @@ -1304,7 +1321,27 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { controller.enqueue({ type: "finish", finishReason, - usage, + usage: { + inputTokens: { + total: usage.inputTokens, + noCache: + usage.inputTokens != null && usage.cachedInputTokens != null + ? usage.inputTokens - usage.cachedInputTokens + : undefined, + cacheRead: usage.cachedInputTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.outputTokens, + text: undefined, + reasoning: usage.reasoningTokens, + }, + raw: { + input_tokens: usage.inputTokens, + output_tokens: usage.outputTokens, + total_tokens: usage.totalTokens, + }, + }, providerMetadata, }) }, diff --git a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts index 791de3e7cf..8b2eb01673 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts @@ -1,8 +1,4 @@ -import { - type LanguageModelV2CallOptions, - type LanguageModelV2CallWarning, - UnsupportedFunctionalityError, -} from "@ai-sdk/provider" +import { type LanguageModelV3CallOptions, type SharedV3Warning, UnsupportedFunctionalityError } from "@ai-sdk/provider" import { codeInterpreterArgsSchema } from "./tool/code-interpreter" import { fileSearchArgsSchema } from "./tool/file-search" import { webSearchArgsSchema } from "./tool/web-search" @@ -15,8 +11,8 @@ export function prepareResponsesTools({ toolChoice, strictJsonSchema, }: { - tools: LanguageModelV2CallOptions["tools"] - toolChoice?: LanguageModelV2CallOptions["toolChoice"] + tools: LanguageModelV3CallOptions["tools"] + toolChoice?: LanguageModelV3CallOptions["toolChoice"] strictJsonSchema: boolean }): { tools?: Array @@ -30,12 +26,12 @@ export function prepareResponsesTools({ | { type: "function"; name: string } | { type: "code_interpreter" } | { type: "image_generation" } - toolWarnings: LanguageModelV2CallWarning[] + toolWarnings: SharedV3Warning[] } { // when the tools array is empty, change it to undefined to prevent errors: tools = tools?.length ? tools : undefined - const toolWarnings: LanguageModelV2CallWarning[] = [] + const toolWarnings: SharedV3Warning[] = [] if (tools == null) { return { tools: undefined, toolChoice: undefined, toolWarnings } @@ -54,7 +50,7 @@ export function prepareResponsesTools({ strict: strictJsonSchema, }) break - case "provider-defined": { + case "provider": { switch (tool.id) { case "openai.file_search": { const args = fileSearchArgsSchema.parse(tool.args) @@ -138,7 +134,7 @@ export function prepareResponsesTools({ break } default: - toolWarnings.push({ type: "unsupported-tool", tool }) + toolWarnings.push({ type: "unsupported", feature: "tool type" }) break } } diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/code-interpreter.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/code-interpreter.ts index 2bb4bce778..909694ec7d 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/code-interpreter.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" import { z } from "zod/v4" export const codeInterpreterInputSchema = z.object({ @@ -37,7 +37,7 @@ type CodeInterpreterArgs = { container?: string | { fileIds?: string[] } } -export const codeInterpreterToolFactory = createProviderDefinedToolFactoryWithOutputSchema< +export const codeInterpreterToolFactory = createProviderToolFactoryWithOutputSchema< { /** * The code to run, or null if not available. @@ -76,7 +76,6 @@ export const codeInterpreterToolFactory = createProviderDefinedToolFactoryWithOu CodeInterpreterArgs >({ id: "openai.code_interpreter", - name: "code_interpreter", inputSchema: codeInterpreterInputSchema, outputSchema: codeInterpreterOutputSchema, }) diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/file-search.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/file-search.ts index 1fccddaf63..12a490e19d 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/file-search.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/file-search.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" import type { OpenAIResponsesFileSearchToolComparisonFilter, OpenAIResponsesFileSearchToolCompoundFilter, @@ -43,7 +43,7 @@ export const fileSearchOutputSchema = z.object({ .nullable(), }) -export const fileSearch = createProviderDefinedToolFactoryWithOutputSchema< +export const fileSearch = createProviderToolFactoryWithOutputSchema< {}, { /** @@ -122,7 +122,6 @@ export const fileSearch = createProviderDefinedToolFactoryWithOutputSchema< } >({ id: "openai.file_search", - name: "file_search", inputSchema: z.object({}), outputSchema: fileSearchOutputSchema, }) diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/image-generation.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/image-generation.ts index 7367a4802b..b67bb76f9c 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/image-generation.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/image-generation.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" import { z } from "zod/v4" export const imageGenerationArgsSchema = z @@ -92,7 +92,7 @@ type ImageGenerationArgs = { size?: "auto" | "1024x1024" | "1024x1536" | "1536x1024" } -const imageGenerationToolFactory = createProviderDefinedToolFactoryWithOutputSchema< +const imageGenerationToolFactory = createProviderToolFactoryWithOutputSchema< {}, { /** @@ -103,7 +103,6 @@ const imageGenerationToolFactory = createProviderDefinedToolFactoryWithOutputSch ImageGenerationArgs >({ id: "openai.image_generation", - name: "image_generation", inputSchema: z.object({}), outputSchema: imageGenerationOutputSchema, }) diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/local-shell.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/local-shell.ts index 4ceca0d6cd..45230d5ce5 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/local-shell.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/local-shell.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" import { z } from "zod/v4" export const localShellInputSchema = z.object({ @@ -16,7 +16,7 @@ export const localShellOutputSchema = z.object({ output: z.string(), }) -export const localShell = createProviderDefinedToolFactoryWithOutputSchema< +export const localShell = createProviderToolFactoryWithOutputSchema< { /** * Execute a shell command on the server. @@ -59,7 +59,6 @@ export const localShell = createProviderDefinedToolFactoryWithOutputSchema< {} >({ id: "openai.local_shell", - name: "local_shell", inputSchema: localShellInputSchema, outputSchema: localShellOutputSchema, }) diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search-preview.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search-preview.ts index 69ea65ef0e..3d9a308d8a 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search-preview.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactory } from "@ai-sdk/provider-utils" +import { createProviderToolFactory } from "@ai-sdk/provider-utils" import { z } from "zod/v4" // Args validation schema @@ -40,7 +40,7 @@ export const webSearchPreviewArgsSchema = z.object({ .optional(), }) -export const webSearchPreview = createProviderDefinedToolFactory< +export const webSearchPreview = createProviderToolFactory< { // Web search doesn't take input parameters - it's controlled by the prompt }, @@ -81,7 +81,6 @@ export const webSearchPreview = createProviderDefinedToolFactory< } >({ id: "openai.web_search_preview", - name: "web_search_preview", inputSchema: z.object({ action: z .discriminatedUnion("type", [ diff --git a/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search.ts b/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search.ts index 89622ad3ce..e380bb13b6 100644 --- a/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search.ts +++ b/packages/opencode/src/provider/sdk/copilot/responses/tool/web-search.ts @@ -1,4 +1,4 @@ -import { createProviderDefinedToolFactory } from "@ai-sdk/provider-utils" +import { createProviderToolFactory } from "@ai-sdk/provider-utils" import { z } from "zod/v4" export const webSearchArgsSchema = z.object({ @@ -21,7 +21,7 @@ export const webSearchArgsSchema = z.object({ .optional(), }) -export const webSearchToolFactory = createProviderDefinedToolFactory< +export const webSearchToolFactory = createProviderToolFactory< { // Web search doesn't take input parameters - it's controlled by the prompt }, @@ -74,7 +74,6 @@ export const webSearchToolFactory = createProviderDefinedToolFactory< } >({ id: "openai.web_search", - name: "web_search", inputSchema: z.object({ action: z .discriminatedUnion("type", [ diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 418ccc5b2e..f651a5b91a 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -25,8 +25,9 @@ export namespace ProviderTransform { switch (npm) { case "@ai-sdk/github-copilot": return "copilot" - case "@ai-sdk/openai": case "@ai-sdk/azure": + return "azure" + case "@ai-sdk/openai": return "openai" case "@ai-sdk/amazon-bedrock": return "bedrock" @@ -34,6 +35,7 @@ export namespace ProviderTransform { case "@ai-sdk/google-vertex/anthropic": return "anthropic" case "@ai-sdk/google-vertex": + return "vertex" case "@ai-sdk/google": return "google" case "@ai-sdk/gateway": @@ -72,17 +74,29 @@ export namespace ProviderTransform { } if (model.api.id.includes("claude")) { + const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_") return msgs.map((msg) => { - if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) { - msg.content = msg.content.map((part) => { - if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) { - return { - ...part, - toolCallId: part.toolCallId.replace(/[^a-zA-Z0-9_-]/g, "_"), + if (msg.role === "assistant" && Array.isArray(msg.content)) { + return { + ...msg, + content: msg.content.map((part) => { + if (part.type === "tool-call" || part.type === "tool-result") { + return { ...part, toolCallId: scrub(part.toolCallId) } } - } - return part - }) + return part + }), + } + } + if (msg.role === "tool" && Array.isArray(msg.content)) { + return { + ...msg, + content: msg.content.map((part) => { + if (part.type === "tool-result") { + return { ...part, toolCallId: scrub(part.toolCallId) } + } + return part + }), + } } return msg }) @@ -92,29 +106,33 @@ export namespace ProviderTransform { model.api.id.toLowerCase().includes("mistral") || model.api.id.toLocaleLowerCase().includes("devstral") ) { + const scrub = (id: string) => { + return id + .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters + .substring(0, 9) // Take first 9 characters + .padEnd(9, "0") // Pad with zeros if less than 9 characters + } const result: ModelMessage[] = [] for (let i = 0; i < msgs.length; i++) { const msg = msgs[i] const nextMsg = msgs[i + 1] - if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) { + if (msg.role === "assistant" && Array.isArray(msg.content)) { msg.content = msg.content.map((part) => { - if ((part.type === "tool-call" || part.type === "tool-result") && "toolCallId" in part) { - // Mistral requires alphanumeric tool call IDs with exactly 9 characters - const normalizedId = part.toolCallId - .replace(/[^a-zA-Z0-9]/g, "") // Remove non-alphanumeric characters - .substring(0, 9) // Take first 9 characters - .padEnd(9, "0") // Pad with zeros if less than 9 characters - - return { - ...part, - toolCallId: normalizedId, - } + if (part.type === "tool-call" || part.type === "tool-result") { + return { ...part, toolCallId: scrub(part.toolCallId) } + } + return part + }) + } + if (msg.role === "tool" && Array.isArray(msg.content)) { + msg.content = msg.content.map((part) => { + if (part.type === "tool-result") { + return { ...part, toolCallId: scrub(part.toolCallId) } } return part }) } - result.push(msg) // Fix message sequence: tool messages cannot be followed by user messages @@ -202,7 +220,12 @@ export namespace ProviderTransform { if (shouldUseContentOptions) { const lastContent = msg.content[msg.content.length - 1] - if (lastContent && typeof lastContent === "object") { + if ( + lastContent && + typeof lastContent === "object" && + lastContent.type !== "tool-approval-request" && + lastContent.type !== "tool-approval-response" + ) { lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } @@ -284,7 +307,12 @@ export namespace ProviderTransform { return { ...msg, providerOptions: remap(msg.providerOptions), - content: msg.content.map((part) => ({ ...part, providerOptions: remap(part.providerOptions) })), + content: msg.content.map((part) => { + if (part.type === "tool-approval-request" || part.type === "tool-approval-response") { + return { ...part } + } + return { ...part, providerOptions: remap(part.providerOptions) } + }), } as typeof msg }) } diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index f6145b7a47..d352d4f079 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -215,7 +215,7 @@ When constructing the summary, try to stick to this template: tools: {}, system: [], messages: [ - ...MessageV2.toModelMessages(msgs, model, { stripMedia: true }), + ...(await MessageV2.toModelMessages(msgs, model, { stripMedia: true })), { role: "user", content: [ diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 075f070e42..ed82ebc592 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -1,16 +1,6 @@ -import { Installation } from "@/installation" import { Provider } from "@/provider/provider" import { Log } from "@/util/log" -import { - streamText, - wrapLanguageModel, - type ModelMessage, - type StreamTextResult, - type Tool, - type ToolSet, - tool, - jsonSchema, -} from "ai" +import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" import { mergeDeep, pipe } from "remeda" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider/transform" @@ -23,6 +13,7 @@ import { SystemPrompt } from "./system" import { Flag } from "@/flag/flag" import { Permission } from "@/permission" import { Auth } from "@/auth" +import { Installation } from "@/installation" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -43,8 +34,6 @@ export namespace LLM { toolChoice?: "auto" | "required" | "none" } - export type StreamOutput = StreamTextResult - export async function stream(input: StreamInput) { const l = log .clone() @@ -273,8 +262,10 @@ export namespace LLM { model: language, middleware: [ { + specificationVersion: "v3" as const, async transformParams(args) { if (args.type === "stream") { + // TODO: verify that LanguageModelV3Prompt is still compat here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // @ts-expect-error args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options) } diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 37cbebc9ce..7260a8af2e 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -573,11 +573,11 @@ export namespace MessageV2 { })) } - export function toModelMessages( + export async function toModelMessages( input: WithParts[], model: Provider.Model, options?: { stripMedia?: boolean }, - ): ModelMessage[] { + ): Promise { const result: UIMessage[] = [] const toolNames = new Set() // Track media from tool results that need to be injected as user messages @@ -601,7 +601,8 @@ export namespace MessageV2 { return false })() - const toModelOutput = (output: unknown) => { + const toModelOutput = (options: { toolCallId: string; input: unknown; output: unknown }) => { + const output = options.output if (typeof output === "string") { return { type: "text", value: output } } @@ -799,7 +800,7 @@ export namespace MessageV2 { const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }])) - return convertToModelMessages( + return await convertToModelMessages( result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), { //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput) @@ -871,7 +872,13 @@ export namespace MessageV2 { db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), ) return rows.map( - (row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part, + (row) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + }) as MessageV2.Part, ) }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b3c34539e7..dd74b83f50 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -11,7 +11,7 @@ import { Session } from "." import { Agent } from "../agent/agent" import { Provider } from "../provider/provider" import { ModelID, ProviderID } from "../provider/schema" -import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions, asSchema } from "ai" +import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" import { SessionCompaction } from "./compaction" import { Instance } from "../project/instance" import { Bus } from "../bus" @@ -321,7 +321,13 @@ export namespace SessionPrompt { if (!lastUser) throw new Error("No user message found in stream. This should never happen.") if ( lastAssistant?.finish && - !["tool-calls", "unknown"].includes(lastAssistant.finish) && + ![ + "tool-calls", + // in v6 unknown became other but other existed in v5 too and was distinctly different + // I think there are certain providers that used to have bad stop reasons, not rlly sure which + // ones if any still have this? + // "unknown", + ].includes(lastAssistant.finish) && lastUser.id < lastAssistant.id ) { log.info("exiting loop", { sessionID }) @@ -692,7 +698,7 @@ export namespace SessionPrompt { sessionID, system, messages: [ - ...MessageV2.toModelMessages(msgs, model), + ...(await MessageV2.toModelMessages(msgs, model)), ...(isLastStep ? [ { @@ -775,7 +781,7 @@ export namespace SessionPrompt { using _ = log.time("resolveTools") const tools: Record = {} - const context = (args: any, options: ToolCallOptions): Tool.Context => ({ + const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({ sessionID: input.session.id, abort: options.abortSignal!, messageID: input.processor.message.id, @@ -861,7 +867,8 @@ export namespace SessionPrompt { const execute = item.execute if (!execute) continue - const transformed = ProviderTransform.schema(input.model, asSchema(item.inputSchema).jsonSchema) + const schema = await asSchema(item.inputSchema).jsonSchema + const transformed = ProviderTransform.schema(input.model, schema) item.inputSchema = jsonSchema(transformed) // Wrap execute to add plugin hooks and format output item.execute = async (args, opts) => { @@ -974,10 +981,10 @@ export namespace SessionPrompt { metadata: { valid: true }, } }, - toModelOutput(result) { + toModelOutput({ output }) { return { type: "text", - value: result.output, + value: output.output, } }, }) @@ -2010,28 +2017,28 @@ NOTE: At any point in time through this workflow you should feel free to ask the (await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID)) ) }) - const result = await LLM.stream({ - agent, - user: firstRealUser.info as MessageV2.User, - system: [], - small: true, - tools: {}, - model, - abort: new AbortController().signal, - sessionID: input.session.id, - retries: 2, - messages: [ - { - role: "user", - content: "Generate a title for this conversation:\n", - }, - ...(hasOnlySubtaskParts - ? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }] - : MessageV2.toModelMessages(contextMessages, model)), - ], - }) - const text = await result.text.catch((err) => log.error("failed to generate title", { error: err })) - if (text) { + try { + const result = await LLM.stream({ + agent, + user: firstRealUser.info as MessageV2.User, + system: [], + small: true, + tools: {}, + model, + abort: new AbortController().signal, + sessionID: input.session.id, + retries: 2, + messages: [ + { + role: "user", + content: "Generate a title for this conversation:\n", + }, + ...(hasOnlySubtaskParts + ? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }] + : await MessageV2.toModelMessages(contextMessages, model)), + ], + }) + const text = await result.text const cleaned = text .replace(/[\s\S]*?<\/think>\s*/g, "") .split("\n") @@ -2044,6 +2051,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (NotFoundError.isInstance(err)) return throw err }) + } catch (error) { + log.error("failed to generate title", { error }) } } } diff --git a/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts b/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts index 562da4507d..389a72bb37 100644 --- a/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts +++ b/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts @@ -1,6 +1,6 @@ import { OpenAICompatibleChatLanguageModel } from "@/provider/sdk/copilot/chat/openai-compatible-chat-language-model" import { describe, test, expect, mock } from "bun:test" -import type { LanguageModelV2Prompt } from "@ai-sdk/provider" +import type { LanguageModelV3Prompt } from "@ai-sdk/provider" async function convertReadableStreamToArray(stream: ReadableStream): Promise { const reader = stream.getReader() @@ -13,7 +13,7 @@ async function convertReadableStreamToArray(stream: ReadableStream): Promi return result } -const TEST_PROMPT: LanguageModelV2Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }] +const TEST_PROMPT: LanguageModelV3Prompt = [{ role: "user", content: [{ type: "text", text: "Hello" }] }] // Fixtures from copilot_test.exs const FIXTURES = { @@ -123,7 +123,7 @@ describe("doStream", () => { { type: "text-delta", id: "txt-0", delta: " world" }, { type: "text-delta", id: "txt-0", delta: "!" }, { type: "text-end", id: "txt-0" }, - { type: "finish", finishReason: "stop" }, + { type: "finish", finishReason: { unified: "stop" } }, ]) }) @@ -201,10 +201,10 @@ describe("doStream", () => { const finish = parts.find((p) => p.type === "finish") expect(finish).toMatchObject({ type: "finish", - finishReason: "tool-calls", + finishReason: { unified: "tool-calls" }, usage: { - inputTokens: 19581, - outputTokens: 53, + inputTokens: { total: 19581 }, + outputTokens: { total: 53 }, }, }) }) @@ -256,10 +256,10 @@ describe("doStream", () => { const finish = parts.find((p) => p.type === "finish") expect(finish).toMatchObject({ type: "finish", - finishReason: "stop", + finishReason: { unified: "stop" }, usage: { - inputTokens: 5778, - outputTokens: 59, + inputTokens: { total: 5778 }, + outputTokens: { total: 59 }, }, providerMetadata: { copilot: { @@ -315,7 +315,7 @@ describe("doStream", () => { const finish = parts.find((p) => p.type === "finish") expect(finish).toMatchObject({ type: "finish", - finishReason: "stop", + finishReason: { unified: "stop" }, }) }) @@ -388,10 +388,10 @@ describe("doStream", () => { const finish = parts.find((p) => p.type === "finish") expect(finish).toMatchObject({ type: "finish", - finishReason: "tool-calls", + finishReason: { unified: "tool-calls" }, usage: { - inputTokens: 3767, - outputTokens: 19, + inputTokens: { total: 3767 }, + outputTokens: { total: 19 }, }, }) }) @@ -449,7 +449,7 @@ describe("doStream", () => { const finish = parts.find((p) => p.type === "finish") expect(finish).toMatchObject({ type: "finish", - finishReason: "tool-calls", + finishReason: { unified: "tool-calls" }, }) }) diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 15c797022d..b669a1e21a 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -1,408 +1,412 @@ -import { test, expect, describe } from "bun:test" -import path from "path" +// TODO: UNCOMMENT WHEN GITLAB SUPPORT IS COMPLETED +// +// +// +// import { test, expect, describe } from "bun:test" +// import path from "path" -import { ProviderID, ModelID } from "../../src/provider/schema" -import { tmpdir } from "../fixture/fixture" -import { Instance } from "../../src/project/instance" -import { Provider } from "../../src/provider/provider" -import { Env } from "../../src/env" -import { Global } from "../../src/global" -import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" +// import { ProviderID, ModelID } from "../../src/provider/schema" +// import { tmpdir } from "../fixture/fixture" +// import { Instance } from "../../src/project/instance" +// import { Provider } from "../../src/provider/provider" +// import { Env } from "../../src/env" +// import { Global } from "../../src/global" +// import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" -test("GitLab Duo: loads provider with API key from environment", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-gitlab-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token") - }, - }) -}) +// test("GitLab Duo: loads provider with API key from environment", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-gitlab-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].key).toBe("test-gitlab-token") +// }, +// }) +// }) -test("GitLab Duo: config instanceUrl option sets baseURL", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - provider: { - gitlab: { - options: { - instanceUrl: "https://gitlab.example.com", - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com") - }, - }) -}) +// test("GitLab Duo: config instanceUrl option sets baseURL", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// provider: { +// gitlab: { +// options: { +// instanceUrl: "https://gitlab.example.com", +// }, +// }, +// }, +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// Env.set("GITLAB_INSTANCE_URL", "https://gitlab.example.com") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.example.com") +// }, +// }) +// }) -test("GitLab Duo: loads with OAuth token from auth.json", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) +// test("GitLab Duo: loads with OAuth token from auth.json", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// }), +// ) +// }, +// }) - const authPath = path.join(Global.Path.data, "auth.json") - await Bun.write( - authPath, - JSON.stringify({ - gitlab: { - type: "oauth", - access: "test-access-token", - refresh: "test-refresh-token", - expires: Date.now() + 3600000, - }, - }), - ) +// const authPath = path.join(Global.Path.data, "auth.json") +// await Bun.write( +// authPath, +// JSON.stringify({ +// gitlab: { +// type: "oauth", +// access: "test-access-token", +// refresh: "test-refresh-token", +// expires: Date.now() + 3600000, +// }, +// }), +// ) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - }, - }) -}) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// }, +// }) +// }) -test("GitLab Duo: loads with Personal Access Token from auth.json", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) +// test("GitLab Duo: loads with Personal Access Token from auth.json", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// }), +// ) +// }, +// }) - const authPath2 = path.join(Global.Path.data, "auth.json") - await Bun.write( - authPath2, - JSON.stringify({ - gitlab: { - type: "api", - key: "glpat-test-pat-token", - }, - }), - ) +// const authPath2 = path.join(Global.Path.data, "auth.json") +// await Bun.write( +// authPath2, +// JSON.stringify({ +// gitlab: { +// type: "api", +// key: "glpat-test-pat-token", +// }, +// }), +// ) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token") - }, - }) -}) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].key).toBe("glpat-test-pat-token") +// }, +// }) +// }) -test("GitLab Duo: supports self-hosted instance configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - provider: { - gitlab: { - options: { - instanceUrl: "https://gitlab.company.internal", - apiKey: "glpat-internal-token", - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal") - }, - }) -}) +// test("GitLab Duo: supports self-hosted instance configuration", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// provider: { +// gitlab: { +// options: { +// instanceUrl: "https://gitlab.company.internal", +// apiKey: "glpat-internal-token", +// }, +// }, +// }, +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_INSTANCE_URL", "https://gitlab.company.internal") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].options?.instanceUrl).toBe("https://gitlab.company.internal") +// }, +// }) +// }) -test("GitLab Duo: config apiKey takes precedence over environment variable", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - provider: { - gitlab: { - options: { - apiKey: "config-token", - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "env-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - }, - }) -}) +// test("GitLab Duo: config apiKey takes precedence over environment variable", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// provider: { +// gitlab: { +// options: { +// apiKey: "config-token", +// }, +// }, +// }, +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "env-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// }, +// }) +// }) -test("GitLab Duo: includes context-1m beta header in aiGatewayHeaders", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain( - "context-1m-2025-08-07", - ) - }, - }) -}) +// test("GitLab Duo: includes context-1m beta header in aiGatewayHeaders", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].options?.aiGatewayHeaders?.["anthropic-beta"]).toContain( +// "context-1m-2025-08-07", +// ) +// }, +// }) +// }) -test("GitLab Duo: supports feature flags configuration", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - provider: { - gitlab: { - options: { - featureFlags: { - duo_agent_platform_agentic_chat: true, - duo_agent_platform: true, - }, - }, - }, - }, - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined() - expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true) - }, - }) -}) +// test("GitLab Duo: supports feature flags configuration", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// provider: { +// gitlab: { +// options: { +// featureFlags: { +// duo_agent_platform_agentic_chat: true, +// duo_agent_platform: true, +// }, +// }, +// }, +// }, +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// expect(providers[ProviderID.gitlab].options?.featureFlags).toBeDefined() +// expect(providers[ProviderID.gitlab].options?.featureFlags?.duo_agent_platform_agentic_chat).toBe(true) +// }, +// }) +// }) -test("GitLab Duo: has multiple agentic chat models available", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write( - path.join(dir, "opencode.json"), - JSON.stringify({ - $schema: "https://opencode.ai/config.json", - }), - ) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - const models = Object.keys(providers[ProviderID.gitlab].models) - expect(models.length).toBeGreaterThan(0) - expect(models).toContain("duo-chat-haiku-4-5") - expect(models).toContain("duo-chat-sonnet-4-5") - expect(models).toContain("duo-chat-opus-4-5") - }, - }) -}) +// test("GitLab Duo: has multiple agentic chat models available", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write( +// path.join(dir, "opencode.json"), +// JSON.stringify({ +// $schema: "https://opencode.ai/config.json", +// }), +// ) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// const models = Object.keys(providers[ProviderID.gitlab].models) +// expect(models.length).toBeGreaterThan(0) +// expect(models).toContain("duo-chat-haiku-4-5") +// expect(models).toContain("duo-chat-sonnet-4-5") +// expect(models).toContain("duo-chat-opus-4-5") +// }, +// }) +// }) -describe("GitLab Duo: workflow model routing", () => { - test("duo-workflow-* model routes through workflowChat", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - const gitlab = providers[ProviderID.gitlab] - expect(gitlab).toBeDefined() - gitlab.models["duo-workflow-sonnet-4-6"] = { - id: ModelID.make("duo-workflow-sonnet-4-6"), - providerID: ProviderID.make("gitlab"), - name: "Agent Platform (Claude Sonnet 4.6)", - family: "", - api: { id: "duo-workflow-sonnet-4-6", url: "https://gitlab.com", npm: "gitlab-ai-provider" }, - status: "active", - headers: {}, - options: { workflowRef: "claude_sonnet_4_6" }, - cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, - limit: { context: 200000, output: 64000 }, - 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, - }, - release_date: "", - variants: {}, - } - const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6")) - expect(model).toBeDefined() - expect(model.options?.workflowRef).toBe("claude_sonnet_4_6") - const language = await Provider.getLanguage(model) - expect(language).toBeDefined() - expect(language).toBeInstanceOf(GitLabWorkflowLanguageModel) - }, - }) - }) +// describe("GitLab Duo: workflow model routing", () => { +// test("duo-workflow-* model routes through workflowChat", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// const gitlab = providers[ProviderID.gitlab] +// expect(gitlab).toBeDefined() +// gitlab.models["duo-workflow-sonnet-4-6"] = { +// id: ModelID.make("duo-workflow-sonnet-4-6"), +// providerID: ProviderID.make("gitlab"), +// name: "Agent Platform (Claude Sonnet 4.6)", +// family: "", +// api: { id: "duo-workflow-sonnet-4-6", url: "https://gitlab.com", npm: "gitlab-ai-provider" }, +// status: "active", +// headers: {}, +// options: { workflowRef: "claude_sonnet_4_6" }, +// cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, +// limit: { context: 200000, output: 64000 }, +// 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, +// }, +// release_date: "", +// variants: {}, +// } +// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-workflow-sonnet-4-6")) +// expect(model).toBeDefined() +// expect(model.options?.workflowRef).toBe("claude_sonnet_4_6") +// const language = await Provider.getLanguage(model) +// expect(language).toBeDefined() +// expect(language).toBeInstanceOf(GitLabWorkflowLanguageModel) +// }, +// }) +// }) - test("duo-chat-* model routes through agenticChat (not workflow)", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - expect(providers[ProviderID.gitlab]).toBeDefined() - const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5")) - expect(model).toBeDefined() - const language = await Provider.getLanguage(model) - expect(language).toBeDefined() - expect(language).not.toBeInstanceOf(GitLabWorkflowLanguageModel) - }, - }) - }) +// test("duo-chat-* model routes through agenticChat (not workflow)", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// expect(providers[ProviderID.gitlab]).toBeDefined() +// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5")) +// expect(model).toBeDefined() +// const language = await Provider.getLanguage(model) +// expect(language).toBeDefined() +// expect(language).not.toBeInstanceOf(GitLabWorkflowLanguageModel) +// }, +// }) +// }) - test("model.options merged with provider.options in getLanguage", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - const gitlab = providers[ProviderID.gitlab] - expect(gitlab.options?.featureFlags).toBeDefined() - const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5")) - expect(model).toBeDefined() - expect(model.options).toBeDefined() - }, - }) - }) -}) +// test("model.options merged with provider.options in getLanguage", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// const gitlab = providers[ProviderID.gitlab] +// expect(gitlab.options?.featureFlags).toBeDefined() +// const model = await Provider.getModel(ProviderID.gitlab, ModelID.make("duo-chat-sonnet-4-5")) +// expect(model).toBeDefined() +// expect(model.options).toBeDefined() +// }, +// }) +// }) +// }) -describe("GitLab Duo: static models", () => { - test("static duo-chat models always present regardless of discovery", async () => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) - }, - }) - await Instance.provide({ - directory: tmp.path, - init: async () => { - Env.set("GITLAB_TOKEN", "test-token") - }, - fn: async () => { - const providers = await Provider.list() - const models = Object.keys(providers[ProviderID.gitlab].models) - expect(models).toContain("duo-chat-haiku-4-5") - expect(models).toContain("duo-chat-sonnet-4-5") - expect(models).toContain("duo-chat-opus-4-5") - }, - }) - }) -}) +// describe("GitLab Duo: static models", () => { +// test("static duo-chat models always present regardless of discovery", async () => { +// await using tmp = await tmpdir({ +// init: async (dir) => { +// await Bun.write(path.join(dir, "opencode.json"), JSON.stringify({ $schema: "https://opencode.ai/config.json" })) +// }, +// }) +// await Instance.provide({ +// directory: tmp.path, +// init: async () => { +// Env.set("GITLAB_TOKEN", "test-token") +// }, +// fn: async () => { +// const providers = await Provider.list() +// const models = Object.keys(providers[ProviderID.gitlab].models) +// expect(models).toContain("duo-chat-haiku-4-5") +// expect(models).toContain("duo-chat-sonnet-4-5") +// expect(models).toContain("duo-chat-opus-4-5") +// }, +// }) +// }) +// }) diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 5202c06dd9..8de7d2723a 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -3,7 +3,6 @@ import path from "path" import { tool, type ModelMessage } from "ai" import z from "zod" import { LLM } from "../../src/session/llm" -import { Global } from "../../src/global" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider/provider" import { ProviderTransform } from "../../src/provider/transform" @@ -535,6 +534,130 @@ describe("session.llm.stream", () => { }) }) + test("accepts user image attachments as data URLs for OpenAI models", async () => { + const server = state.server + if (!server) { + throw new Error("Server not initialized") + } + + const source = await loadFixture("openai", "gpt-5.2") + const model = source.model + const chunks = [ + { + type: "response.created", + response: { + id: "resp-data-url", + created_at: Math.floor(Date.now() / 1000), + model: model.id, + service_tier: null, + }, + }, + { + type: "response.output_text.delta", + item_id: "item-data-url", + delta: "Looks good", + logprobs: null, + }, + { + type: "response.completed", + response: { + incomplete_details: null, + usage: { + input_tokens: 1, + input_tokens_details: null, + output_tokens: 1, + output_tokens_details: null, + }, + service_tier: null, + }, + }, + ] + const request = waitRequest("/responses", createEventResponse(chunks, true)) + const image = `data:image/png;base64,${Buffer.from( + await Bun.file(path.join(import.meta.dir, "../tool/fixtures/large-image.png")).arrayBuffer(), + ).toString("base64")}` + + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + enabled_providers: ["openai"], + provider: { + openai: { + name: "OpenAI", + env: ["OPENAI_API_KEY"], + npm: "@ai-sdk/openai", + api: "https://api.openai.com/v1", + models: { + [model.id]: model, + }, + options: { + apiKey: "test-openai-key", + baseURL: `${server.url.origin}/v1`, + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const resolved = await Provider.getModel(ProviderID.openai, ModelID.make(model.id)) + const sessionID = SessionID.make("session-test-data-url") + const agent = { + name: "test", + mode: "primary", + options: {}, + permission: [{ permission: "*", pattern: "*", action: "allow" }], + } satisfies Agent.Info + + const user = { + id: MessageID.make("user-data-url"), + sessionID, + role: "user", + time: { created: Date.now() }, + agent: agent.name, + model: { providerID: ProviderID.make("openai"), modelID: resolved.id }, + } satisfies MessageV2.User + + const stream = await LLM.stream({ + user, + sessionID, + model: resolved, + agent, + system: ["You are a helpful assistant."], + abort: new AbortController().signal, + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Describe this image" }, + { + type: "file", + mediaType: "image/png", + filename: "large-image.png", + data: image, + }, + ], + }, + ] as ModelMessage[], + tools: {}, + }) + + for await (const _ of stream.fullStream) { + } + + const capture = await request + expect(capture.url.pathname.endsWith("/responses")).toBe(true) + }, + }) + }) + test("sends messages API payload for Anthropic models", async () => { const server = state.server if (!server) { @@ -625,7 +748,7 @@ describe("session.llm.stream", () => { role: "user", time: { created: Date.now() }, agent: agent.name, - model: { providerID: ProviderID.make(providerID), modelID: resolved.id }, + model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.7") }, } satisfies MessageV2.User const stream = await LLM.stream({ diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 7d416597a8..3634d6fb7e 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -108,7 +108,7 @@ function basePart(messageID: string, id: string) { } describe("session.message-v2.toModelMessage", () => { - test("filters out messages with no parts", () => { + test("filters out messages with no parts", async () => { const input: MessageV2.WithParts[] = [ { info: userInfo("m-empty"), @@ -126,7 +126,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "hello" }], @@ -134,7 +134,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("filters out messages with only ignored parts", () => { + test("filters out messages with only ignored parts", async () => { const messageID = "m-user" const input: MessageV2.WithParts[] = [ @@ -151,10 +151,10 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([]) + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([]) }) - test("includes synthetic text parts", () => { + test("includes synthetic text parts", async () => { const messageID = "m-user" const input: MessageV2.WithParts[] = [ @@ -182,7 +182,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "hello" }], @@ -194,7 +194,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("converts user text/file parts and injects compaction/subtask prompts", () => { + test("converts user text/file parts and injects compaction/subtask prompts", async () => { const messageID = "m-user" const input: MessageV2.WithParts[] = [ @@ -249,7 +249,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [ @@ -267,7 +267,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("converts assistant tool completion into tool-call + tool-result messages with attachments", () => { + test("converts assistant tool completion into tool-call + tool-result messages with attachments", async () => { const userID = "m-user" const assistantID = "m-assistant" @@ -319,7 +319,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], @@ -359,7 +359,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("omits provider metadata when assistant model differs", () => { + test("omits provider metadata when assistant model differs", async () => { const userID = "m-user" const assistantID = "m-assistant" @@ -402,7 +402,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], @@ -434,7 +434,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("replaces compacted tool output with placeholder", () => { + test("replaces compacted tool output with placeholder", async () => { const userID = "m-user" const assistantID = "m-assistant" @@ -470,7 +470,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], @@ -501,7 +501,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("converts assistant tool error into error-text tool result", () => { + test("converts assistant tool error into error-text tool result", async () => { const userID = "m-user" const assistantID = "m-assistant" @@ -537,7 +537,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "user", content: [{ type: "text", text: "run tool" }], @@ -570,7 +570,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("filters assistant messages with non-abort errors", () => { + test("filters assistant messages with non-abort errors", async () => { const assistantID = "m-assistant" const input: MessageV2.WithParts[] = [ @@ -590,10 +590,10 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([]) + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([]) }) - test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => { + test("includes aborted assistant messages only when they have non-step-start/reasoning content", async () => { const assistantID1 = "m-assistant-1" const assistantID2 = "m-assistant-2" @@ -633,7 +633,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "assistant", content: [ @@ -644,7 +644,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("splits assistant messages on step-start boundaries", () => { + test("splits assistant messages on step-start boundaries", async () => { const assistantID = "m-assistant" const input: MessageV2.WithParts[] = [ @@ -669,7 +669,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([ + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([ { role: "assistant", content: [{ type: "text", text: "first" }], @@ -681,7 +681,7 @@ describe("session.message-v2.toModelMessage", () => { ]) }) - test("drops messages that only contain step-start parts", () => { + test("drops messages that only contain step-start parts", async () => { const assistantID = "m-assistant" const input: MessageV2.WithParts[] = [ @@ -696,10 +696,10 @@ describe("session.message-v2.toModelMessage", () => { }, ] - expect(MessageV2.toModelMessages(input, model)).toStrictEqual([]) + expect(await MessageV2.toModelMessages(input, model)).toStrictEqual([]) }) - test("converts pending/running tool calls to error results to prevent dangling tool_use", () => { + test("converts pending/running tool calls to error results to prevent dangling tool_use", async () => { const userID = "m-user" const assistantID = "m-assistant" @@ -743,7 +743,7 @@ describe("session.message-v2.toModelMessage", () => { }, ] - const result = MessageV2.toModelMessages(input, model) + const result = await MessageV2.toModelMessages(input, model) expect(result).toStrictEqual([ { diff --git a/packages/opencode/test/session/structured-output.test.ts b/packages/opencode/test/session/structured-output.test.ts index f6131b149b..db3f8cfded 100644 --- a/packages/opencode/test/session/structured-output.test.ts +++ b/packages/opencode/test/session/structured-output.test.ts @@ -363,20 +363,25 @@ describe("structured-output.createStructuredOutputTool", () => { expect(inputSchema.jsonSchema?.properties?.tags?.items?.type).toBe("string") }) - test("toModelOutput returns text value", () => { + test("toModelOutput returns text value", async () => { const tool = SessionPrompt.createStructuredOutputTool({ schema: { type: "object" }, onSuccess: () => {}, }) expect(tool.toModelOutput).toBeDefined() - const modelOutput = tool.toModelOutput!({ - output: "Test output", - title: "Test", - metadata: { valid: true }, - }) + const modelOutput = await Promise.resolve( + tool.toModelOutput!({ + toolCallId: "test-call-id", + input: {}, + output: { + output: "Test output", + }, + }), + ) expect(modelOutput.type).toBe("text") + if (modelOutput.type !== "text") throw new Error("expected text model output") expect(modelOutput.value).toBe("Test output") }) diff --git a/patches/@ai-sdk%2Fanthropic@3.0.64.patch b/patches/@ai-sdk%2Fanthropic@3.0.64.patch new file mode 100644 index 0000000000..b8c2f387d7 --- /dev/null +++ b/patches/@ai-sdk%2Fanthropic@3.0.64.patch @@ -0,0 +1,119 @@ +--- a/dist/index.js ++++ b/dist/index.js +@@ -3155,15 +3155,6 @@ + }); + } + baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); +- } else { +- if (topP != null && temperature != null) { +- warnings.push({ +- type: "unsupported", +- feature: "topP", +- details: `topP is not supported when temperature is set. topP is ignored.` +- }); +- baseArgs.top_p = void 0; +- } + } + if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { + if (maxOutputTokens != null) { +@@ -5180,4 +5171,4 @@ + createAnthropic, + forwardAnthropicContainerIdFromLastStep + }); +-//# sourceMappingURL=index.js.map +\ No newline at end of file ++//# sourceMappingURL=index.js.map +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -3192,15 +3192,6 @@ + }); + } + baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); +- } else { +- if (topP != null && temperature != null) { +- warnings.push({ +- type: "unsupported", +- feature: "topP", +- details: `topP is not supported when temperature is set. topP is ignored.` +- }); +- baseArgs.top_p = void 0; +- } + } + if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { + if (maxOutputTokens != null) { +@@ -5256,4 +5247,4 @@ + createAnthropic, + forwardAnthropicContainerIdFromLastStep + }; +-//# sourceMappingURL=index.mjs.map +\ No newline at end of file ++//# sourceMappingURL=index.mjs.map +--- a/dist/internal/index.js ++++ b/dist/internal/index.js +@@ -3147,15 +3147,6 @@ + }); + } + baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); +- } else { +- if (topP != null && temperature != null) { +- warnings.push({ +- type: "unsupported", +- feature: "topP", +- details: `topP is not supported when temperature is set. topP is ignored.` +- }); +- baseArgs.top_p = void 0; +- } + } + if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { + if (maxOutputTokens != null) { +@@ -5080,4 +5071,4 @@ + anthropicTools, + prepareTools + }); +-//# sourceMappingURL=index.js.map +\ No newline at end of file ++//# sourceMappingURL=index.js.map +--- a/dist/internal/index.mjs ++++ b/dist/internal/index.mjs +@@ -3176,15 +3176,6 @@ + }); + } + baseArgs.max_tokens = maxTokens + (thinkingBudget != null ? thinkingBudget : 0); +- } else { +- if (topP != null && temperature != null) { +- warnings.push({ +- type: "unsupported", +- feature: "topP", +- details: `topP is not supported when temperature is set. topP is ignored.` +- }); +- baseArgs.top_p = void 0; +- } + } + if (isKnownModel && baseArgs.max_tokens > maxOutputTokensForModel) { + if (maxOutputTokens != null) { +@@ -5148,4 +5139,4 @@ + anthropicTools, + prepareTools + }; +-//# sourceMappingURL=index.mjs.map +\ No newline at end of file ++//# sourceMappingURL=index.mjs.map +--- a/src/anthropic-messages-language-model.ts ++++ b/src/anthropic-messages-language-model.ts +@@ -534,16 +534,6 @@ + + // adjust max tokens to account for thinking: + baseArgs.max_tokens = maxTokens + (thinkingBudget ?? 0); +- } else { +- // Only check temperature/topP mutual exclusivity when thinking is not enabled +- if (topP != null && temperature != null) { +- warnings.push({ +- type: 'unsupported', +- feature: 'topP', +- details: `topP is not supported when temperature is set. topP is ignored.`, +- }); +- baseArgs.top_p = undefined; +- } + } + + // limit to max output tokens for known models to enable model switching without breaking it: diff --git a/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch b/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch new file mode 100644 index 0000000000..b93092c466 --- /dev/null +++ b/patches/@ai-sdk%2Fprovider-utils@4.0.21.patch @@ -0,0 +1,61 @@ +diff --git a/dist/index.js b/dist/index.js +index 9aa8e83684777e860d905ff7a6895995a7347a4f..820797581ac2a33e731e139da3ebc98b4d93fdcf 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -395,10 +395,13 @@ function validateDownloadUrl(url) { + message: `Invalid URL: ${url}` + }); + } ++ if (parsed.protocol === "data:") { ++ return; ++ } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new DownloadError({ + url, +- message: `URL scheme must be http or https, got ${parsed.protocol}` ++ message: `URL scheme must be http, https, or data, got ${parsed.protocol}` + }); + } + const hostname = parsed.hostname; +diff --git a/dist/index.mjs b/dist/index.mjs +index 095fdc188b1d7f227b42591c78ecb71fe2e2cf8b..ca5227d3b6e358aea8ecd85782a0a2b48130a2c9 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -299,10 +299,13 @@ function validateDownloadUrl(url) { + message: `Invalid URL: ${url}` + }); + } ++ if (parsed.protocol === "data:") { ++ return; ++ } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new DownloadError({ + url, +- message: `URL scheme must be http or https, got ${parsed.protocol}` ++ message: `URL scheme must be http, https, or data, got ${parsed.protocol}` + }); + } + const hostname = parsed.hostname; +diff --git a/src/validate-download-url.ts b/src/validate-download-url.ts +index 7c026ad6b400aef551ce3a424c343e1cedc60997..6a2f11398e58f80a8e11995ac1ce5f4d7c110561 100644 +--- a/src/validate-download-url.ts ++++ b/src/validate-download-url.ts +@@ -18,11 +18,16 @@ export function validateDownloadUrl(url: string): void { + }); + } + +- // Only allow http and https protocols ++ // data: URLs are inline content and do not make network requests. ++ if (parsed.protocol === 'data:') { ++ return; ++ } ++ ++ // Only allow http and https network protocols + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new DownloadError({ + url, +- message: `URL scheme must be http or https, got ${parsed.protocol}`, ++ message: `URL scheme must be http, https, or data, got ${parsed.protocol}`, + }); + } + diff --git a/patches/@ai-sdk%2Fxai@2.0.51.patch b/patches/@ai-sdk%2Fxai@2.0.51.patch deleted file mode 100644 index 8776cab483..0000000000 --- a/patches/@ai-sdk%2Fxai@2.0.51.patch +++ /dev/null @@ -1,108 +0,0 @@ -diff --git a/dist/index.mjs b/dist/index.mjs ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -959,7 +959,7 @@ - model: z4.string().nullish(), - object: z4.literal("response"), - output: z4.array(outputItemSchema), -- usage: xaiResponsesUsageSchema, -+ usage: xaiResponsesUsageSchema.nullish(), - status: z4.string() - }); - var xaiResponsesChunkSchema = z4.union([ -\ No newline at end of file -@@ -1143,6 +1143,18 @@ - z4.object({ - type: z4.literal("response.completed"), - response: xaiResponsesResponseSchema -+ }), -+ z4.object({ -+ type: z4.literal("response.function_call_arguments.delta"), -+ item_id: z4.string(), -+ output_index: z4.number(), -+ delta: z4.string() -+ }), -+ z4.object({ -+ type: z4.literal("response.function_call_arguments.done"), -+ item_id: z4.string(), -+ output_index: z4.number(), -+ arguments: z4.string() - }) - ]); - -\ No newline at end of file -@@ -1940,6 +1952,9 @@ - if (response2.status) { - finishReason = mapXaiResponsesFinishReason(response2.status); - } -+ if (seenToolCalls.size > 0 && finishReason !== "tool-calls") { -+ finishReason = "tool-calls"; -+ } - return; - } - if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { -\ No newline at end of file -@@ -2024,7 +2039,7 @@ - } - } - } else if (part.type === "function_call") { -- if (!seenToolCalls.has(part.call_id)) { -+ if (event.type === "response.output_item.done" && !seenToolCalls.has(part.call_id)) { - seenToolCalls.add(part.call_id); - controller.enqueue({ - type: "tool-input-start", -\ No newline at end of file -diff --git a/dist/index.js b/dist/index.js ---- a/dist/index.js -+++ b/dist/index.js -@@ -964,7 +964,7 @@ - model: import_v44.z.string().nullish(), - object: import_v44.z.literal("response"), - output: import_v44.z.array(outputItemSchema), -- usage: xaiResponsesUsageSchema, -+ usage: xaiResponsesUsageSchema.nullish(), - status: import_v44.z.string() - }); - var xaiResponsesChunkSchema = import_v44.z.union([ -\ No newline at end of file -@@ -1148,6 +1148,18 @@ - import_v44.z.object({ - type: import_v44.z.literal("response.completed"), - response: xaiResponsesResponseSchema -+ }), -+ import_v44.z.object({ -+ type: import_v44.z.literal("response.function_call_arguments.delta"), -+ item_id: import_v44.z.string(), -+ output_index: import_v44.z.number(), -+ delta: import_v44.z.string() -+ }), -+ import_v44.z.object({ -+ type: import_v44.z.literal("response.function_call_arguments.done"), -+ item_id: import_v44.z.string(), -+ output_index: import_v44.z.number(), -+ arguments: import_v44.z.string() - }) - ]); - -\ No newline at end of file -@@ -1935,6 +1947,9 @@ - if (response2.status) { - finishReason = mapXaiResponsesFinishReason(response2.status); - } -+ if (seenToolCalls.size > 0 && finishReason !== "tool-calls") { -+ finishReason = "tool-calls"; -+ } - return; - } - if (event.type === "response.output_item.added" || event.type === "response.output_item.done") { -\ No newline at end of file -@@ -2019,7 +2034,7 @@ - } - } - } else if (part.type === "function_call") { -- if (!seenToolCalls.has(part.call_id)) { -+ if (event.type === "response.output_item.done" && !seenToolCalls.has(part.call_id)) { - seenToolCalls.add(part.call_id); - controller.enqueue({ - type: "tool-input-start", -\ No newline at end of file diff --git a/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch b/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch deleted file mode 100644 index 6226bf790c..0000000000 --- a/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch +++ /dev/null @@ -1,128 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index f33510a50d11a2cb92a90ea70cc0ac84c89f29b9..e887a60352c0c08ab794b1e6821854dfeefd20cc 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -2110,7 +2110,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted && !textStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - reasoningStarted = false; - } -@@ -2307,7 +2312,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - } - if (textStarted) { -diff --git a/dist/index.mjs b/dist/index.mjs -index 8a688331b88b4af738ee4ca8062b5f24124d3d81..6310cb8b7c8d0a728d86e1eed09906c6b4c91ae2 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -2075,7 +2075,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted && !textStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - reasoningStarted = false; - } -@@ -2272,7 +2277,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - } - if (textStarted) { -diff --git a/dist/internal/index.js b/dist/internal/index.js -index d40fa66125941155ac13a4619503caba24d89f8a..8dd86d1b473f2fa31c1acd9881d72945b294a197 100644 ---- a/dist/internal/index.js -+++ b/dist/internal/index.js -@@ -2064,7 +2064,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted && !textStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - reasoningStarted = false; - } -@@ -2261,7 +2266,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - } - if (textStarted) { -diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs -index b0ed9d113549c5c55ea3b1e08abb3db6f92ae5a7..5695930a8e038facc071d58a4179a369a29be9c7 100644 ---- a/dist/internal/index.mjs -+++ b/dist/internal/index.mjs -@@ -2030,7 +2030,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted && !textStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - reasoningStarted = false; - } -@@ -2227,7 +2232,12 @@ var OpenRouterChatLanguageModel = class { - if (reasoningStarted) { - controller.enqueue({ - type: "reasoning-end", -- id: reasoningId || generateId() -+ id: reasoningId || generateId(), -+ providerMetadata: accumulatedReasoningDetails.length > 0 ? { -+ openrouter: { -+ reasoning_details: accumulatedReasoningDetails -+ } -+ } : undefined - }); - } - if (textStarted) { From e5f0e813b6e2f9305fc27d432689f95a56beea51 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 27 Mar 2026 16:25:47 -0400 Subject: [PATCH 013/138] refactor(session): effectify Session service (#19449) --- packages/opencode/src/session/index.ts | 897 ++++++++++++++----------- 1 file changed, 502 insertions(+), 395 deletions(-) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 6102b7b413..eb01739c15 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -33,6 +33,8 @@ import { Permission } from "@/permission" import { Global } from "@/global" import type { LanguageModelV2Usage } from "@ai-sdk/provider" import { iife } from "@/util/iife" +import { Effect, Layer, Scope, ServiceMap } from "effect" +import { makeRuntime } from "@/effect/run-service" export namespace Session { const log = Log.create({ service: "session" }) @@ -233,6 +235,473 @@ export namespace Session { ), } + export function plan(input: { slug: string; time: { created: number } }) { + const base = Instance.project.vcs + ? path.join(Instance.worktree, ".opencode", "plans") + : path.join(Global.Path.data, "plans") + return path.join(base, [input.time.created, input.slug].join("-") + ".md") + } + + export const getUsage = (input: { + model: Provider.Model + usage: LanguageModelV2Usage + metadata?: ProviderMetadata + }) => { + const safe = (value: number) => { + if (!Number.isFinite(value)) return 0 + return value + } + const inputTokens = safe(input.usage.inputTokens ?? 0) + const outputTokens = safe(input.usage.outputTokens ?? 0) + const reasoningTokens = safe(input.usage.reasoningTokens ?? 0) + + const cacheReadInputTokens = safe(input.usage.cachedInputTokens ?? 0) + const cacheWriteInputTokens = safe( + (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? + // @ts-expect-error + input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? + // @ts-expect-error + input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? + 0) as number, + ) + + // OpenRouter provides inputTokens as the total count of input tokens (including cached). + // AFAIK other providers (OpenRouter/OpenAI/Gemini etc.) do it the same way e.g. vercel/ai#8794 (comment) + // Anthropic does it differently though - inputTokens doesn't include cached tokens. + // It looks like OpenCode's cost calculation assumes all providers return inputTokens the same way Anthropic does (I'm guessing getUsage logic was originally implemented with anthropic), so it's causing incorrect cost calculation for OpenRouter and others. + const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"]) + const adjustedInputTokens = safe( + excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens, + ) + + const total = iife(() => { + // Anthropic doesn't provide total_tokens, also ai sdk will vastly undercount if we + // don't compute from components + if ( + input.model.api.npm === "@ai-sdk/anthropic" || + input.model.api.npm === "@ai-sdk/amazon-bedrock" || + input.model.api.npm === "@ai-sdk/google-vertex/anthropic" + ) { + return adjustedInputTokens + outputTokens + cacheReadInputTokens + cacheWriteInputTokens + } + return input.usage.totalTokens + }) + + const tokens = { + total, + input: adjustedInputTokens, + output: outputTokens, + reasoning: reasoningTokens, + cache: { + write: cacheWriteInputTokens, + read: cacheReadInputTokens, + }, + } + + const costInfo = + input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 + ? input.model.cost.experimentalOver200K + : input.model.cost + return { + cost: safe( + new Decimal(0) + .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) + .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) + .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) + // TODO: update models.dev to have better pricing model, for now: + // charge reasoning tokens at the same rate as output tokens + .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) + .toNumber(), + ), + tokens, + } + } + + export class BusyError extends Error { + constructor(public readonly sessionID: string) { + super(`Session ${sessionID} is busy`) + } + } + + export interface Interface { + readonly create: (input?: { + parentID?: SessionID + title?: string + permission?: Permission.Ruleset + workspaceID?: WorkspaceID + }) => Effect.Effect + readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect + readonly touch: (sessionID: SessionID) => Effect.Effect + readonly get: (id: SessionID) => Effect.Effect + readonly share: (id: SessionID) => Effect.Effect<{ url: string }> + readonly unshare: (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 + readonly setRevert: (input: { + sessionID: SessionID + revert: Info["revert"] + summary: Info["summary"] + }) => Effect.Effect + readonly clearRevert: (sessionID: SessionID) => Effect.Effect + readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect + 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 updateMessage: (msg: MessageV2.Info) => Effect.Effect + readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect + readonly removePart: (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + }) => Effect.Effect + readonly updatePart: (part: MessageV2.Part) => Effect.Effect + readonly updatePartDelta: (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + field: string + delta: string + }) => Effect.Effect + readonly initialize: (input: { + sessionID: SessionID + modelID: ModelID + providerID: ProviderID + messageID: MessageID + }) => Effect.Effect + } + + export class Service extends ServiceMap.Service()("@opencode/Session") {} + + type Patch = z.infer["info"] + + const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => + Effect.sync(() => Database.use(fn)) + + export const layer: Layer.Layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const config = yield* Config.Service + const scope = yield* Scope.Scope + + const createNext = Effect.fn("Session.createNext")(function* (input: { + id?: SessionID + title?: string + parentID?: SessionID + workspaceID?: WorkspaceID + directory: string + permission?: Permission.Ruleset + }) { + const result: Info = { + id: SessionID.descending(input.id), + slug: Slug.create(), + version: Installation.VERSION, + projectID: Instance.project.id, + directory: input.directory, + workspaceID: input.workspaceID, + parentID: input.parentID, + title: input.title ?? createDefaultTitle(!!input.parentID), + permission: input.permission, + time: { + created: Date.now(), + updated: Date.now(), + }, + } + log.info("created", result) + + yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result })) + + const cfg = yield* config.get() + if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) { + yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope)) + } + + if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { + // This only exist for backwards compatibility. We should not be + // manually publishing this event; it is a sync event now + yield* bus.publish(Event.Updated, { + sessionID: result.id, + info: result, + }) + } + + return result + }) + + const get = Effect.fn("Session.get")(function* (id: SessionID) { + const row = yield* db((d) => d.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) + if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) + return fromRow(row) + }) + + const share = Effect.fn("Session.share")(function* (id: SessionID) { + const cfg = yield* config.get() + if (cfg.share === "disabled") throw new Error("Sharing is disabled in configuration") + const result = yield* Effect.promise(async () => { + const { ShareNext } = await import("@/share/share-next") + return ShareNext.create(id) + }) + yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: result.url } } })) + return result + }) + + const unshare = Effect.fn("Session.unshare")(function* (id: SessionID) { + yield* Effect.promise(async () => { + const { ShareNext } = await import("@/share/share-next") + await ShareNext.remove(id) + }) + yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: null } } })) + }) + + const children = Effect.fn("Session.children")(function* (parentID: SessionID) { + const project = Instance.project + const rows = yield* db((d) => + d + .select() + .from(SessionTable) + .where(and(eq(SessionTable.project_id, project.id), eq(SessionTable.parent_id, parentID))) + .all(), + ) + return rows.map(fromRow) + }) + + const remove: (sessionID: SessionID) => Effect.Effect = Effect.fnUntraced(function* (sessionID: SessionID) { + try { + const session = yield* get(sessionID) + const kids = yield* children(sessionID) + for (const child of kids) { + yield* remove(child.id) + } + yield* unshare(sessionID).pipe(Effect.ignore) + yield* Effect.sync(() => { + SyncEvent.run(Event.Deleted, { sessionID, info: session }) + SyncEvent.remove(sessionID) + }) + } catch (e) { + log.error(e) + } + }) + + const updateMessage = Effect.fn("Session.updateMessage")(function* (msg: MessageV2.Info) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.Updated, { + sessionID: msg.sessionID, + info: msg, + }), + ) + return msg + }) + + const updatePart = Effect.fn("Session.updatePart")(function* (part: MessageV2.Part) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.PartUpdated, { + sessionID: part.sessionID, + part: structuredClone(part), + time: Date.now(), + }), + ) + return part + }) + + const create = Effect.fn("Session.create")(function* (input?: { + parentID?: SessionID + title?: string + permission?: Permission.Ruleset + workspaceID?: WorkspaceID + }) { + return yield* createNext({ + parentID: input?.parentID, + directory: Instance.directory, + title: input?.title, + permission: input?.permission, + workspaceID: input?.workspaceID, + }) + }) + + const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { + const original = yield* get(input.sessionID) + const title = getForkedTitle(original.title) + const session = yield* createNext({ + directory: Instance.directory, + workspaceID: original.workspaceID, + title, + }) + const msgs = yield* messages({ sessionID: input.sessionID }) + const idMap = new Map() + + for (const msg of msgs) { + if (input.messageID && msg.info.id >= input.messageID) break + const newID = MessageID.ascending() + idMap.set(msg.info.id, newID) + + const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined + const cloned = yield* updateMessage({ + ...msg.info, + sessionID: session.id, + id: newID, + ...(parentID && { parentID }), + }) + + for (const part of msg.parts) { + yield* updatePart({ + ...part, + id: PartID.ascending(), + messageID: cloned.id, + sessionID: session.id, + }) + } + } + return session + }) + + const patch = (sessionID: SessionID, info: Patch) => + Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID, info })) + + const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) { + yield* patch(sessionID, { time: { updated: Date.now() } }) + }) + + const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) { + yield* patch(input.sessionID, { title: input.title }) + }) + + const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) { + yield* patch(input.sessionID, { time: { archived: input.time } }) + }) + + const setPermission = Effect.fn("Session.setPermission")(function* (input: { + sessionID: SessionID + permission: Permission.Ruleset + }) { + yield* patch(input.sessionID, { permission: input.permission, time: { updated: Date.now() } }) + }) + + const setRevert = Effect.fn("Session.setRevert")(function* (input: { + sessionID: SessionID + revert: Info["revert"] + summary: Info["summary"] + }) { + yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert }) + }) + + const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) { + yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }) + }) + + const setSummary = Effect.fn("Session.setSummary")(function* (input: { + sessionID: SessionID + summary: Info["summary"] + }) { + yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }) + }) + + const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) { + return yield* Effect.tryPromise(() => Storage.read(["session_diff", sessionID])).pipe( + Effect.orElseSucceed(() => [] as Snapshot.FileDiff[]), + ) + }) + + const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) { + return yield* Effect.promise(async () => { + const result = [] as MessageV2.WithParts[] + for await (const msg of MessageV2.stream(input.sessionID)) { + if (input.limit && result.length >= input.limit) break + result.push(msg) + } + result.reverse() + return result + }) + }) + + const removeMessage = Effect.fn("Session.removeMessage")(function* (input: { + sessionID: SessionID + messageID: MessageID + }) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.Removed, { + sessionID: input.sessionID, + messageID: input.messageID, + }), + ) + return input.messageID + }) + + const removePart = Effect.fn("Session.removePart")(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + }) { + yield* Effect.sync(() => + SyncEvent.run(MessageV2.Event.PartRemoved, { + sessionID: input.sessionID, + messageID: input.messageID, + partID: input.partID, + }), + ) + return input.partID + }) + + const updatePartDelta = Effect.fn("Session.updatePartDelta")(function* (input: { + sessionID: SessionID + messageID: MessageID + partID: PartID + field: string + delta: string + }) { + yield* bus.publish(MessageV2.Event.PartDelta, input) + }) + + const initialize = Effect.fn("Session.initialize")(function* (input: { + sessionID: SessionID + modelID: ModelID + providerID: ProviderID + messageID: MessageID + }) { + yield* Effect.promise(() => + SessionPrompt.command({ + sessionID: input.sessionID, + messageID: input.messageID, + model: input.providerID + "/" + input.modelID, + command: Command.Default.INIT, + arguments: "", + }), + ) + }) + + return Service.of({ + create, + fork, + touch, + get, + share, + unshare, + setTitle, + setArchived, + setPermission, + setRevert, + clearRevert, + setSummary, + diff, + messages, + children, + remove, + updateMessage, + removeMessage, + removePart, + updatePart, + updatePartDelta, + initialize, + }) + }), + ) + + export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer)) + + const { runPromise } = makeRuntime(Service, defaultLayer) + export const create = fn( z .object({ @@ -242,244 +711,46 @@ export namespace Session { workspaceID: WorkspaceID.zod.optional(), }) .optional(), - async (input) => { - return createNext({ - parentID: input?.parentID, - directory: Instance.directory, - title: input?.title, - permission: input?.permission, - workspaceID: input?.workspaceID, - }) - }, + (input) => runPromise((svc) => svc.create(input)), ) - export const fork = fn( - z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod.optional(), - }), - async (input) => { - const original = await get(input.sessionID) - if (!original) throw new Error("session not found") - const title = getForkedTitle(original.title) - const session = await createNext({ - directory: Instance.directory, - workspaceID: original.workspaceID, - title, - }) - const msgs = await messages({ sessionID: input.sessionID }) - const idMap = new Map() - - for (const msg of msgs) { - if (input.messageID && msg.info.id >= input.messageID) break - const newID = MessageID.ascending() - idMap.set(msg.info.id, newID) - - const parentID = msg.info.role === "assistant" && msg.info.parentID ? idMap.get(msg.info.parentID) : undefined - const cloned = await updateMessage({ - ...msg.info, - sessionID: session.id, - id: newID, - ...(parentID && { parentID }), - }) - - for (const part of msg.parts) { - await updatePart({ - ...part, - id: PartID.ascending(), - messageID: cloned.id, - sessionID: session.id, - }) - } - } - return session - }, + export const fork = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }), (input) => + runPromise((svc) => svc.fork(input)), ) - export const touch = fn(SessionID.zod, async (sessionID) => { - const time = Date.now() - SyncEvent.run(Event.Updated, { sessionID, info: { time: { updated: time } } }) - }) + export const touch = fn(SessionID.zod, (id) => runPromise((svc) => svc.touch(id))) + export const get = fn(SessionID.zod, (id) => runPromise((svc) => svc.get(id))) + export const share = fn(SessionID.zod, (id) => runPromise((svc) => svc.share(id))) + export const unshare = fn(SessionID.zod, (id) => runPromise((svc) => svc.unshare(id))) - export async function createNext(input: { - id?: SessionID - title?: string - parentID?: SessionID - workspaceID?: WorkspaceID - directory: string - permission?: Permission.Ruleset - }) { - const result: Info = { - id: SessionID.descending(input.id), - slug: Slug.create(), - version: Installation.VERSION, - projectID: Instance.project.id, - directory: input.directory, - workspaceID: input.workspaceID, - parentID: input.parentID, - title: input.title ?? createDefaultTitle(!!input.parentID), - permission: input.permission, - time: { - created: Date.now(), - updated: Date.now(), - }, - } - log.info("created", result) - - SyncEvent.run(Event.Created, { sessionID: result.id, info: result }) - - const cfg = await Config.get() - if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) { - share(result.id).catch(() => { - // Silently ignore sharing errors during session creation - }) - } - - if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { - // This only exist for backwards compatibility. We should not be - // manually publishing this event; it is a sync event now - Bus.publish(Event.Updated, { - sessionID: result.id, - info: result, - }) - } - - return result - } - - export function plan(input: { slug: string; time: { created: number } }) { - const base = Instance.project.vcs - ? path.join(Instance.worktree, ".opencode", "plans") - : path.join(Global.Path.data, "plans") - return path.join(base, [input.time.created, input.slug].join("-") + ".md") - } - - export const get = fn(SessionID.zod, async (id) => { - const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get()) - if (!row) throw new NotFoundError({ message: `Session not found: ${id}` }) - return fromRow(row) - }) - - export const share = fn(SessionID.zod, async (id) => { - const cfg = await Config.get() - if (cfg.share === "disabled") { - throw new Error("Sharing is disabled in configuration") - } - const { ShareNext } = await import("@/share/share-next") - const share = await ShareNext.create(id) - - SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: share.url } } }) - - return share - }) - - export const unshare = fn(SessionID.zod, async (id) => { - // Use ShareNext to remove the share (same as share function uses ShareNext to create) - const { ShareNext } = await import("@/share/share-next") - await ShareNext.remove(id) - - SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: null } } }) - }) - - export const setTitle = fn( - z.object({ - sessionID: SessionID.zod, - title: z.string(), - }), - async (input) => { - SyncEvent.run(Event.Updated, { sessionID: input.sessionID, info: { title: input.title } }) - }, + export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) => + runPromise((svc) => svc.setTitle(input)), ) - export const setArchived = fn( - z.object({ - sessionID: SessionID.zod, - time: z.number().optional(), - }), - async (input) => { - SyncEvent.run(Event.Updated, { sessionID: input.sessionID, info: { time: { archived: input.time } } }) - }, + export const setArchived = fn(z.object({ sessionID: SessionID.zod, time: z.number().optional() }), (input) => + runPromise((svc) => svc.setArchived(input)), ) - export const setPermission = fn( - z.object({ - sessionID: SessionID.zod, - permission: Permission.Ruleset, - }), - async (input) => { - SyncEvent.run(Event.Updated, { - sessionID: input.sessionID, - info: { permission: input.permission, time: { updated: Date.now() } }, - }) - }, + export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) => + runPromise((svc) => svc.setPermission(input)), ) export const setRevert = fn( - z.object({ - sessionID: SessionID.zod, - revert: Info.shape.revert, - summary: Info.shape.summary, - }), - async (input) => { - SyncEvent.run(Event.Updated, { - sessionID: input.sessionID, - info: { - summary: input.summary, - time: { updated: Date.now() }, - revert: input.revert, - }, - }) - }, + z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }), + (input) => + runPromise((svc) => svc.setRevert({ sessionID: input.sessionID, revert: input.revert, summary: input.summary })), ) - export const clearRevert = fn(SessionID.zod, async (sessionID) => { - SyncEvent.run(Event.Updated, { - sessionID, - info: { - time: { updated: Date.now() }, - revert: null, - }, - }) - }) + export const clearRevert = fn(SessionID.zod, (id) => runPromise((svc) => svc.clearRevert(id))) - export const setSummary = fn( - z.object({ - sessionID: SessionID.zod, - summary: Info.shape.summary, - }), - async (input) => { - SyncEvent.run(Event.Updated, { - sessionID: input.sessionID, - info: { - time: { updated: Date.now() }, - summary: input.summary, - }, - }) - }, + export const setSummary = fn(z.object({ sessionID: SessionID.zod, summary: Info.shape.summary }), (input) => + runPromise((svc) => svc.setSummary({ sessionID: input.sessionID, summary: input.summary })), ) - export const diff = fn(SessionID.zod, async (sessionID) => { - try { - return await Storage.read(["session_diff", sessionID]) - } catch { - return [] - } - }) + export const diff = fn(SessionID.zod, (id) => runPromise((svc) => svc.diff(id))) - export const messages = fn( - z.object({ - sessionID: SessionID.zod, - limit: z.number().optional(), - }), - async (input) => { - const result = [] as MessageV2.WithParts[] - for await (const msg of MessageV2.stream(input.sessionID)) { - if (input.limit && result.length >= input.limit) break - result.push(msg) - } - result.reverse() - return result - }, + export const messages = fn(z.object({ sessionID: SessionID.zod, limit: z.number().optional() }), (input) => + runPromise((svc) => svc.messages(input)), ) export function* list(input?: { @@ -594,84 +865,20 @@ export namespace Session { } } - export const children = fn(SessionID.zod, async (parentID) => { - const project = Instance.project - const rows = Database.use((db) => - db - .select() - .from(SessionTable) - .where(and(eq(SessionTable.project_id, project.id), eq(SessionTable.parent_id, parentID))) - .all(), - ) - return rows.map(fromRow) - }) + export const children = fn(SessionID.zod, (id) => runPromise((svc) => svc.children(id))) + export const remove = fn(SessionID.zod, (id) => runPromise((svc) => svc.remove(id))) + export const updateMessage = fn(MessageV2.Info, (msg) => runPromise((svc) => svc.updateMessage(msg))) - export const remove = fn(SessionID.zod, async (sessionID) => { - try { - const session = await get(sessionID) - for (const child of await children(sessionID)) { - await remove(child.id) - } - await unshare(sessionID).catch(() => {}) - - SyncEvent.run(Event.Deleted, { sessionID, info: session }) - - // Eagerly remove event sourcing data to free up space - SyncEvent.remove(sessionID) - } catch (e) { - log.error(e) - } - }) - - export const updateMessage = fn(MessageV2.Info, async (msg) => { - SyncEvent.run(MessageV2.Event.Updated, { - sessionID: msg.sessionID, - info: msg, - }) - - return msg - }) - - export const removeMessage = fn( - z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - }), - async (input) => { - SyncEvent.run(MessageV2.Event.Removed, { - sessionID: input.sessionID, - messageID: input.messageID, - }) - return input.messageID - }, + export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) => + runPromise((svc) => svc.removeMessage(input)), ) export const removePart = fn( - z.object({ - sessionID: SessionID.zod, - messageID: MessageID.zod, - partID: PartID.zod, - }), - async (input) => { - SyncEvent.run(MessageV2.Event.PartRemoved, { - sessionID: input.sessionID, - messageID: input.messageID, - partID: input.partID, - }) - return input.partID - }, + z.object({ sessionID: SessionID.zod, messageID: MessageID.zod, partID: PartID.zod }), + (input) => runPromise((svc) => svc.removePart(input)), ) - const UpdatePartInput = MessageV2.Part - - export const updatePart = fn(UpdatePartInput, async (part) => { - SyncEvent.run(MessageV2.Event.PartUpdated, { - sessionID: part.sessionID, - part: structuredClone(part), - time: Date.now(), - }) - return part - }) + export const updatePart = fn(MessageV2.Part, (part) => runPromise((svc) => svc.updatePart(part))) export const updatePartDelta = fn( z.object({ @@ -681,111 +888,11 @@ export namespace Session { field: z.string(), delta: z.string(), }), - async (input) => { - Bus.publish(MessageV2.Event.PartDelta, input) - }, + (input) => runPromise((svc) => svc.updatePartDelta(input)), ) - export const getUsage = fn( - z.object({ - model: z.custom(), - usage: z.custom(), - metadata: z.custom().optional(), - }), - (input) => { - const safe = (value: number) => { - if (!Number.isFinite(value)) return 0 - return value - } - const inputTokens = safe(input.usage.inputTokens ?? 0) - const outputTokens = safe(input.usage.outputTokens ?? 0) - const reasoningTokens = safe(input.usage.reasoningTokens ?? 0) - - const cacheReadInputTokens = safe(input.usage.cachedInputTokens ?? 0) - const cacheWriteInputTokens = safe( - (input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ?? - // @ts-expect-error - input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ?? - // @ts-expect-error - input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ?? - 0) as number, - ) - - // OpenRouter provides inputTokens as the total count of input tokens (including cached). - // AFAIK other providers (OpenRouter/OpenAI/Gemini etc.) do it the same way e.g. vercel/ai#8794 (comment) - // Anthropic does it differently though - inputTokens doesn't include cached tokens. - // It looks like OpenCode's cost calculation assumes all providers return inputTokens the same way Anthropic does (I'm guessing getUsage logic was originally implemented with anthropic), so it's causing incorrect cost calculation for OpenRouter and others. - const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"]) - const adjustedInputTokens = safe( - excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens, - ) - - const total = iife(() => { - // Anthropic doesn't provide total_tokens, also ai sdk will vastly undercount if we - // don't compute from components - if ( - input.model.api.npm === "@ai-sdk/anthropic" || - input.model.api.npm === "@ai-sdk/amazon-bedrock" || - input.model.api.npm === "@ai-sdk/google-vertex/anthropic" - ) { - return adjustedInputTokens + outputTokens + cacheReadInputTokens + cacheWriteInputTokens - } - return input.usage.totalTokens - }) - - const tokens = { - total, - input: adjustedInputTokens, - output: outputTokens, - reasoning: reasoningTokens, - cache: { - write: cacheWriteInputTokens, - read: cacheReadInputTokens, - }, - } - - const costInfo = - input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000 - ? input.model.cost.experimentalOver200K - : input.model.cost - return { - cost: safe( - new Decimal(0) - .add(new Decimal(tokens.input).mul(costInfo?.input ?? 0).div(1_000_000)) - .add(new Decimal(tokens.output).mul(costInfo?.output ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.read).mul(costInfo?.cache?.read ?? 0).div(1_000_000)) - .add(new Decimal(tokens.cache.write).mul(costInfo?.cache?.write ?? 0).div(1_000_000)) - // TODO: update models.dev to have better pricing model, for now: - // charge reasoning tokens at the same rate as output tokens - .add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000)) - .toNumber(), - ), - tokens, - } - }, - ) - - export class BusyError extends Error { - constructor(public readonly sessionID: string) { - super(`Session ${sessionID} is busy`) - } - } - export const initialize = fn( - z.object({ - sessionID: SessionID.zod, - modelID: ModelID.zod, - providerID: ProviderID.zod, - messageID: MessageID.zod, - }), - async (input) => { - await SessionPrompt.command({ - sessionID: input.sessionID, - messageID: input.messageID, - model: input.providerID + "/" + input.modelID, - command: Command.Default.INIT, - arguments: "", - }) - }, + z.object({ sessionID: SessionID.zod, modelID: ModelID.zod, providerID: ProviderID.zod, messageID: MessageID.zod }), + (input) => runPromise((svc) => svc.initialize(input)), ) } From 4b9660b211aa57477b6baa1848e582d3279f4db7 Mon Sep 17 00:00:00 2001 From: James Long Date: Fri, 27 Mar 2026 16:33:56 -0400 Subject: [PATCH 014/138] refactor(core): move more responsibility to workspace routing (#19455) --- .../src/control-plane/adaptors/worktree.ts | 12 +-- .../workspace-router-middleware.ts | 64 ------------ packages/opencode/src/server/instance.ts | 22 ----- packages/opencode/src/server/router.ts | 99 +++++++++++++++++++ packages/opencode/src/server/server.ts | 2 +- 5 files changed, 102 insertions(+), 97 deletions(-) delete mode 100644 packages/opencode/src/control-plane/workspace-router-middleware.ts create mode 100644 packages/opencode/src/server/router.ts diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index 2a96034d78..719748e3a1 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -32,15 +32,7 @@ export const WorktreeAdaptor: Adaptor = { const config = Config.parse(info) await Worktree.remove({ directory: config.directory }) }, - async fetch(info, input: RequestInfo | URL, init?: RequestInit) { - const { Server } = await import("../../server/server") - - const config = Config.parse(info) - const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal") - const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)) - headers.set("x-opencode-directory", config.directory) - - const request = new Request(url, { ...init, headers }) - return Server.Default().fetch(request) + async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) { + throw new Error("fetch not implemented") }, } diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts deleted file mode 100644 index 1fc19a22b1..0000000000 --- a/packages/opencode/src/control-plane/workspace-router-middleware.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { MiddlewareHandler } from "hono" -import { Flag } from "../flag/flag" -import { getAdaptor } from "./adaptors" -import { WorkspaceID } from "./schema" -import { Workspace } from "./workspace" -import { InstanceRoutes } from "../server/instance" -import { lazy } from "../util/lazy" - -type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } - -const RULES: Array = [ - { path: "/session/status", action: "forward" }, - { method: "GET", path: "/session", action: "local" }, -] - -function local(method: string, path: string) { - for (const rule of RULES) { - if (rule.method && rule.method !== method) continue - const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/") - if (match) return rule.action === "local" - } - return false -} - -const routes = lazy(() => InstanceRoutes()) - -export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { - if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) { - return routes().fetch(c.req.raw, c.env) - } - - const url = new URL(c.req.url) - const raw = url.searchParams.get("workspace") - - if (!raw) { - return routes().fetch(c.req.raw, c.env) - } - - if (local(c.req.method, url.pathname)) { - return routes().fetch(c.req.raw, c.env) - } - - const workspaceID = WorkspaceID.make(raw) - const workspace = await Workspace.get(workspaceID) - if (!workspace) { - return new Response(`Workspace not found: ${workspaceID}`, { - status: 500, - headers: { - "content-type": "text/plain; charset=utf-8", - }, - }) - } - - const adaptor = await getAdaptor(workspace.type) - const headers = new Headers(c.req.raw.headers) - headers.delete("x-opencode-workspace") - - return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { - method: c.req.method, - body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), - signal: c.req.raw.signal, - headers, - }) -} diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts index b99cf3d99f..4bb6efaf9b 100644 --- a/packages/opencode/src/server/instance.ts +++ b/packages/opencode/src/server/instance.ts @@ -14,7 +14,6 @@ import { Global } from "../global" import { LSP } from "../lsp" import { Command } from "../command" import { Flag } from "../flag/flag" -import { Filesystem } from "@/util/filesystem" import { QuestionRoutes } from "./routes/question" import { PermissionRoutes } from "./routes/permission" import { ProjectRoutes } from "./routes/project" @@ -26,7 +25,6 @@ import { ConfigRoutes } from "./routes/config" import { ExperimentalRoutes } from "./routes/experimental" import { ProviderRoutes } from "./routes/provider" import { EventRoutes } from "./routes/event" -import { InstanceBootstrap } from "../project/bootstrap" import { errorHandler } from "./middleware" const log = Log.create({ service: "server" }) @@ -45,26 +43,6 @@ const csp = (hash = "") => export const InstanceRoutes = (app?: Hono) => (app ?? new Hono()) .onError(errorHandler(log)) - .use(async (c, next) => { - const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() - const directory = Filesystem.resolve( - (() => { - try { - return decodeURIComponent(raw) - } catch { - return raw - } - })(), - ) - - return Instance.provide({ - directory, - init: InstanceBootstrap, - async fn() { - return next() - }, - }) - }) .route("/project", ProjectRoutes()) .route("/pty", PtyRoutes()) .route("/config", ConfigRoutes()) diff --git a/packages/opencode/src/server/router.ts b/packages/opencode/src/server/router.ts new file mode 100644 index 0000000000..f64180892e --- /dev/null +++ b/packages/opencode/src/server/router.ts @@ -0,0 +1,99 @@ +import type { MiddlewareHandler } from "hono" +import { getAdaptor } from "@/control-plane/adaptors" +import { WorkspaceID } from "@/control-plane/schema" +import { Workspace } from "@/control-plane/workspace" +import { lazy } from "@/util/lazy" +import { Filesystem } from "@/util/filesystem" +import { Instance } from "@/project/instance" +import { InstanceBootstrap } from "@/project/bootstrap" +import { InstanceRoutes } from "./instance" + +type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" } + +const RULES: Array = [ + { path: "/session/status", action: "forward" }, + { method: "GET", path: "/session", action: "local" }, +] + +function local(method: string, path: string) { + for (const rule of RULES) { + if (rule.method && rule.method !== method) continue + const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/") + if (match) return rule.action === "local" + } + return false +} + +const routes = lazy(() => InstanceRoutes()) + +export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => { + const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd() + const directory = Filesystem.resolve( + (() => { + try { + return decodeURIComponent(raw) + } catch { + return raw + } + })(), + ) + + const url = new URL(c.req.url) + const workspaceParam = url.searchParams.get("workspace") + + // TODO: If session is being routed, force it to lookup the + // project/workspace + + // If no workspace is provided we use the "project" workspace + if (!workspaceParam) { + return Instance.provide({ + directory, + init: InstanceBootstrap, + async fn() { + return routes().fetch(c.req.raw, c.env) + }, + }) + } + + const workspaceID = WorkspaceID.make(workspaceParam) + const workspace = await Workspace.get(workspaceID) + if (!workspace) { + return new Response(`Workspace not found: ${workspaceID}`, { + status: 500, + headers: { + "content-type": "text/plain; charset=utf-8", + }, + }) + } + + // Handle local workspaces directly so we can pass env to `fetch`, + // necessary for websocket upgrades + if (workspace.type === "worktree") { + return Instance.provide({ + directory: workspace.directory!, + init: InstanceBootstrap, + async fn() { + return routes().fetch(c.req.raw, c.env) + }, + }) + } + + // Remote workspaces + + if (local(c.req.method, url.pathname)) { + // No instance provided because we are serving cached data; there + // is no instance to work with + return routes().fetch(c.req.raw, c.env) + } + + const adaptor = await getAdaptor(workspace.type) + const headers = new Headers(c.req.raw.headers) + headers.delete("x-opencode-workspace") + + return adaptor.fetch(workspace, `${url.pathname}${url.search}`, { + method: c.req.method, + body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(), + signal: c.req.raw.signal, + headers, + }) +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index cfb22929bc..ec245ed59f 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -8,7 +8,7 @@ import z from "zod" import { Auth } from "../auth" import { Flag } from "../flag/flag" import { ProviderID } from "../provider/schema" -import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware" +import { WorkspaceRouterMiddleware } from "./router" import { websocket } from "hono/bun" import { errors } from "./error" import { GlobalRoutes } from "./routes/global" From c8909908f50afc3622d354cd8fd7a83dc3445706 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 27 Mar 2026 21:11:06 +0000 Subject: [PATCH 015/138] 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 4971aa4eb9..8b5f69d0d7 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-4XhUHjgqinKxOeT8K5hGAjpFA2vzOp8QpEg0uYCZwvg=", - "aarch64-linux": "sha256-X2YTNOpJocIkWkkfS8RnuDW+tvj4riHs7CXM+cS9iv0=", - "aarch64-darwin": "sha256-pN0rY+cpdW+6gNWeegVprdmhc2H72OZ9WxKDIs1fvJM=", - "x86_64-darwin": "sha256-l8+Yz/6UfSPJrdgfcqy/L2SvxN2i9Apv2R0B61rpEmw=" + "x86_64-linux": "sha256-aqmdiQeFREbUfRi3YX+ot4+CjykDuJpxYQH54W3hxME=", + "aarch64-linux": "sha256-ykJp6rFFwXkfJpMRJheTw+r495Wpmx5nj2LKxgSSVDw=", + "aarch64-darwin": "sha256-xHGM1rLld8sqkY+lhvec7fWkPPajIE403viIcpsFnk4=", + "x86_64-darwin": "sha256-QkGtT76P9Kf2+Ny0rI4CwMrIFzRIXiZwi8KS2o+jECU=" } } From 5cd54ec345f3dd501131f0c255d86ddfc8a90e07 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 27 Mar 2026 17:37:07 -0400 Subject: [PATCH 016/138] refactor(format): use ChildProcessSpawner instead of Process.spawn (#19457) --- packages/opencode/specs/effect-migration.md | 83 ++++++++++++++++++-- packages/opencode/src/format/index.ts | 65 ++++++++------- packages/opencode/test/format/format.test.ts | 10 +-- 3 files changed, 118 insertions(+), 40 deletions(-) diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md index f4acc6e52e..93b9cf8fb9 100644 --- a/packages/opencode/specs/effect-migration.md +++ b/packages/opencode/specs/effect-migration.md @@ -212,8 +212,81 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade): Still open and likely worth migrating: -- [ ] `Session` -- [ ] `SessionProcessor` -- [ ] `SessionPrompt` -- [ ] `SessionCompaction` -- [ ] `Provider` +- [x] `Session` — `session/index.ts` +- [ ] `SessionProcessor` — blocked by AI SDK v6 PR (#18433) +- [ ] `SessionPrompt` — blocked by AI SDK v6 PR (#18433) +- [ ] `SessionCompaction` — blocked by AI SDK v6 PR (#18433) +- [ ] `Provider` — blocked by AI SDK v6 PR (#18433) + +Other services not yet migrated: + +- [ ] `SessionSummary` — `session/summary.ts` +- [ ] `SessionTodo` — `session/todo.ts` +- [ ] `SessionRevert` — `session/revert.ts` +- [ ] `Instruction` — `session/instruction.ts` +- [ ] `ShareNext` — `share/share-next.ts` +- [ ] `SyncEvent` — `sync/index.ts` +- [ ] `Storage` — `storage/storage.ts` +- [ ] `Workspace` — `control-plane/workspace.ts` + +## Tool interface → Effect + +Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires: + +1. Migrate each tool to return Effects +2. Update `Tool.define()` factory to work with Effects +3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing — blocked by AI SDK v6 PR (#18433) + +Individual tools, ordered by value: + +- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events +- [ ] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream +- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock +- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling +- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events +- [ ] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout +- [ ] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient +- [ ] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient +- [ ] `batch.ts` — MEDIUM: parallel execution, per-call error recovery → Effect.all +- [ ] `task.ts` — MEDIUM: task state management +- [ ] `glob.ts` — LOW: simple async generator +- [ ] `lsp.ts` — LOW: dispatch switch over LSP operations +- [ ] `skill.ts` — LOW: skill tool adapter +- [ ] `plan.ts` — LOW: plan file operations + +## Effect service adoption in already-migrated code + +Some services are effectified but still use raw `Filesystem.*` or `Process.spawn` instead of the Effect equivalents. These are low-hanging fruit — the layers already exist, they just need the dependency swap. + +### `Filesystem.*` → `AppFileSystem.Service` (yield in layer) + +- [ ] `file/index.ts` — 11 calls (the File service itself) +- [ ] `config/config.ts` — 7 calls +- [ ] `auth/index.ts` — 3 calls +- [ ] `skill/index.ts` — 3 calls +- [ ] `file/time.ts` — 1 call + +### `Process.spawn` → `ChildProcessSpawner` (yield in layer) + +- [ ] `format/index.ts` — 1 call + +## Filesystem consolidation + +`util/filesystem.ts` (raw fs wrapper) is used by **64 files**. The effectified `AppFileSystem` service (`filesystem/index.ts`) exists but only has **8 consumers**. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` — this happens naturally during each migration, not as a separate effort. + +Similarly, **28 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched. + +Current raw fs users that will convert during tool migration: +- `tool/read.ts` — fs.createReadStream, readline +- `tool/apply_patch.ts` — fs/promises +- `tool/bash.ts` — fs/promises +- `file/ripgrep.ts` — fs/promises +- `storage/storage.ts` — fs/promises +- `patch/index.ts` — fs, fs/promises + +## Primitives & utilities + +- [ ] `util/lock.ts` — reader-writer lock → Effect Semaphore/Permit +- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer +- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise +- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 314e8c6e71..47b7d76b77 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,4 +1,6 @@ import { Effect, Layer, ServiceMap } from "effect" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" +import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import path from "path" @@ -6,7 +8,6 @@ import { mergeDeep } from "remeda" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" -import { Process } from "../util/process" import { Log } from "../util/log" import * as Formatter from "./formatter" @@ -36,6 +37,7 @@ export namespace Format { Service, Effect.gen(function* () { const config = yield* Config.Service + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const state = yield* InstanceState.make( Effect.fn("Format.state")(function* (_ctx) { @@ -98,38 +100,45 @@ export namespace Format { return checks.filter((x) => x.enabled).map((x) => x.item) } - async function formatFile(filepath: string) { - log.info("formatting", { file: filepath }) - const ext = path.extname(filepath) + function formatFile(filepath: string) { + return Effect.gen(function* () { + log.info("formatting", { file: filepath }) + const ext = path.extname(filepath) - for (const item of await getFormatter(ext)) { - log.info("running", { command: item.command }) - try { - const proc = Process.spawn( - item.command.map((x) => x.replace("$FILE", filepath)), - { - cwd: Instance.directory, - env: { ...process.env, ...item.environment }, - stdout: "ignore", - stderr: "ignore", - }, - ) - const exit = await proc.exited - if (exit !== 0) { + for (const item of yield* Effect.promise(() => getFormatter(ext))) { + log.info("running", { command: item.command }) + const cmd = item.command.map((x) => x.replace("$FILE", filepath)) + const code = yield* spawner + .spawn( + ChildProcess.make(cmd[0]!, cmd.slice(1), { + cwd: Instance.directory, + env: item.environment, + extendEnv: true, + }), + ) + .pipe( + Effect.flatMap((handle) => handle.exitCode), + Effect.scoped, + Effect.catch(() => + Effect.sync(() => { + log.error("failed to format file", { + error: "spawn failed", + command: item.command, + ...item.environment, + file: filepath, + }) + return ChildProcessSpawner.ExitCode(1) + }), + ), + ) + if (code !== 0) { log.error("failed", { command: item.command, ...item.environment, }) } - } catch (error) { - log.error("failed to format file", { - error, - command: item.command, - ...item.environment, - file: filepath, - }) } - } + }) } log.info("init") @@ -162,14 +171,14 @@ export namespace Format { const file = Effect.fn("Format.file")(function* (filepath: string) { const { formatFile } = yield* InstanceState.get(state) - yield* Effect.promise(() => formatFile(filepath)) + yield* formatFile(filepath) }) return Service.of({ init, status, file }) }), ) - export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer)) + export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer)) const { runPromise } = makeRuntime(Service, defaultLayer) diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index 6a9b4f5eda..74336e02a3 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -1,17 +1,13 @@ -import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node" +import { NodeFileSystem } from "@effect/platform-node" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" +import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Format } from "../../src/format" -import { Config } from "../../src/config/config" import * as Formatter from "../../src/format/formatter" -const node = NodeChildProcessSpawner.layer.pipe( - Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)), -) - -const it = testEffect(Layer.mergeAll(Format.layer, node).pipe(Layer.provide(Config.defaultLayer))) +const it = testEffect(Layer.mergeAll(Format.defaultLayer, CrossSpawnSpawner.defaultLayer, NodeFileSystem.layer)) describe("Format", () => { it.effect("status() returns built-in formatters when no config overrides", () => From 02b19bc3d733ee2e4220971fa421d4a6f05a9468 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 27 Mar 2026 21:38:08 +0000 Subject: [PATCH 017/138] chore: generate --- packages/opencode/specs/effect-migration.md | 1 + packages/opencode/src/format/index.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md index 93b9cf8fb9..38871356fd 100644 --- a/packages/opencode/specs/effect-migration.md +++ b/packages/opencode/specs/effect-migration.md @@ -277,6 +277,7 @@ Some services are effectified but still use raw `Filesystem.*` or `Process.spawn Similarly, **28 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched. Current raw fs users that will convert during tool migration: + - `tool/read.ts` — fs.createReadStream, readline - `tool/apply_patch.ts` — fs/promises - `tool/bash.ts` — fs/promises diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 47b7d76b77..8def248757 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -178,7 +178,10 @@ export namespace Format { }), ) - export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer)) + export const defaultLayer = layer.pipe( + Layer.provide(Config.defaultLayer), + Layer.provide(CrossSpawnSpawner.defaultLayer), + ) const { runPromise } = makeRuntime(Service, defaultLayer) From f3997d8082413c8b3a506d24fbfb3c58a0c3dedb Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 28 Mar 2026 00:44:46 +0100 Subject: [PATCH 018/138] Single target plugin entrypoints (#19467) --- .opencode/plugins/tui-smoke.tsx | 15 +++- packages/opencode/specs/tui-plugins.md | 14 +++- .../cli/cmd/tui/feature-plugins/home/tips.tsx | 6 +- .../tui/feature-plugins/sidebar/context.tsx | 6 +- .../cmd/tui/feature-plugins/sidebar/files.tsx | 6 +- .../tui/feature-plugins/sidebar/footer.tsx | 6 +- .../cmd/tui/feature-plugins/sidebar/lsp.tsx | 6 +- .../cmd/tui/feature-plugins/sidebar/mcp.tsx | 6 +- .../cmd/tui/feature-plugins/sidebar/todo.tsx | 6 +- .../tui/feature-plugins/system/plugins.tsx | 18 +++-- .../src/cli/cmd/tui/plugin/runtime.ts | 11 +-- .../src/cli/cmd/tui/ui/dialog-prompt.tsx | 41 ++++++++-- packages/opencode/src/plugin/index.ts | 19 +++-- packages/opencode/src/plugin/shared.ts | 47 ++++++++--- .../cli/tui/plugin-loader-entrypoint.test.ts | 57 ++++++++++++++ .../test/plugin/loader-shared.test.ts | 77 +++++++++++++++++++ packages/plugin/src/index.ts | 3 +- packages/plugin/src/tui.ts | 10 ++- 18 files changed, 292 insertions(+), 62 deletions(-) diff --git a/.opencode/plugins/tui-smoke.tsx b/.opencode/plugins/tui-smoke.tsx index 3e90bafb65..deb3c3e3e4 100644 --- a/.opencode/plugins/tui-smoke.tsx +++ b/.opencode/plugins/tui-smoke.tsx @@ -1,7 +1,14 @@ /** @jsxImportSource @opentui/solid */ import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { RGBA, VignetteEffect } from "@opentui/core" -import type { TuiKeybindSet, TuiPluginApi, TuiPluginMeta, TuiSlotPlugin } from "@opencode-ai/plugin/tui" +import type { + TuiKeybindSet, + TuiPlugin, + TuiPluginApi, + TuiPluginMeta, + TuiPluginModule, + TuiSlotPlugin, +} from "@opencode-ai/plugin/tui" const tabs = ["overview", "counter", "help"] const bind = { @@ -813,7 +820,7 @@ const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => { ]) } -const tui = async (api: TuiPluginApi, options: Record | null, meta: TuiPluginMeta) => { +const tui: TuiPlugin = async (api, options, meta) => { if (options?.enabled === false) return await api.theme.install("./smoke-theme.json") @@ -846,7 +853,9 @@ const tui = async (api: TuiPluginApi, options: Record | null, m } } -export default { +const plugin: TuiPluginModule & { id: string } = { id: "tui-smoke", tui, } + +export default plugin diff --git a/packages/opencode/specs/tui-plugins.md b/packages/opencode/specs/tui-plugins.md index 1a7ba55a02..02b2a9741d 100644 --- a/packages/opencode/specs/tui-plugins.md +++ b/packages/opencode/specs/tui-plugins.md @@ -8,6 +8,8 @@ Technical reference for the current TUI plugin system. - Author package entrypoint is `@opencode-ai/plugin/tui`. - Internal plugins load inside the CLI app the same way external TUI plugins do. - Package plugins can be installed from CLI or TUI. +- v1 plugin modules are target-exclusive: a module can export `server` or `tui`, never both. +- Server runtime keeps v0 legacy fallback (function exports / enumerated exports) after v1 parsing. ## TUI config @@ -27,6 +29,7 @@ Example: - `plugin` entries can be either a string spec or `[spec, options]`. - Plugin specs can be npm specs, `file://` URLs, relative paths, or absolute paths. - Relative path specs are resolved relative to the config file that declared them. +- A file module listed in `tui.json` must be a TUI module (`default export { id?, tui }`) and must not export `server`. - Duplicate npm plugins are deduped by package name; higher-precedence config wins. - Duplicate file plugins are deduped by exact resolved file spec. This happens while merging config, before plugin modules are loaded. - `plugin_enabled` is keyed by plugin id, not by plugin spec. @@ -46,7 +49,7 @@ Minimal module shape: ```tsx /** @jsxImportSource @opentui/solid */ -import type { TuiPlugin } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" const tui: TuiPlugin = async (api, options, meta) => { api.command.register(() => [ @@ -69,16 +72,20 @@ const tui: TuiPlugin = async (api, options, meta) => { ]) } -export default { +const plugin: TuiPluginModule & { id: string } = { id: "acme.demo", tui, } + +export default plugin ``` - Loader only reads the module default export object. Named exports are ignored. -- TUI shape is `default export { id?, tui }`. +- TUI shape is `default export { id?, tui }`; including `server` is rejected. +- A single module cannot export both `server` and `tui`. - `tui` signature is `(api, options, meta) => Promise`. - If package `exports` contains `./tui`, the loader resolves that entrypoint. Otherwise it uses the resolved package target. +- If a package supports both server and TUI, use separate files and package `exports` (`./server` and `./tui`) so each target resolves to a target-only module. - File/path plugins must export a non-empty `id`. - npm plugins may omit `id`; package `name` is used. - Runtime identity is the resolved plugin id. Later plugins with the same id are rejected, including collisions with internal plugin ids. @@ -137,6 +144,7 @@ npm plugins can declare a version compatibility range in `package.json` using th - With `--force`, replacement matches by package name. If the existing row is `[spec, options]`, those tuple options are kept. - Tuple targets in `oc-plugin` provide default options written into config. - A package can target `server`, `tui`, or both. +- If a package targets both, each target must still resolve to a separate target-only module. Do not export `{ server, tui }` from one module. - There is no uninstall, list, or update CLI command for external plugins. - Local file plugins are configured directly in `tui.json`. diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx index 1a1d3c174c..c0e02f74af 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { Tips } from "./tips-view" @@ -42,7 +42,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx index c8538ae2a7..9ffe779791 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx @@ -1,5 +1,5 @@ import type { AssistantMessage } from "@opencode-ai/sdk/v2" -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo } from "solid-js" const id = "internal:sidebar-context" @@ -55,7 +55,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx index 16bed72878..c865c5eb49 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, For, Show, createSignal } from "solid-js" const id = "internal:sidebar-files" @@ -54,7 +54,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx index a6bff01a57..b468d851b0 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" import { Global } from "@/global" @@ -85,7 +85,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx index db9b3a7e56..cb4050fdb8 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, For, Show, createSignal } from "solid-js" const id = "internal:sidebar-lsp" @@ -58,7 +58,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx index 178050abd5..391bf27b90 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, For, Match, Show, Switch, createSignal } from "solid-js" const id = "internal:sidebar-mcp" @@ -88,7 +88,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx index c9e904debd..eed0cb703d 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx @@ -1,4 +1,4 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, For, Show, createSignal } from "solid-js" import { TodoItem } from "../../component/todo-item" @@ -40,7 +40,9 @@ const tui: TuiPlugin = async (api) => { }) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx index 8293be5068..f2fd25ffb6 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/plugins.tsx @@ -1,9 +1,9 @@ import { Keybind } from "@/util/keybind" -import type { TuiPlugin, TuiPluginApi, TuiPluginStatus } from "@opencode-ai/plugin/tui" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import { fileURLToPath } from "url" import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select" -import { createEffect, createMemo, createSignal } from "solid-js" +import { Show, createEffect, createMemo, createSignal } from "solid-js" const id = "internal:plugin-manager" const key = Keybind.parse("space").at(0) @@ -53,11 +53,17 @@ function Install(props: { api: TuiPluginApi }) { ( scope: - {global() ? "global" : "local"} - ({Keybind.toString(tab)} toggle) + + {global() ? "global" : "local"} + + + ({Keybind.toString(tab)} toggle) + )} onConfirm={(raw) => { @@ -256,7 +262,9 @@ const tui: TuiPlugin = async (api) => { ]) } -export default { +const plugin: TuiPluginModule & { id: string } = { id, tui, } + +export default plugin diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index 9cc5194df0..0e1674bdac 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -20,10 +20,10 @@ import { isRecord } from "@/util/record" import { Instance } from "@/project/instance" import { checkPluginCompatibility, - getDefaultPlugin, isDeprecatedPlugin, pluginSource, readPluginId, + readV1Plugin, resolvePluginEntrypoint, resolvePluginId, resolvePluginTarget, @@ -231,9 +231,7 @@ async function loadExternalPlugin( const mod = await import(entry) .then((raw) => { - const mod = getDefaultPlugin(raw) as TuiPluginModule | undefined - if (!mod?.tui) throw new TypeError(`Plugin ${spec} must default export an object with tui()`) - return mod + return readV1Plugin(raw as Record, spec, "tui") as TuiPluginModule }) .catch((error) => { fail("failed to load tui plugin", { path: spec, target: entry, retry, error }) @@ -566,16 +564,13 @@ function pluginApi(runtime: RuntimeState, load: PluginLoad, scope: PluginScope, } function collectPluginEntries(load: PluginLoad, meta: TuiPluginMeta) { - // TUI stays default-only so plugin ids, lifecycle, and errors remain stable. - const plugin = load.module.tui - if (!plugin) return [] const options = load.item ? Config.pluginOptions(load.item) : undefined return [ { id: load.id, load, meta, - plugin, + plugin: load.module.tui, options, enabled: true, }, diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index b1b05a0f1a..cb1b8257ab 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -1,14 +1,17 @@ import { TextareaRenderable, TextAttributes } from "@opentui/core" import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" -import { onMount, type JSX } from "solid-js" +import { Show, createEffect, onMount, type JSX } from "solid-js" import { useKeyboard } from "@opentui/solid" +import { Spinner } from "../component/spinner" export type DialogPromptProps = { title: string description?: () => JSX.Element placeholder?: string value?: string + busy?: boolean + busyText?: string onConfirm?: (value: string) => void onCancel?: () => void } @@ -19,6 +22,12 @@ export function DialogPrompt(props: DialogPromptProps) { let textarea: TextareaRenderable useKeyboard((evt) => { + if (props.busy) { + if (evt.name === "escape") return + evt.preventDefault() + evt.stopPropagation() + return + } if (evt.name === "return") { props.onConfirm?.(textarea.plainText) } @@ -28,11 +37,21 @@ export function DialogPrompt(props: DialogPromptProps) { dialog.setSize("medium") setTimeout(() => { if (!textarea || textarea.isDestroyed) return + if (props.busy) return textarea.focus() }, 1) textarea.gotoLineEnd() }) + createEffect(() => { + if (!textarea || textarea.isDestroyed) return + if (props.busy) { + textarea.blur() + return + } + textarea.focus() + }) + return ( @@ -47,22 +66,28 @@ export function DialogPrompt(props: DialogPromptProps) { {props.description}