summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-05-01 22:18:52 -0400
committerGitHub <[email protected]>2026-05-01 22:18:52 -0400
commit0b498dd4483a408dc2e142bde3d7c6173cd824db (patch)
tree7ec3706702ed943bbc3c367ef50489379bc5bd4f
parentcec9c6122af88ed76264f9e899a26fb250943df3 (diff)
downloadopencode-0b498dd4483a408dc2e142bde3d7c6173cd824db.tar.gz
opencode-0b498dd4483a408dc2e142bde3d7c6173cd824db.zip
fix(httpapi): preserve OpenAPI parameter parity (#25291)
-rw-r--r--packages/opencode/src/server/routes/instance/AGENTS.md8
-rw-r--r--packages/opencode/src/server/routes/instance/httpapi/public.ts54
-rw-r--r--packages/opencode/test/server/httpapi-bridge.test.ts18
3 files changed, 67 insertions, 13 deletions
diff --git a/packages/opencode/src/server/routes/instance/AGENTS.md b/packages/opencode/src/server/routes/instance/AGENTS.md
new file mode 100644
index 000000000..c94fa64af
--- /dev/null
+++ b/packages/opencode/src/server/routes/instance/AGENTS.md
@@ -0,0 +1,8 @@
+# Instance Route Parity
+
+This directory contains the legacy Hono instance routes and the experimental Effect HttpApi implementation under `httpapi/`. Keep them behaviorally aligned.
+
+- When adding, removing, or changing a legacy Hono route, update the matching Effect HttpApi group and handler in `httpapi/` in the same change unless the route is intentionally unsupported.
+- When changing an Effect HttpApi route, verify the legacy Hono route has the same public behavior, request shape, response shape, status codes, and instance/workspace routing semantics.
+- Keep OpenAPI/SDK-visible schemas aligned. If a difference is only an OpenAPI generation artifact, prefer fixing the source schema first; use `httpapi/public.ts` normalization only for compatibility shims that cannot be represented cleanly in the source schema.
+- Add or update parity coverage in `test/server/httpapi-bridge.test.ts` or the focused HttpApi tests when behavior or schema parity could regress.
diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts
index 17d6e0d06..c9668336a 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/public.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts
@@ -39,6 +39,7 @@ type OpenApiSchema = {
maximum?: number
minimum?: number
oneOf?: OpenApiSchema[]
+ pattern?: string
prefixItems?: OpenApiSchema[]
properties?: Record<string, OpenApiSchema>
required?: string[]
@@ -74,9 +75,18 @@ const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
const QueryBooleanParameters = new Set(["roots", "archived"])
const QueryParameterSchemas = {
"GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 },
+ "GET /session/{sessionID}/diff messageID": { type: "string", pattern: "^msg.*" },
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
} satisfies Record<string, OpenApiSchema>
+const PathParameterSchemas = {
+ sessionID: { type: "string", pattern: "^ses.*" },
+ messageID: { type: "string", pattern: "^msg.*" },
+ partID: { type: "string", pattern: "^prt.*" },
+ permissionID: { type: "string", pattern: "^per.*" },
+ ptyID: { type: "string", pattern: "^pty.*" },
+} satisfies Record<string, OpenApiSchema>
+
const LegacyComponentDescriptions = {
LogLevel: "Log level",
ServerConfig: "Server configuration for opencode serve and web commands",
@@ -428,6 +438,11 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) {
/** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */
function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema {
+ if (schema.allOf?.length === 1) {
+ const [constraint] = schema.allOf
+ delete schema.allOf
+ return stripOptionalNull({ ...schema, ...constraint })
+ }
if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} }
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
if (options) {
@@ -476,25 +491,40 @@ function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] |
}
function normalizeParameter(param: OpenApiParameter, route: string) {
- if (param.in !== "query" || !param.schema || typeof param.schema !== "object") return
- const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
- if (override) {
- param.schema = override
+ if (!param.schema || typeof param.schema !== "object") return
+ if (param.in === "path") {
+ param.schema = pathParameterSchema(route, param.name) ?? stripOptionalNull(param.schema)
return
}
- if (QueryNumberParameters.has(param.name)) {
- param.schema = { type: "number" }
- return
- }
- if (QueryBooleanParameters.has(param.name)) {
- param.schema = {
- anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
+ if (param.in === "query") {
+ const override = QueryParameterSchemas[`${route} ${param.name}` as keyof typeof QueryParameterSchemas]
+ if (override) {
+ param.schema = override
+ return
+ }
+ if (QueryNumberParameters.has(param.name)) {
+ param.schema = { type: "number" }
+ return
+ }
+ if (QueryBooleanParameters.has(param.name)) {
+ param.schema = {
+ anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }],
+ }
+ return
}
- return
}
param.schema = stripOptionalNull(param.schema)
}
+function pathParameterSchema(route: string, name: string) {
+ if (name in PathParameterSchemas) return PathParameterSchemas[name as keyof typeof PathParameterSchemas]
+ if (name === "id" && route.startsWith("DELETE /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
+ if (name === "id" && route.startsWith("POST /experimental/workspace/")) return { type: "string", pattern: "^wrk.*" }
+ if (name === "requestID" && route.startsWith("POST /permission/")) return { type: "string", pattern: "^per.*" }
+ if (name === "requestID" && route.startsWith("POST /question/")) return { type: "string", pattern: "^que.*" }
+ return undefined
+}
+
export const PublicApi = OpenCodeHttpApi.annotateMerge(
OpenApi.annotations({
title: "opencode",
diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts
index a01b7330e..2b8a62cc5 100644
--- a/packages/opencode/test/server/httpapi-bridge.test.ts
+++ b/packages/opencode/test/server/httpapi-bridge.test.ts
@@ -119,7 +119,23 @@ type RequestBody = {
function parameterKey(param: unknown): string | undefined {
if (!param || typeof param !== "object" || !("in" in param) || !("name" in param)) return undefined
if (typeof param.in !== "string" || typeof param.name !== "string") return undefined
- return `${param.in}:${param.name}:${"required" in param && param.required === true}`
+ return `${param.in}:${param.name}:${"required" in param && param.required === true}:${stableSchema(
+ "schema" in param ? param.schema : undefined,
+ )}`
+}
+
+function stableSchema(input: unknown): string {
+ return JSON.stringify(sortSchema(input))
+}
+
+function sortSchema(input: unknown): unknown {
+ if (Array.isArray(input)) return input.map(sortSchema)
+ if (!input || typeof input !== "object") return input
+ return Object.fromEntries(
+ Object.entries(input)
+ .sort(([left], [right]) => left.localeCompare(right))
+ .map(([key, value]) => [key, sortSchema(value)]),
+ )
}
function parameterSchema(input: {