Skip to content

Commit 9616f48

Browse files
committed
fix(httpapi): align default backend parity
1 parent 0712ef6 commit 9616f48

8 files changed

Lines changed: 88 additions & 79 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Flag } from "@opencode-ai/core/flag/flag"
22
import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version"
33

4-
export type Runtime = "effect-httpapi" | "hono"
4+
export type Backend = "effect-httpapi" | "hono"
55

66
export type Selection = {
7-
runtime: Runtime
7+
backend: Backend
88
reason: "env" | "channel" | "stable" | "explicit"
99
}
1010

@@ -18,23 +18,23 @@ const channelDefaultsToHttpApi = () =>
1818
InstallationVersion.includes("-beta")
1919

2020
export function select(): Selection {
21-
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { runtime: "effect-httpapi", reason: "env" }
22-
if (channelDefaultsToHttpApi()) return { runtime: "effect-httpapi", reason: "channel" }
23-
return { runtime: "hono", reason: "stable" }
21+
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" }
22+
if (channelDefaultsToHttpApi()) return { backend: "effect-httpapi", reason: "channel" }
23+
return { backend: "hono", reason: "stable" }
2424
}
2525

2626
export function attributes(selection: Selection): Record<string, string> {
2727
return {
28-
"opencode.server.runtime": selection.runtime,
29-
"opencode.server.runtime.reason": selection.reason,
28+
"opencode.server.backend": selection.backend,
29+
"opencode.server.backend.reason": selection.reason,
3030
"opencode.installation.channel": InstallationChannel,
3131
"opencode.installation.version": InstallationVersion,
3232
}
3333
}
3434

35-
export function force(selection: Selection, runtime: Runtime): Selection {
35+
export function force(selection: Selection, backend: Backend): Selection {
3636
return {
37-
runtime,
38-
reason: selection.runtime === runtime ? selection.reason : "explicit",
37+
backend,
38+
reason: selection.backend === backend ? selection.reason : "explicit",
3939
}
4040
}

packages/opencode/src/server/middleware.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
1010
import { basicAuth } from "hono/basic-auth"
1111
import { cors } from "hono/cors"
1212
import { compress } from "hono/compress"
13-
import * as ServerRuntime from "./runtime"
13+
import * as ServerBackend from "./backend"
1414

1515
const log = Log.create({ service: "server" })
1616

@@ -50,14 +50,14 @@ export const AuthMiddleware: MiddlewareHandler = (c, next) => {
5050
return basicAuth({ username, password })(c, next)
5151
}
5252

53-
export function LoggerMiddleware(runtimeAttributes: ServerRuntime.Attributes): MiddlewareHandler {
53+
export function LoggerMiddleware(backendAttributes: ServerBackend.Attributes): MiddlewareHandler {
5454
return async (c, next) => {
5555
const skip = c.req.path === "/log"
5656
if (skip) return next()
5757
const attributes = {
5858
method: c.req.method,
5959
path: c.req.path,
60-
...runtimeAttributes,
60+
...backendAttributes,
6161
}
6262
log.info("request", attributes)
6363
const timer = log.time("request", attributes)

packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Effect } from "effect"
33
import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
44

55
const disposeAfterResponse = new WeakMap<object, InstanceContext>()
6-
const reloadAfterResponse = new WeakMap<object, InstanceContext & { next: Parameters<typeof Instance.reload>[0] }>()
76

87
export const markInstanceForDisposal = (ctx: InstanceContext) =>
98
HttpEffect.appendPreResponseHandler((request, response) =>
@@ -14,27 +13,17 @@ export const markInstanceForDisposal = (ctx: InstanceContext) =>
1413
)
1514

1615
export const markInstanceForReload = (ctx: InstanceContext, next: Parameters<typeof Instance.reload>[0]) =>
17-
HttpEffect.appendPreResponseHandler((request, response) =>
18-
Effect.sync(() => {
19-
reloadAfterResponse.set(request.source, { ...ctx, next })
20-
return response
21-
}),
16+
HttpEffect.appendPreResponseHandler((_request, response) =>
17+
Effect.as(Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.reload(next)))), response),
2218
)
2319

2420
export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>
2521
Effect.gen(function* () {
2622
const response = yield* effect
2723
const request = yield* HttpServerRequest.HttpServerRequest
28-
const reload = reloadAfterResponse.get(request.source)
29-
if (reload) {
30-
reloadAfterResponse.delete(request.source)
31-
yield* Effect.promise(() => Instance.restore(reload, () => Instance.reload(reload.next)))
32-
return response
33-
}
34-
3524
const ctx = disposeAfterResponse.get(request.source)
3625
if (!ctx) return response
3726
disposeAfterResponse.delete(request.source)
38-
yield* Effect.promise(() => Instance.restore(ctx, () => Instance.dispose()))
27+
yield* Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.dispose())))
3928
return response
4029
})

packages/opencode/src/server/routes/instance/httpapi/server.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import { TuiApi, tuiHandlers } from "./tui"
5555
import { WorkspaceApi, workspaceHandlers } from "./workspace"
5656
import { disposeMiddleware } from "./lifecycle"
5757
import { memoMap } from "@opencode-ai/core/effect/memo-map"
58-
import * as ServerRuntime from "@/server/runtime"
58+
import * as ServerBackend from "@/server/backend"
5959

6060
const Query = Schema.Struct({
6161
directory: Schema.optional(Schema.String),
@@ -78,13 +78,21 @@ function decode(input: string) {
7878
}
7979
}
8080

81+
function currentDirectory() {
82+
try {
83+
return Instance.directory
84+
} catch {
85+
return process.cwd()
86+
}
87+
}
88+
8189
const instance = HttpRouter.middleware()(
8290
Effect.gen(function* () {
8391
return (effect) =>
8492
Effect.gen(function* () {
8593
const query = yield* HttpServerRequest.schemaSearchParams(Query)
8694
const headers = yield* HttpServerRequest.schemaHeaders(Headers)
87-
const raw = query.directory || headers["x-opencode-directory"] || process.cwd()
95+
const raw = query.directory || headers["x-opencode-directory"] || currentDirectory()
8896
const workspace = query.workspace || undefined
8997
const ctx = yield* Effect.promise(() =>
9098
Instance.provide({
@@ -103,9 +111,9 @@ const instance = HttpRouter.middleware()(
103111
const runtime = HttpRouter.middleware()(
104112
Effect.succeed((effect) =>
105113
Effect.gen(function* () {
106-
const selected = ServerRuntime.select()
114+
const selected = ServerBackend.select()
107115
yield* Effect.annotateCurrentSpan(
108-
ServerRuntime.attributes(ServerRuntime.force(selected, "effect-httpapi")),
116+
ServerBackend.attributes(ServerBackend.force(selected, "effect-httpapi")),
109117
)
110118
return yield* effect
111119
}),

packages/opencode/src/server/routes/instance/httpapi/session.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { SessionSummary } from "@/session/summary"
1919
import { Todo } from "@/session/todo"
2020
import { MessageID, PartID, SessionID } from "@/session/schema"
2121
import { Snapshot } from "@/snapshot"
22+
import { NotFoundError } from "@/storage/storage"
2223
import * as Log from "@opencode-ai/core/util/log"
2324
import { NamedError } from "@opencode-ai/core/util/error"
2425
import { Effect, Schema, SchemaGetter, Struct } from "effect"
@@ -191,7 +192,7 @@ export const SessionApi = HttpApi.make("session")
191192
params: { sessionID: SessionID },
192193
query: MessagesQuery,
193194
success: Schema.Array(MessageV2.WithParts),
194-
error: HttpApiError.BadRequest,
195+
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
195196
}).annotateMerge(
196197
OpenApi.annotations({
197198
identifier: "session.messages",
@@ -480,37 +481,47 @@ export const sessionHandlers = HttpApiBuilder.group(SessionApi, "session", (hand
480481
params: { sessionID: SessionID }
481482
query: typeof MessagesQuery.Type
482483
}) {
483-
if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
484-
if (ctx.query.before) {
485-
const before = ctx.query.before
486-
yield* Effect.try({
487-
try: () => MessageV2.cursor.decode(before),
488-
catch: () => new HttpApiError.BadRequest({}),
489-
})
490-
}
491-
if (ctx.query.limit === undefined || ctx.query.limit === 0) {
492-
yield* session.get(ctx.params.sessionID)
493-
return yield* session.messages({ sessionID: ctx.params.sessionID })
494-
}
484+
return yield* Effect.gen(function* () {
485+
if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
486+
if (ctx.query.before) {
487+
const before = ctx.query.before
488+
yield* Effect.try({
489+
try: () => MessageV2.cursor.decode(before),
490+
catch: () => new HttpApiError.BadRequest({}),
491+
})
492+
}
493+
if (ctx.query.limit === undefined || ctx.query.limit === 0) {
494+
yield* session.get(ctx.params.sessionID)
495+
return yield* session.messages({ sessionID: ctx.params.sessionID })
496+
}
495497

496-
const page = MessageV2.page({
497-
sessionID: ctx.params.sessionID,
498-
limit: ctx.query.limit,
499-
before: ctx.query.before,
500-
})
501-
if (!page.cursor) return page.items
502-
503-
const request = yield* HttpServerRequest.HttpServerRequest
504-
const url = new URL(request.url, "http://localhost")
505-
url.searchParams.set("limit", ctx.query.limit.toString())
506-
url.searchParams.set("before", page.cursor)
507-
return HttpServerResponse.jsonUnsafe(page.items, {
508-
headers: {
509-
"Access-Control-Expose-Headers": "Link, X-Next-Cursor",
510-
Link: `<${url.toString()}>; rel="next"`,
511-
"X-Next-Cursor": page.cursor,
512-
},
513-
})
498+
yield* session.get(ctx.params.sessionID)
499+
const page = MessageV2.page({
500+
sessionID: ctx.params.sessionID,
501+
limit: ctx.query.limit,
502+
before: ctx.query.before,
503+
})
504+
if (!page.cursor) return page.items
505+
506+
const request = yield* HttpServerRequest.HttpServerRequest
507+
const url = new URL(request.url, "http://localhost")
508+
url.searchParams.set("limit", ctx.query.limit.toString())
509+
url.searchParams.set("before", page.cursor)
510+
return HttpServerResponse.jsonUnsafe(page.items, {
511+
headers: {
512+
"Access-Control-Expose-Headers": "Link, X-Next-Cursor",
513+
Link: `<${url.toString()}>; rel="next"`,
514+
"X-Next-Cursor": page.cursor,
515+
},
516+
})
517+
}).pipe(
518+
Effect.catchIf(NotFoundError.isInstance, () => Effect.fail(new HttpApiError.NotFound({}))),
519+
Effect.catchDefect((error) =>
520+
NotFoundError.isInstance(error)
521+
? Effect.fail(new HttpApiError.NotFound({}))
522+
: Effect.die(error),
523+
),
524+
)
514525
})
515526

516527
const message = Effect.fn("SessionHttpApi.message")(function* (ctx: {

packages/opencode/src/server/routes/instance/httpapi/tui.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export const tuiHandlers = HttpApiBuilder.group(TuiApi, "tui", (handlers) =>
254254
const selectSession = Effect.fn("TuiHttpApi.selectSession")(function* (ctx: {
255255
payload: typeof TuiEvent.SessionSelect.properties.Type
256256
}) {
257+
if (!ctx.payload.sessionID.startsWith("ses")) return yield* new HttpApiError.BadRequest({})
257258
const row = yield* Effect.sync(() =>
258259
Database.use((db) =>
259260
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, ctx.payload.sessionID)).get(),

packages/opencode/src/server/server.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { WorkspaceRouterMiddleware } from "./workspace"
1717
import { InstanceMiddleware } from "./routes/instance/middleware"
1818
import { WorkspaceRoutes } from "./routes/control/workspace"
1919
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
20-
import * as ServerRuntime from "./runtime"
20+
import * as ServerBackend from "./backend"
2121

2222
// @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
2323
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -38,37 +38,37 @@ type ServerApp = {
3838
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
3939
}
4040

41-
const DefaultHono = lazy(() => withRuntime({ runtime: "hono", reason: "stable" }, createHono({}, { runtime: "hono", reason: "stable" })))
41+
const DefaultHono = lazy(() => withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" })))
4242
const DefaultHttpApi = lazy(() => createDefaultHttpApi())
4343

4444
function select() {
45-
return ServerRuntime.select()
45+
return ServerBackend.select()
4646
}
4747

48-
export const runtime = select
48+
export const backend = select
4949

5050
export const Default = () => {
5151
const selected = select()
52-
return selected.runtime === "effect-httpapi" ? DefaultHttpApi() : DefaultHono()
52+
return selected.backend === "effect-httpapi" ? DefaultHttpApi() : DefaultHono()
5353
}
5454

5555
function create(opts: { cors?: string[] }) {
5656
const selected = select()
57-
return selected.runtime === "effect-httpapi"
58-
? withRuntime(selected, createHttpApi())
59-
: withRuntime(selected, createHono(opts, selected))
57+
return selected.backend === "effect-httpapi"
58+
? withBackend(selected, createHttpApi())
59+
: withBackend(selected, createHono(opts, selected))
6060
}
6161

6262
export function Legacy(opts: { cors?: string[] } = {}) {
63-
return withRuntime({ runtime: "hono", reason: "explicit" }, createHono(opts, { runtime: "hono", reason: "explicit" }))
63+
return withBackend({ backend: "hono", reason: "explicit" }, createHono(opts, { backend: "hono", reason: "explicit" }))
6464
}
6565

6666
function createDefaultHttpApi() {
67-
return withRuntime(select(), createHttpApi())
67+
return withBackend(select(), createHttpApi())
6868
}
6969

70-
function withRuntime<T extends { app: ServerApp; runtime: unknown }>(selection: ServerRuntime.Selection, built: T) {
71-
log.info("server runtime selected", ServerRuntime.attributes(selection))
70+
function withBackend<T extends { app: ServerApp; runtime: unknown }>(selection: ServerBackend.Selection, built: T) {
71+
log.info("server backend selected", ServerBackend.attributes(selection))
7272
return built
7373
}
7474

@@ -86,12 +86,12 @@ function createHttpApi() {
8686
}
8787
}
8888

89-
function createHono(opts: { cors?: string[] }, selection: ServerRuntime.Selection = ServerRuntime.force(select(), "hono")) {
90-
const runtimeAttributes = ServerRuntime.attributes(selection)
89+
function createHono(opts: { cors?: string[] }, selection: ServerBackend.Selection = ServerBackend.force(select(), "hono")) {
90+
const backendAttributes = ServerBackend.attributes(selection)
9191
const app = new Hono()
9292
.onError(ErrorMiddleware)
9393
.use(AuthMiddleware)
94-
.use(LoggerMiddleware(runtimeAttributes))
94+
.use(LoggerMiddleware(backendAttributes))
9595
.use(CompressionMiddleware)
9696
.use(CorsMiddleware(opts))
9797
.route("/global", GlobalRoutes())

packages/opencode/test/server/httpapi-bridge.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ afterEach(async () => {
146146
})
147147

148148
describe("HttpApi server", () => {
149-
test("defaults local/dev builds to the Effect HttpApi runtime", () => {
149+
test("defaults local/dev builds to the Effect HttpApi backend", () => {
150150
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
151-
expect(Server.runtime().runtime).toBe("effect-httpapi")
151+
expect(Server.backend().backend).toBe("effect-httpapi")
152152
})
153153

154154
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {

0 commit comments

Comments
 (0)