Compare commits

..

2 Commits

Author SHA1 Message Date
Kit Langton eeca56a943 Merge branch 'dev' into fix-effect-context-bridges 2026-04-14 19:12:35 -04:00
Kit Langton af5d7b67b0 fix(effect): preserve context across callback bridges 2026-04-14 17:44:01 -04:00
43 changed files with 109 additions and 7924 deletions
+17 -17
View File
@@ -27,7 +27,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -81,7 +81,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -115,7 +115,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -142,7 +142,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48",
@@ -166,7 +166,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -190,7 +190,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -223,7 +223,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"effect": "catalog:",
"electron-context-menu": "4.1.2",
@@ -266,7 +266,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -295,7 +295,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -311,7 +311,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.4.4",
"version": "1.4.3",
"bin": {
"opencode": "./bin/opencode",
},
@@ -450,7 +450,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"effect": "catalog:",
@@ -485,7 +485,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -500,14 +500,14 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.4.4",
"version": "1.4.3",
"devDependencies": {
"typescript": "catalog:",
},
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -542,7 +542,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -591,7 +591,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"zod": "catalog:",
},
@@ -602,7 +602,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-2p0WOk7qE2zC8S5mIDmpefjhJv8zhsgT33crGFWl6LI=",
"aarch64-linux": "sha256-sMW7pXoFtV6r4ySoYB8ISqKFHFeAMmiCUvHtiplwxak=",
"aarch64-darwin": "sha256-/4g2e39t9huLXOObdolDPmImGNhndOsxeAGJjw+bE8g=",
"x86_64-darwin": "sha256-SJ9y58ZwQnXhMtus0ITQo3sfHzHfOSPkJRK24n5pnBw="
"x86_64-linux": "sha256-1qeXIfqPCo5Px0ebdTvL09kav5Ib79E35Ed1FgiLnPY=",
"aarch64-linux": "sha256-987XZxDqDf6gm0kjXg606BSEYvryrUD8vZopsADQIN8=",
"aarch64-darwin": "sha256-RI3D3bPxotudTWmdQNIPZ/oBrmDl5PAdJGc93M4bKHs=",
"x86_64-darwin": "sha256-LE6hrDPXWKjcePc1nv+O6tIN0ZXUrzWI1XBN6Fm/NKw="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.4.4",
"version": "1.4.3",
"description": "",
"type": "module",
"exports": {
@@ -122,21 +122,20 @@ export async function bootstrapGlobal(input: {
}),
),
]
await runAll(fast)
// showErrors({
// errors: errors(await runAll(fast)),
// title: input.requestFailedTitle,
// translate: input.translate,
// formatMoreCount: input.formatMoreCount,
// })
showErrors({
errors: errors(await runAll(fast)),
title: input.requestFailedTitle,
translate: input.translate,
formatMoreCount: input.formatMoreCount,
})
await waitForPaint()
await runAll(slow)
// showErrors({
// errors: errors(),
// title: input.requestFailedTitle,
// translate: input.translate,
// formatMoreCount: input.formatMoreCount,
// })
showErrors({
errors: errors(await runAll(slow)),
title: input.requestFailedTitle,
translate: input.translate,
formatMoreCount: input.formatMoreCount,
})
input.setGlobalStore("ready", true)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"scripts": {
@@ -11,6 +11,5 @@ class LimitError extends Error {
this.retryAfter = retryAfter
}
}
export class RateLimitError extends LimitError {}
export class FreeUsageLimitError extends LimitError {}
export class SubscriptionUsageLimitError extends LimitError {}
@@ -21,7 +21,6 @@ import {
MonthlyLimitError,
UserLimitError,
ModelError,
RateLimitError,
FreeUsageLimitError,
SubscriptionUsageLimitError,
} from "./error"
@@ -36,8 +35,7 @@ import { anthropicHelper } from "./provider/anthropic"
import { googleHelper } from "./provider/google"
import { openaiHelper } from "./provider/openai"
import { oaCompatHelper } from "./provider/openai-compatible"
import { createRateLimiter as createIpRateLimiter } from "./ipRateLimiter"
import { createRateLimiter as createKeyRateLimiter } from "./keyRateLimiter"
import { createRateLimiter } from "./rateLimiter"
import { createDataDumper } from "./dataDumper"
import { createTrialLimiter } from "./trialLimiter"
import { createStickyTracker } from "./stickyProviderTracker"
@@ -94,8 +92,6 @@ export async function handler(
const isStream = opts.parseIsStream(url, body)
const rawIp = input.request.headers.get("x-real-ip") ?? ""
const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp
const rawZenApiKey = opts.parseApiKey(input.request.headers)
const zenApiKey = rawZenApiKey === "public" ? undefined : rawZenApiKey
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
const requestId = input.request.headers.get("x-opencode-request") ?? ""
const projectId = input.request.headers.get("x-opencode-project") ?? ""
@@ -112,13 +108,17 @@ export async function handler(
const dataDumper = createDataDumper(sessionId, requestId, projectId)
const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip)
const trialProviders = await trialLimiter?.check()
const rateLimiter = modelInfo.allowAnonymous
? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request)
: createKeyRateLimiter(modelInfo.id, zenApiKey, input.request)
const rateLimiter = createRateLimiter(
modelInfo.id,
modelInfo.allowAnonymous,
modelInfo.rateLimit,
ip,
input.request,
)
await rateLimiter?.check()
const stickyTracker = createStickyTracker(modelInfo.stickyProvider, sessionId)
const stickyProvider = await stickyTracker?.get()
const authInfo = await authenticate(modelInfo, zenApiKey)
const authInfo = await authenticate(modelInfo)
const billingSource = validateBilling(authInfo, modelInfo)
logger.metric({ source: billingSource })
@@ -363,11 +363,7 @@ export async function handler(
{ status: 401 },
)
if (
error instanceof RateLimitError ||
error instanceof FreeUsageLimitError ||
error instanceof SubscriptionUsageLimitError
) {
if (error instanceof FreeUsageLimitError || error instanceof SubscriptionUsageLimitError) {
const headers = new Headers()
if (error.retryAfter) {
headers.set("retry-after", String(error.retryAfter))
@@ -496,8 +492,9 @@ export async function handler(
}
}
async function authenticate(modelInfo: ModelInfo, zenApiKey?: string) {
if (!zenApiKey) {
async function authenticate(modelInfo: ModelInfo) {
const apiKey = opts.parseApiKey(input.request.headers)
if (!apiKey || apiKey === "public") {
if (modelInfo.allowAnonymous) return
throw new AuthError(t("zen.api.error.missingApiKey"))
}
@@ -576,7 +573,7 @@ export async function handler(
isNull(LiteTable.timeDeleted),
),
)
.where(and(eq(KeyTable.key, zenApiKey), isNull(KeyTable.timeDeleted)))
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]),
)
@@ -1,39 +0,0 @@
import { Database, eq, and, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyRateLimitTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { RateLimitError } from "./error"
import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
export function createRateLimiter(modelId: string, zenApiKey: string | undefined, request: Request) {
if (!zenApiKey) return
const dict = i18n(localeFromRequest(request))
const LIMIT = 100
const yyyyMMddHHmm = new Date(Date.now())
.toISOString()
.replace(/[^0-9]/g, "")
.substring(0, 12)
const interval = `${modelId.substring(0, 27)}-${yyyyMMddHHmm}`
return {
check: async () => {
const rows = await Database.use((tx) =>
tx
.select({ interval: KeyRateLimitTable.interval, count: KeyRateLimitTable.count })
.from(KeyRateLimitTable)
.where(and(eq(KeyRateLimitTable.key, zenApiKey), eq(KeyRateLimitTable.interval, interval))),
).then((rows) => rows[0])
const count = rows?.count ?? 0
if (count >= LIMIT) throw new RateLimitError(dict["zen.api.error.rateLimitExceeded"], 60)
},
track: async () => {
await Database.use((tx) =>
tx
.insert(KeyRateLimitTable)
.values({ key: zenApiKey, interval, count: 1 })
.onDuplicateKeyUpdate({ set: { count: sql`${KeyRateLimitTable.count} + 1` } }),
)
},
}
}
@@ -6,7 +6,14 @@ import { i18n } from "~/i18n"
import { localeFromRequest } from "~/lib/language"
import { Subscription } from "@opencode-ai/console-core/subscription.js"
export function createRateLimiter(modelId: string, rateLimit: number | undefined, rawIp: string, request: Request) {
export function createRateLimiter(
modelId: string,
allowAnonymous: boolean | undefined,
rateLimit: number | undefined,
rawIp: string,
request: Request,
) {
if (!allowAnonymous) return
const dict = i18n(localeFromRequest(request))
const limits = Subscription.getFreeLimits()
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { getRetryAfterDay } from "../src/routes/zen/util/ipRateLimiter"
import { getRetryAfterDay } from "../src/routes/zen/util/rateLimiter"
describe("getRetryAfterDay", () => {
test("returns full day at midnight UTC", () => {
@@ -1,6 +0,0 @@
CREATE TABLE `key_rate_limit` (
`key` varchar(255) NOT NULL,
`interval` varchar(12) NOT NULL,
`count` int NOT NULL,
CONSTRAINT PRIMARY KEY(`key`,`interval`)
);
File diff suppressed because it is too large Load Diff
@@ -1 +0,0 @@
ALTER TABLE `key_rate_limit` MODIFY COLUMN `interval` varchar(20) NOT NULL;
@@ -1 +0,0 @@
ALTER TABLE `key_rate_limit` MODIFY COLUMN `interval` varchar(40) NOT NULL;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.4.4",
"version": "1.4.3",
"private": true,
"type": "module",
"license": "MIT",
@@ -20,13 +20,3 @@ export const IpRateLimitTable = mysqlTable(
},
(table) => [primaryKey({ columns: [table.ip, table.interval] })],
)
export const KeyRateLimitTable = mysqlTable(
"key_rate_limit",
{
key: varchar("key", { length: 255 }).notNull(),
interval: varchar("interval", { length: 40 }).notNull(),
count: int("count").notNull(),
},
(table) => [primaryKey({ columns: [table.key, table.interval] })],
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.4.4",
"version": "1.4.3",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.4.4",
"version": "1.4.3",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.4.4",
"version": "1.4.3",
"private": true,
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.4.4"
version = "1.4.3"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.4/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.3/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.4.4",
"version": "1.4.3",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.4.4",
"version": "1.4.3",
"name": "opencode",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,5 @@
import z from "zod"
import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { Log } from "../util/log"
import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
@@ -128,6 +127,7 @@ export namespace Bus {
function on<T>(pubsub: PubSub.PubSub<T>, type: string, callback: (event: T) => unknown) {
return Effect.gen(function* () {
log.info("subscribing", { type })
const ctx = yield* Effect.context()
const scope = yield* Scope.make()
const subscription = yield* Scope.provide(scope)(PubSub.subscribe(pubsub))
@@ -147,7 +147,7 @@ export namespace Bus {
return () => {
log.info("unsubscribing", { type })
Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer)))
Effect.runForkWith(ctx)(Scope.close(scope, Exit.void))
}
})
}
+2 -3
View File
@@ -3,7 +3,6 @@ import { InstanceState } from "@/effect/instance-state"
import type { InstanceContext } from "@/project/instance"
import { SessionID, MessageID } from "@/session/schema"
import { Effect, Layer, Context } from "effect"
import { EffectLogger } from "@/effect/logger"
import z from "zod"
import { Config } from "../config/config"
import { MCP } from "../mcp"
@@ -79,6 +78,7 @@ export namespace Command {
const config = yield* Config.Service
const mcp = yield* MCP.Service
const skill = yield* Skill.Service
const fx = yield* Effect.context()
const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
const cfg = yield* config.get()
@@ -125,7 +125,7 @@ export namespace Command {
source: "mcp",
description: prompt.description,
get template() {
return Effect.runPromise(
return Effect.runPromiseWith(fx)(
mcp
.getPrompt(
prompt.client,
@@ -141,7 +141,6 @@ export namespace Command {
.map((message) => (message.content.type === "text" ? message.content.text : ""))
.join("\n") || "",
),
Effect.provide(EffectLogger.layer),
),
)
},
+7 -5
View File
@@ -25,7 +25,6 @@ import { Bus } from "@/bus"
import { TuiEvent } from "@/cli/cmd/tui/event"
import open from "open"
import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
@@ -444,6 +443,11 @@ export namespace MCP {
return { mcpClient, status, defs: listed } satisfies CreateResult
})
const cfgSvc = yield* Config.Service
const ctx = yield* Effect.context()
const run = {
promise: <A, E>(effect: Effect.Effect<A, E>) => Effect.runPromiseWith(ctx)(effect),
}
const descendants = Effect.fnUntraced(
function* (pid: number) {
@@ -476,14 +480,12 @@ export namespace MCP {
log.info("tools list changed notification received", { server: name })
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
const listed = await Effect.runPromise(defs(name, client, timeout).pipe(Effect.provide(EffectLogger.layer)))
const listed = await run.promise(defs(name, client, timeout))
if (!listed) return
if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
s.defs[name] = listed
await Effect.runPromise(
bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore, Effect.provide(EffectLogger.layer)),
)
await run.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
})
}
+11 -13
View File
@@ -18,7 +18,6 @@ import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
import { PoeAuthPlugin } from "opencode-poe-auth"
import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
import { Effect, Layer, Context, Stream } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { errorMessage } from "@/util/error"
import { PluginLoader } from "./loader"
@@ -90,14 +89,6 @@ export namespace Plugin {
return result
}
function publishPluginError(bus: Bus.Interface, message: string) {
Effect.runFork(
bus
.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
.pipe(Effect.provide(EffectLogger.layer)),
)
}
async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) {
const plugin = readV1Plugin(load.mod, load.spec, "server", "detect")
if (plugin) {
@@ -116,6 +107,13 @@ export namespace Plugin {
Effect.gen(function* () {
const bus = yield* Bus.Service
const config = yield* Config.Service
const ctx = yield* Effect.context()
function publishPluginError(message: string) {
Effect.runForkWith(ctx)(
bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }),
)
}
const state = yield* InstanceState.make<State>(
Effect.fn("Plugin.state")(function* (ctx) {
@@ -187,24 +185,24 @@ export namespace Plugin {
if (stage === "install") {
const parsed = parsePluginSpecifier(spec)
log.error("failed to install plugin", { pkg: parsed.pkg, version: parsed.version, error: message })
publishPluginError(bus, `Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
publishPluginError(`Failed to install plugin ${parsed.pkg}@${parsed.version}: ${message}`)
return
}
if (stage === "compatibility") {
log.warn("plugin incompatible", { path: spec, error: message })
publishPluginError(bus, `Plugin ${spec} skipped: ${message}`)
publishPluginError(`Plugin ${spec} skipped: ${message}`)
return
}
if (stage === "entry") {
log.error("failed to resolve plugin server entry", { path: spec, error: message })
publishPluginError(bus, `Failed to load plugin ${spec}: ${message}`)
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
return
}
log.error("failed to load plugin", { path: spec, target: resolved?.entry, error: message })
publishPluginError(bus, `Failed to load plugin ${spec}: ${message}`)
publishPluginError(`Failed to load plugin ${spec}: ${message}`)
},
},
}),
+2 -3
View File
@@ -19,7 +19,6 @@ import { iife } from "@/util/iife"
import { Global } from "../global"
import path from "path"
import { Effect, Layer, Context } from "effect"
import { EffectLogger } from "@/effect/logger"
import { InstanceState } from "@/effect/instance-state"
import { AppFileSystem } from "@/filesystem"
import { isRecord } from "@/util/record"
@@ -1039,6 +1038,7 @@ export namespace Provider {
const auth = yield* Auth.Service
const env = yield* Env.Service
const plugin = yield* Plugin.Service
const ctx = yield* Effect.context()
const state = yield* InstanceState.make<State>(() =>
Effect.gen(function* () {
@@ -1223,8 +1223,7 @@ export namespace Provider {
const options = yield* Effect.promise(() =>
plugin.auth!.loader!(
() =>
Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any,
() => Effect.runPromiseWith(ctx)(auth.get(providerID).pipe(Effect.orDie)) as any,
database[plugin.auth!.provider],
),
)
+3 -3
View File
@@ -10,7 +10,6 @@ import { Shell } from "@/shell/shell"
import { Plugin } from "@/plugin"
import { PtyID } from "./schema"
import { Effect, Layer, Context } from "effect"
import { EffectLogger } from "@/effect/logger"
export namespace Pty {
const log = Log.create({ service: "pty" })
@@ -119,6 +118,7 @@ export namespace Pty {
Effect.gen(function* () {
const bus = yield* Bus.Service
const plugin = yield* Plugin.Service
const ctx = yield* Effect.context()
function teardown(session: Active) {
try {
session.process.kill()
@@ -256,8 +256,8 @@ export namespace Pty {
if (session.info.status === "exited") return
log.info("session exited", { id, exitCode })
session.info.status = "exited"
Effect.runFork(bus.publish(Event.Exited, { id, exitCode }).pipe(Effect.provide(EffectLogger.layer)))
Effect.runFork(remove(id).pipe(Effect.provide(EffectLogger.layer)))
Effect.runForkWith(ctx)(bus.publish(Event.Exited, { id, exitCode }))
Effect.runForkWith(ctx)(remove(id))
}),
)
yield* bus.publish(Event.Created, { info })
@@ -1,175 +0,0 @@
import { AppLayer } from "@/effect/app-runtime"
import { memoMap } from "@/effect/run-service"
import { Provider } from "@/provider/provider"
import { lazy } from "@/util/lazy"
import { Effect, Layer, Schema } from "effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import type { Handler } from "hono"
import { mapValues } from "remeda"
const ApiInfo = Schema.Struct({
id: Schema.String,
url: Schema.String,
npm: Schema.String,
}).annotate({ identifier: "ConfigProvidersModelApi" })
const Mode = Schema.Struct({
text: Schema.Boolean,
audio: Schema.Boolean,
image: Schema.Boolean,
video: Schema.Boolean,
pdf: Schema.Boolean,
}).annotate({ identifier: "ConfigProvidersModelMode" })
const Interleaved = Schema.Union([
Schema.Boolean,
Schema.Struct({
field: Schema.Literals(["reasoning_content", "reasoning_details"]),
}),
]).annotate({ identifier: "ConfigProvidersModelInterleaved" })
const Capabilities = Schema.Struct({
temperature: Schema.Boolean,
reasoning: Schema.Boolean,
attachment: Schema.Boolean,
toolcall: Schema.Boolean,
input: Mode,
output: Mode,
interleaved: Interleaved,
}).annotate({ identifier: "ConfigProvidersModelCapabilities" })
const Cache = Schema.Struct({
read: Schema.Number,
write: Schema.Number,
}).annotate({ identifier: "ConfigProvidersModelCache" })
const ExperimentalOver200K = Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache: Cache,
})
.pipe(Schema.optional)
.annotate({ identifier: "ConfigProvidersModelExperimentalOver200K" })
const Cost = Schema.Struct({
input: Schema.Number,
output: Schema.Number,
cache: Cache,
experimentalOver200K: ExperimentalOver200K,
}).annotate({ identifier: "ConfigProvidersModelCost" })
const Limit = Schema.Struct({
context: Schema.Number,
input: Schema.optional(Schema.Number),
output: Schema.Number,
}).annotate({ identifier: "ConfigProvidersModelLimit" })
const Model = Schema.Struct({
id: Schema.String,
providerID: Schema.String,
api: ApiInfo,
name: Schema.String,
family: Schema.optional(Schema.String),
capabilities: Capabilities,
cost: Cost,
limit: Limit,
status: Schema.Union([
Schema.Literal("alpha"),
Schema.Literal("beta"),
Schema.Literal("deprecated"),
Schema.Literal("active"),
]),
options: Schema.Record(Schema.String, Schema.Unknown),
headers: Schema.Record(Schema.String, Schema.String),
release_date: Schema.String,
variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
}).annotate({ identifier: "ConfigProvidersModel" })
const ProviderInfo = Schema.Struct({
id: Schema.String,
name: Schema.String,
source: Schema.Union([
Schema.Literal("env"),
Schema.Literal("config"),
Schema.Literal("custom"),
Schema.Literal("api"),
]),
env: Schema.Array(Schema.String),
key: Schema.optional(Schema.String),
options: Schema.Record(Schema.String, Schema.Unknown),
models: Schema.Record(Schema.String, Schema.Unknown),
}).annotate({ identifier: "ConfigProvidersProvider" })
const Providers = Schema.Unknown
const root = "/experimental/httpapi/config"
const Api = HttpApi.make("config")
.add(
HttpApiGroup.make("config")
.add(
HttpApiEndpoint.get("providers", `${root}/providers`, {
success: Providers,
}).annotateMerge(
OpenApi.annotations({
identifier: "config.providers",
summary: "List config providers",
description: "Get a list of all configured AI providers and their default models.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "config",
description: "Experimental HttpApi config routes.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "opencode experimental HttpApi",
version: "0.0.1",
description: "Experimental HttpApi surface for selected instance routes.",
}),
)
const ConfigLive = HttpApiBuilder.group(
Api,
"config",
Effect.fn("ConfigHttpApi.handlers")(function* (handlers) {
const svc = yield* Provider.Service
const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
const all = mapValues(yield* svc.list(), (item) => item)
return Schema.decodeUnknownSync(Providers)(
JSON.parse(
JSON.stringify({
providers: Object.values(all),
default: mapValues(all, (item) => Provider.sort(Object.values(item.models))[0].id),
}),
),
)
})
return handlers.handle("providers", providers)
}),
).pipe(Layer.provide(Provider.defaultLayer))
const web = lazy(() =>
HttpRouter.toWebHandler(
Layer.mergeAll(
AppLayer,
HttpApiBuilder.layer(Api, { openapiPath: `${root}/doc` }).pipe(
Layer.provide(ConfigLive),
Layer.provide(HttpServer.layerServices),
),
),
{
disableLogger: true,
memoMap,
},
),
)
export const ConfigHttpApiHandler: Handler = (c, _next) => web().handler(c.req.raw)
@@ -1,12 +1,7 @@
import { lazy } from "@/util/lazy"
import { Hono } from "hono"
import { ConfigHttpApiHandler } from "./config"
import { QuestionHttpApiHandler } from "./question"
export const HttpApiRoutes = lazy(() =>
new Hono()
.all("/question", QuestionHttpApiHandler)
.all("/question/*", QuestionHttpApiHandler)
.all("/config", ConfigHttpApiHandler)
.all("/config/*", ConfigHttpApiHandler),
new Hono().all("/question", QuestionHttpApiHandler).all("/question/*", QuestionHttpApiHandler),
)
@@ -1,33 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import { Log } from "../../src/util/log"
import { tmpdir } from "../fixture/fixture"
Log.init({ print: false })
afterEach(async () => {
await Instance.disposeAll()
})
describe("experimental config providers httpapi", () => {
test("lists config providers and serves docs", async () => {
await using tmp = await tmpdir({ git: true })
const app = Server.Default().app
const headers = {
"content-type": "application/json",
"x-opencode-directory": tmp.path,
}
const res = await app.request("/experimental/httpapi/config/providers", { headers })
expect(res.status).toBe(200)
const body = await res.json()
expect(Array.isArray(body.providers)).toBe(true)
expect(typeof body.default).toBe("object")
const doc = await app.request("/experimental/httpapi/config/doc", { headers })
expect(doc.status).toBe(200)
const spec = await doc.json()
expect(spec.paths["/experimental/httpapi/config/providers"]?.get?.operationId).toBe("config.providers")
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.4.4",
"version": "1.4.3",
"type": "module",
"license": "MIT",
"exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.4.4",
"version": "1.4.3",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.4.4",
"version": "1.4.3",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.4.4",
"version": "1.4.3",
"publisher": "sst-dev",
"repository": {
"type": "git",