Skip to content

Commit 0ee3b87

Browse files
authored
feat(server): Server.openapi() backed by HttpApi spec, parity-checked against Hono output (#25545)
1 parent 3c9f3c5 commit 0ee3b87

6 files changed

Lines changed: 48 additions & 15 deletions

File tree

packages/opencode/script/httpapi-exercise.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1506,7 +1506,7 @@ const main = Effect.gen(function* () {
15061506
const options = parseOptions(Bun.argv.slice(2))
15071507
const modules = yield* Effect.promise(() => runtime())
15081508
const effectRoutes = routeKeys(OpenApi.fromApi(modules.PublicApi))
1509-
const honoRoutes = routeKeys(yield* Effect.promise(() => modules.Server.openapi()))
1509+
const honoRoutes = routeKeys(yield* Effect.promise(() => modules.Server.openapiHono()))
15101510
const selected = scenarios.filter((scenario) => matches(options, scenario))
15111511
const missing = effectRoutes.filter((route) => !scenarios.some((scenario) => route === routeKey(scenario)))
15121512
const extra = scenarios.filter((scenario) => !effectRoutes.includes(routeKey(scenario)))

packages/opencode/src/cli/cmd/generate.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import { Server } from "../../server/server"
2-
import { PublicApi } from "../../server/routes/instance/httpapi/public"
32
import type { CommandModule } from "yargs"
4-
import { OpenApi } from "effect/unstable/httpapi"
53

64
type Args = {
75
httpapi: boolean
6+
hono: boolean
87
}
98

109
export const GenerateCommand = {
1110
command: "generate",
1211
builder: (yargs) =>
13-
yargs.option("httpapi", {
14-
type: "boolean",
15-
default: false,
16-
description: "Generate OpenAPI from the experimental Effect HttpApi contract",
17-
}),
12+
yargs
13+
.option("httpapi", {
14+
type: "boolean",
15+
default: false,
16+
description:
17+
"Generate OpenAPI from the Effect HttpApi contract (default; flag retained for backwards compatibility)",
18+
})
19+
.option("hono", {
20+
type: "boolean",
21+
default: false,
22+
description: "Generate OpenAPI from the legacy Hono backend (parity-diff only; will be removed)",
23+
}),
1824
handler: async (args) => {
19-
const specs = args.httpapi ? OpenApi.fromApi(PublicApi) : await Server.openapi()
25+
const specs = args.hono ? await Server.openapiHono() : await Server.openapi()
2026
for (const item of Object.values(specs.paths)) {
2127
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
2228
const operation = item[method]

packages/opencode/src/server/server.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { lazy } from "@/util/lazy"
55
import * as Log from "@opencode-ai/core/util/log"
66
import { Flag } from "@opencode-ai/core/flag/flag"
77
import { WorkspaceID } from "@/control-plane/schema"
8+
import { OpenApi } from "effect/unstable/httpapi"
89
import { MDNS } from "./mdns"
910
import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
1011
import { FenceMiddleware } from "./fence"
@@ -17,6 +18,7 @@ import { WorkspaceRouterMiddleware } from "./workspace"
1718
import { InstanceMiddleware } from "./routes/instance/middleware"
1819
import { WorkspaceRoutes } from "./routes/control/workspace"
1920
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
21+
import { PublicApi } from "./routes/instance/httpapi/public"
2022
import * as ServerBackend from "./backend"
2123
import type { CorsOptions } from "./cors"
2224

@@ -135,7 +137,30 @@ function createHono(opts: CorsOptions, selection: ServerBackend.Selection = Serv
135137
}
136138
}
137139

140+
/**
141+
* Generate the OpenAPI document used by the SDK build.
142+
*
143+
* Since the Effect HttpApi backend now covers every Hono route (plus the new
144+
* `/api/session/*` v2 routes — see `httpapi-bridge.test.ts` for the parity
145+
* audit), `Server.openapi()` derives the spec from `OpenApi.fromApi(PublicApi)`.
146+
* `PublicApi` is `OpenCodeHttpApi` annotated with the `matchLegacyOpenApi`
147+
* transform that injects instance query parameters, strips Effect's optional
148+
* null arms, normalizes component names, and patches SSE response schemas so
149+
* the generated SDK keeps the legacy Hono shape.
150+
*
151+
* The Hono-derived spec is still reachable via `openapiHono()` so reviewers
152+
* can diff the two outputs while the Hono backend lingers; once the Hono
153+
* backend is deleted that helper goes with it.
154+
*/
138155
export async function openapi() {
156+
return OpenApi.fromApi(PublicApi)
157+
}
158+
159+
/**
160+
* Hono-derived OpenAPI spec, retained for parity diffing only. Delete once
161+
* the Hono backend is removed.
162+
*/
163+
export async function openapiHono() {
139164
// Build a fresh app with all routes registered directly so
140165
// hono-openapi can see describeRoute metadata (`.route()` wraps
141166
// handlers when the sub-app has a custom errorHandler, which

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ describe("HttpApi server", () => {
222222
})
223223

224224
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
225-
const honoRoutes = openApiRouteKeys(await Server.openapi())
225+
const honoRoutes = openApiRouteKeys(await Server.openapiHono())
226226
const effectRoutes = openApiRouteKeys(effectOpenApi())
227227

228228
expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([])
@@ -237,7 +237,7 @@ describe("HttpApi server", () => {
237237
})
238238

239239
test("matches generated OpenAPI route parameters", async () => {
240-
const hono = openApiParameters(await Server.openapi())
240+
const hono = openApiParameters(await Server.openapiHono())
241241
const effect = openApiParameters(effectOpenApi())
242242

243243
expect(
@@ -248,7 +248,7 @@ describe("HttpApi server", () => {
248248
})
249249

250250
test("matches generated OpenAPI request body shape", async () => {
251-
const hono = openApiRequestBodies(await Server.openapi())
251+
const hono = openApiRequestBodies(await Server.openapiHono())
252252
const effect = openApiRequestBodies(effectOpenApi())
253253

254254
expect(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ afterEach(async () => {
4646

4747
describe("tui HttpApi bridge", () => {
4848
test("documents legacy bad request responses", async () => {
49-
const legacy = await Server.openapi()
49+
const legacy = await Server.openapiHono()
5050
const effect = OpenApi.fromApi(TuiApi)
5151
for (const path of [TuiPaths.appendPrompt, TuiPaths.executeCommand, TuiPaths.publish, TuiPaths.selectSession]) {
5252
expect(legacy.paths[path].post?.responses?.[400]).toBeDefined()

packages/sdk/js/script/build.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import { createClient } from "@hey-api/openapi-ts"
1212
const openapiSource = process.env.OPENCODE_SDK_OPENAPI === "hono" ? "hono" : "httpapi"
1313
const opencode = path.resolve(dir, "../../opencode")
1414

15+
// `bun dev generate` now derives the spec from the Effect HttpApi contract by
16+
// default; pass `--hono` to fall back to the legacy Hono spec for parity diffs.
1517
if (openapiSource === "httpapi") {
16-
await $`bun dev generate --httpapi > ${dir}/openapi.json`.cwd(opencode)
17-
} else {
1818
await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode)
19+
} else {
20+
await $`bun dev generate --hono > ${dir}/openapi.json`.cwd(opencode)
1921
}
2022

2123
await createClient({

0 commit comments

Comments
 (0)