Prepare Effect HttpApi backend parity (#24853)
This commit is contained in:
@@ -1,21 +1,5 @@
|
||||
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ConfigApi } from "./config"
|
||||
import { ControlApi } from "./control"
|
||||
import { EventApi } from "./event"
|
||||
import { ExperimentalApi } from "./experimental"
|
||||
import { FileApi } from "./file"
|
||||
import { GlobalApi } from "./global"
|
||||
import { InstanceApi } from "./instance"
|
||||
import { McpApi } from "./mcp"
|
||||
import { PermissionApi } from "./permission"
|
||||
import { ProjectApi } from "./project"
|
||||
import { ProviderApi } from "./provider"
|
||||
import { PtyApi, PtyConnectApi } from "./pty"
|
||||
import { QuestionApi } from "./question"
|
||||
import { SessionApi } from "./session"
|
||||
import { SyncApi } from "./sync"
|
||||
import { TuiApi } from "./tui"
|
||||
import { WorkspaceApi } from "./workspace"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { OpenCodeHttpApi } from "./api"
|
||||
|
||||
type OpenApiParameter = {
|
||||
name: string
|
||||
@@ -26,11 +10,12 @@ type OpenApiParameter = {
|
||||
|
||||
type OpenApiOperation = {
|
||||
parameters?: OpenApiParameter[]
|
||||
responses?: Record<string, unknown>
|
||||
responses?: Record<string, OpenApiResponse>
|
||||
requestBody?: {
|
||||
required?: boolean
|
||||
content?: Record<string, { schema?: OpenApiSchema }>
|
||||
}
|
||||
security?: unknown
|
||||
}
|
||||
|
||||
type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch", OpenApiOperation>>
|
||||
@@ -38,6 +23,7 @@ type OpenApiPathItem = Partial<Record<"get" | "post" | "put" | "delete" | "patch
|
||||
type OpenApiSpec = {
|
||||
components?: {
|
||||
schemas?: Record<string, OpenApiSchema>
|
||||
securitySchemes?: Record<string, unknown>
|
||||
}
|
||||
paths?: Record<string, OpenApiPathItem>
|
||||
}
|
||||
@@ -47,16 +33,25 @@ type OpenApiSchema = {
|
||||
additionalProperties?: OpenApiSchema | boolean
|
||||
allOf?: OpenApiSchema[]
|
||||
anyOf?: OpenApiSchema[]
|
||||
enum?: string[]
|
||||
description?: string
|
||||
enum?: Array<string | boolean>
|
||||
items?: OpenApiSchema
|
||||
maximum?: number
|
||||
minimum?: number
|
||||
oneOf?: OpenApiSchema[]
|
||||
prefixItems?: OpenApiSchema[]
|
||||
properties?: Record<string, OpenApiSchema>
|
||||
required?: string[]
|
||||
type?: string
|
||||
}
|
||||
|
||||
type OpenApiResponse = {
|
||||
description?: string
|
||||
content?: Record<string, { schema?: OpenApiSchema }>
|
||||
}
|
||||
|
||||
// Instance routes use middleware for directory/workspace resolution, but HttpApi
|
||||
// doesn't surface middleware query params in the spec. Inject them explicitly.
|
||||
const InstanceQueryParameters = [
|
||||
{
|
||||
name: "directory",
|
||||
@@ -72,8 +67,9 @@ const InstanceQueryParameters = [
|
||||
},
|
||||
] satisfies OpenApiParameter[]
|
||||
|
||||
const LegacyBodyRefParameters = new Set(["Auth", "Config", "Part", "WorktreeRemoveInput", "WorktreeResetInput"])
|
||||
const FiniteNumberValues = new Set(["Infinity", "-Infinity", "NaN"])
|
||||
// Query schemas describe decoded Effect values, but the generated SDK needs the
|
||||
// public call shape. These keep SDK callers passing numbers/booleans while the
|
||||
// server still decodes string query params at runtime.
|
||||
const QueryNumberParameters = new Set(["start", "cursor", "limit", "method"])
|
||||
const QueryBooleanParameters = new Set(["roots", "archived"])
|
||||
const QueryParameterSchemas = {
|
||||
@@ -81,60 +77,80 @@ const QueryParameterSchemas = {
|
||||
"GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
||||
} satisfies Record<string, OpenApiSchema>
|
||||
|
||||
const LegacyComponentDescriptions = {
|
||||
LogLevel: "Log level",
|
||||
ServerConfig: "Server configuration for opencode serve and web commands",
|
||||
LayoutConfig: "@deprecated Always uses stretch layout.",
|
||||
} satisfies Record<string, string>
|
||||
|
||||
function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
const spec = input as OpenApiSpec
|
||||
|
||||
// Effect's multi-document JSON Schema deduplicator can produce self-referencing
|
||||
// component schemas (e.g. `{"$ref":"#/components/schemas/X"}` as the definition
|
||||
// of X itself) when the same AST node appears both as a standalone endpoint
|
||||
// payload and inside an annotated union arm. Resolve these by inlining the
|
||||
// actual schema from any parent union that references them.
|
||||
fixSelfReferencingComponents(spec)
|
||||
|
||||
// Effect's Schema.optional emits `anyOf: [T, {type:"null"}]` in OpenAPI,
|
||||
// but the legacy SDK expected plain `T` for optional fields. Strip null
|
||||
// from all component schemas so both request and response types match.
|
||||
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
||||
spec.components!.schemas![name] = stripOptionalNull(structuredClone(schema))
|
||||
}
|
||||
normalizeComponentNames(spec)
|
||||
collapseDuplicateComponents(spec)
|
||||
applyLegacySchemaOverrides(spec)
|
||||
normalizeComponentDescriptions(spec)
|
||||
addLegacyErrorSchemas(spec)
|
||||
delete spec.components?.schemas?.Unauthorized
|
||||
delete spec.components?.schemas?.EffectHttpApiErrorBadRequest
|
||||
delete spec.components?.schemas?.EffectHttpApiErrorNotFound
|
||||
delete spec.components?.schemas?.effect_HttpApiError_BadRequest
|
||||
delete spec.components?.schemas?.effect_HttpApiError_NotFound
|
||||
delete spec.components?.securitySchemes
|
||||
|
||||
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
||||
const isInstanceRoute = !path.startsWith("/global/") && !path.startsWith("/auth/")
|
||||
for (const method of ["get", "post", "put", "delete", "patch"] as const) {
|
||||
const operation = item[method]
|
||||
if (!operation) continue
|
||||
if (operation.requestBody) {
|
||||
// Hono's generated OpenAPI never marked request bodies as required. Keep
|
||||
// that SDK surface stable during the HttpApi migration.
|
||||
delete operation.requestBody.required
|
||||
for (const media of Object.values(operation.requestBody.content ?? {})) {
|
||||
const ref = media.schema?.$ref?.replace("#/components/schemas/", "")
|
||||
if (ref && LegacyBodyRefParameters.has(ref)) continue
|
||||
if (ref && spec.components?.schemas?.[ref]) {
|
||||
media.schema = normalizeRequestSchema(structuredClone(spec.components.schemas[ref]))
|
||||
continue
|
||||
}
|
||||
if (media.schema) media.schema = normalizeRequestSchema(media.schema)
|
||||
}
|
||||
const body = operation.requestBody.content?.["application/json"]
|
||||
if (body?.schema) body.schema = stripOptionalNull(structuredClone(body.schema))
|
||||
if (path === "/experimental/workspace" && method === "post") {
|
||||
const properties = operation.requestBody.content?.["application/json"]?.schema?.properties
|
||||
// Workspace creation fields `branch` and `extra` are Schema.NullOr —
|
||||
// genuinely nullable, not just optional. Re-add the null that the
|
||||
// component-level strip above removed.
|
||||
const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace("#/components/schemas/", "")
|
||||
const properties = ref ? spec.components?.schemas?.[ref]?.properties : operation.requestBody.content?.["application/json"]?.schema?.properties
|
||||
if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
|
||||
if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
|
||||
}
|
||||
if (path === "/tui/publish" && method === "post" && spec.components?.schemas) {
|
||||
const schema = operation.requestBody.content?.["application/json"]?.schema
|
||||
const anyOf = schema?.anyOf
|
||||
if (anyOf?.length === 4) {
|
||||
spec.components.schemas.EventTuiPromptAppend = anyOf[0]
|
||||
spec.components.schemas.EventTuiCommandExecute = anyOf[1]
|
||||
spec.components.schemas.EventTuiToastShow = anyOf[2]
|
||||
spec.components.schemas.EventTuiSessionSelect = anyOf[3]
|
||||
operation.requestBody.content!["application/json"]!.schema = {
|
||||
anyOf: [
|
||||
{ $ref: "#/components/schemas/EventTuiPromptAppend" },
|
||||
{ $ref: "#/components/schemas/EventTuiCommandExecute" },
|
||||
{ $ref: "#/components/schemas/EventTuiToastShow" },
|
||||
{ $ref: "#/components/schemas/EventTuiSessionSelect" },
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
if (path === "/sync/replay" && method === "post" && spec.components?.schemas?.SyncReplayEvent) {
|
||||
const events = operation.requestBody.content?.["application/json"]?.schema?.properties?.events
|
||||
if (events?.items?.$ref === "#/components/schemas/SyncReplayEvent") {
|
||||
events.items = normalizeRequestSchema(structuredClone(spec.components.schemas.SyncReplayEvent))
|
||||
}
|
||||
}
|
||||
for (const response of Object.values(operation.responses ?? {})) {
|
||||
for (const content of Object.values(response.content ?? {})) {
|
||||
if (content.schema) content.schema = stripOptionalNull(structuredClone(content.schema))
|
||||
}
|
||||
}
|
||||
// Hono applied auth as runtime middleware outside OpenAPI metadata, so the
|
||||
// legacy SDK did not expose auth schemes or generated 401 error unions.
|
||||
delete operation.security
|
||||
delete operation.responses?.["401"]
|
||||
normalizeLegacyErrorResponses(operation)
|
||||
normalizeLegacyOperation(operation, path, method)
|
||||
if ((path === "/event" || path === "/global/event") && method === "get") {
|
||||
// HttpApi has no first-class SSE response schema, and these handlers are
|
||||
// raw/streaming routes. Document the actual wire protocol explicitly.
|
||||
operation.responses!["200"] = {
|
||||
description: "Event stream",
|
||||
content: {
|
||||
"text/event-stream": {
|
||||
schema: path === "/event" ? {} : { $ref: "#/components/schemas/GlobalEvent" },
|
||||
schema: path === "/event" ? { $ref: "#/components/schemas/Event" } : { $ref: "#/components/schemas/GlobalEvent" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -152,40 +168,302 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
|
||||
return input
|
||||
}
|
||||
|
||||
function normalizeRequestSchema(schema: OpenApiSchema): OpenApiSchema {
|
||||
function addLegacyErrorSchemas(spec: OpenApiSpec) {
|
||||
if (!spec.components?.schemas) return
|
||||
spec.components.schemas.BadRequestError = {
|
||||
type: "object",
|
||||
required: ["data", "errors", "success"],
|
||||
properties: {
|
||||
data: {},
|
||||
errors: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
success: { type: "boolean", enum: [false] },
|
||||
},
|
||||
}
|
||||
spec.components.schemas.NotFoundError = {
|
||||
type: "object",
|
||||
required: ["name", "data"],
|
||||
properties: {
|
||||
name: { type: "string", enum: ["NotFoundError"] },
|
||||
data: {
|
||||
type: "object",
|
||||
required: ["message"],
|
||||
properties: {
|
||||
message: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function collapseDuplicateComponents(spec: OpenApiSpec) {
|
||||
const schemas = spec.components?.schemas
|
||||
if (!schemas) return
|
||||
for (const name of Object.keys(schemas)) {
|
||||
const base = name.replace(/\d+$/, "")
|
||||
if (base === name || !schemas[base]) continue
|
||||
if (stableSchema(schemas[name], schemas) !== stableSchema(schemas[base], schemas)) continue
|
||||
rewriteRefs(spec, name, base)
|
||||
delete schemas[name]
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeComponentNames(spec: OpenApiSpec) {
|
||||
const schemas = spec.components?.schemas
|
||||
if (!schemas) return
|
||||
for (const name of Object.keys(schemas)) {
|
||||
const next = componentTypeName(name)
|
||||
if (next === name) continue
|
||||
if (schemas[next]) {
|
||||
if (stableSchema(schemas[name], schemas) === stableSchema(schemas[next], schemas)) {
|
||||
rewriteRefs(spec, name, next)
|
||||
delete schemas[name]
|
||||
}
|
||||
continue
|
||||
}
|
||||
schemas[next] = schemas[name]
|
||||
rewriteRefs(spec, name, next)
|
||||
delete schemas[name]
|
||||
}
|
||||
}
|
||||
|
||||
function componentTypeName(name: string) {
|
||||
if (!name.includes(".")) return name
|
||||
return name
|
||||
.split(".")
|
||||
.filter((part) => !/^\d+$/.test(part))
|
||||
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
||||
.join("")
|
||||
}
|
||||
|
||||
function applyLegacySchemaOverrides(spec: OpenApiSpec) {
|
||||
const schemas = spec.components?.schemas
|
||||
if (!schemas) return
|
||||
if (schemas.AgentConfig) schemas.AgentConfig.additionalProperties = {}
|
||||
if (schemas.Command?.properties?.template) schemas.Command.properties.template = { type: "string" }
|
||||
if (schemas.Workspace?.properties) {
|
||||
schemas.Workspace.properties.branch = nullable(schemas.Workspace.properties.branch)
|
||||
schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory)
|
||||
schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra)
|
||||
}
|
||||
if (schemas.GlobalSession?.properties?.project) schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project)
|
||||
const providerOptions = schemas.ProviderConfig?.properties?.options
|
||||
if (providerOptions) providerOptions.additionalProperties = {}
|
||||
const model = schemas.ProviderConfig?.properties?.models?.additionalProperties
|
||||
const variants = typeof model === "object" ? model.properties?.variants?.additionalProperties : undefined
|
||||
if (variants && typeof variants === "object") variants.additionalProperties = {}
|
||||
const syncInfo = schemas.SyncEventSessionUpdated?.properties?.data?.properties?.info
|
||||
if (syncInfo?.properties) makePropertiesNullable(syncInfo.properties)
|
||||
}
|
||||
|
||||
function normalizeComponentDescriptions(spec: OpenApiSpec) {
|
||||
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
||||
const description = LegacyComponentDescriptions[name as keyof typeof LegacyComponentDescriptions]
|
||||
if (description) {
|
||||
schema.description = description
|
||||
continue
|
||||
}
|
||||
delete schema.description
|
||||
}
|
||||
}
|
||||
|
||||
function makePropertiesNullable(properties: Record<string, OpenApiSchema>) {
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (key === "share" && value.properties?.url) {
|
||||
value.properties.url = nullable(value.properties.url)
|
||||
continue
|
||||
}
|
||||
if (key === "time" && value.properties) {
|
||||
makePropertiesNullable(value.properties)
|
||||
continue
|
||||
}
|
||||
properties[key] = nullable(value)
|
||||
}
|
||||
}
|
||||
|
||||
function nullable(schema: OpenApiSchema): OpenApiSchema {
|
||||
if (flattenOptions(schema.anyOf ?? schema.oneOf)?.some((item) => item.type === "null")) return schema
|
||||
return { anyOf: [schema, { type: "null" }] }
|
||||
}
|
||||
|
||||
function stableSchema(input: unknown, schemas: Record<string, OpenApiSchema>): string {
|
||||
return JSON.stringify(canonicalizeSchema(input, schemas))
|
||||
}
|
||||
|
||||
function canonicalizeSchema(input: unknown, schemas: Record<string, OpenApiSchema>): unknown {
|
||||
if (Array.isArray(input)) return input.map((item) => canonicalizeSchema(item, schemas))
|
||||
if (!input || typeof input !== "object") return input
|
||||
const schema = input as OpenApiSchema
|
||||
if (schema.$ref) return { $ref: canonicalRef(schema.$ref, schemas) }
|
||||
return Object.fromEntries(
|
||||
Object.entries(input)
|
||||
.filter(([key]) => key !== "description")
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => [key, canonicalizeSchema(value, schemas)]),
|
||||
)
|
||||
}
|
||||
|
||||
function canonicalRef(ref: string, schemas: Record<string, OpenApiSchema>) {
|
||||
const name = ref.replace("#/components/schemas/", "")
|
||||
const base = name.replace(/\d+$/, "")
|
||||
if (base !== name && schemas[base]) return `#/components/schemas/${base}`
|
||||
return ref
|
||||
}
|
||||
|
||||
function rewriteRefs(input: unknown, from: string, to: string): void {
|
||||
if (Array.isArray(input)) {
|
||||
for (const item of input) rewriteRefs(item, from, to)
|
||||
return
|
||||
}
|
||||
if (!input || typeof input !== "object") return
|
||||
const schema = input as OpenApiSchema
|
||||
if (schema.$ref === `#/components/schemas/${from}`) schema.$ref = `#/components/schemas/${to}`
|
||||
for (const value of Object.values(input)) rewriteRefs(value, from, to)
|
||||
}
|
||||
|
||||
function normalizeLegacyErrorResponses(operation: OpenApiOperation) {
|
||||
if (operation.responses?.["400"] && isBuiltInErrorResponse(operation.responses["400"], "BadRequest")) {
|
||||
operation.responses["400"] = legacyErrorResponse("Bad request", "BadRequestError")
|
||||
}
|
||||
if (operation.responses?.["404"] && isBuiltInErrorResponse(operation.responses["404"], "NotFound")) {
|
||||
operation.responses["404"] = legacyErrorResponse("Not found", "NotFoundError")
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLegacyOperation(operation: OpenApiOperation, path: string, method: string) {
|
||||
if (path === "/experimental/console/switch" && method === "post") delete operation.responses?.["400"]
|
||||
if (path === "/pty/{ptyID}" && method === "put") delete operation.responses?.["404"]
|
||||
if ((path !== "/session/{sessionID}/message" && path !== "/session/{sessionID}/command") || method !== "post") return
|
||||
const response = operation.responses?.["200"]?.content?.["application/json"]
|
||||
if (!response) return
|
||||
response.schema = {
|
||||
type: "object",
|
||||
required: ["info", "parts"],
|
||||
properties: {
|
||||
info: { $ref: "#/components/schemas/AssistantMessage" },
|
||||
parts: {
|
||||
type: "array",
|
||||
items: { $ref: "#/components/schemas/Part" },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isRefResponse(response: OpenApiResponse, name: string) {
|
||||
return response.content?.["application/json"]?.schema?.$ref === `#/components/schemas/${name}`
|
||||
}
|
||||
|
||||
function isBuiltInErrorResponse(response: OpenApiResponse, name: "BadRequest" | "NotFound") {
|
||||
return response.description === name || isRefResponse(response, `EffectHttpApiError${name}`)
|
||||
}
|
||||
|
||||
function legacyErrorResponse(description: string, name: "BadRequestError" | "NotFoundError"): OpenApiResponse {
|
||||
return {
|
||||
description,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: `#/components/schemas/${name}` },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix component schemas that are self-referencing `$ref`s — an Effect OpenAPI
|
||||
* generation bug where annotated union arms that share AST nodes with other
|
||||
* endpoints produce `{"$ref":"#/components/schemas/X"}` as the definition of X.
|
||||
*
|
||||
* Resolves by finding the actual schema from a parent union's `anyOf`/`oneOf`
|
||||
* that references the broken component, then inlining that schema.
|
||||
*/
|
||||
function fixSelfReferencingComponents(spec: OpenApiSpec) {
|
||||
const schemas = spec.components?.schemas
|
||||
if (!schemas) return
|
||||
const selfRefs = new Set<string>()
|
||||
for (const [name, schema] of Object.entries(schemas)) {
|
||||
if (schema.$ref === `#/components/schemas/${name}`) selfRefs.add(name)
|
||||
}
|
||||
if (selfRefs.size === 0) return
|
||||
// Find a parent union component whose anyOf/oneOf contains a $ref to the
|
||||
// broken component — that parent was generated correctly and holds the inline
|
||||
// schema we need.
|
||||
for (const [, schema] of Object.entries(schemas)) {
|
||||
for (const member of schema.anyOf ?? schema.oneOf ?? []) {
|
||||
const ref = member.$ref?.replace("#/components/schemas/", "")
|
||||
if (!ref || !selfRefs.has(ref)) continue
|
||||
// This member's $ref points to a self-referencing component. The member
|
||||
// itself is just {$ref:...}, so the actual schema must be resolved from
|
||||
// the union. Since the union component was generated before the
|
||||
// deduplicator broke things, the inline version lives elsewhere. Generate
|
||||
// a fresh spec without the transform to get the correct schema.
|
||||
// Simpler approach: look through all paths for an endpoint that uses this
|
||||
// schema as a payload (it would have been expanded by the ref-expansion
|
||||
// logic above if we ran after that, but we run before). Instead, just
|
||||
// delete the broken component — if it's referenced via $ref elsewhere,
|
||||
// the ref expansion in the request body loop will inline it anyway.
|
||||
}
|
||||
}
|
||||
// Simplest fix: generate the raw spec (without transform) to get correct schemas
|
||||
const raw = OpenApi.fromApi(OpenCodeHttpApi) as unknown as OpenApiSpec
|
||||
const rawSchemas = raw.components?.schemas
|
||||
if (!rawSchemas) return
|
||||
for (const name of selfRefs) {
|
||||
if (rawSchemas[name]) schemas[name] = rawSchemas[name]
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */
|
||||
function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema {
|
||||
if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} }
|
||||
const options = flattenOptions(schema.anyOf ?? schema.oneOf)
|
||||
if (options) {
|
||||
const withoutNull = options.filter((item) => item.type !== "null")
|
||||
const finite = withoutNull.find((item) => item.type === "number")
|
||||
if (finite && withoutNull.every(isFiniteNumberOption)) return { type: "number" }
|
||||
if (withoutNull.length === 1) return normalizeRequestSchema(withoutNull[0])
|
||||
if (schema.anyOf) schema.anyOf = withoutNull.map(normalizeRequestSchema)
|
||||
if (schema.oneOf) schema.oneOf = withoutNull.map(normalizeRequestSchema)
|
||||
if (withoutNull.length === 1) return stripOptionalNull(withoutNull[0])
|
||||
if (schema.anyOf) schema.anyOf = withoutNull.map(stripOptionalNull)
|
||||
if (schema.oneOf) schema.oneOf = withoutNull.map(stripOptionalNull)
|
||||
}
|
||||
if (schema.allOf) {
|
||||
if (schema.type) delete schema.allOf
|
||||
else schema.allOf = schema.allOf.map(normalizeRequestSchema)
|
||||
const allOf = schema.allOf.map(stripOptionalNull)
|
||||
if (schema.type) {
|
||||
delete schema.allOf
|
||||
for (const item of allOf) Object.assign(schema, item)
|
||||
} else {
|
||||
schema.allOf = allOf
|
||||
}
|
||||
}
|
||||
if (schema.prefixItems && schema.items) delete schema.prefixItems
|
||||
if (schema.items) schema.items = normalizeRequestSchema(schema.items)
|
||||
if (schema.items) schema.items = stripOptionalNull(schema.items)
|
||||
if (schema.properties) {
|
||||
for (const [key, value] of Object.entries(schema.properties)) {
|
||||
schema.properties[key] = normalizeRequestSchema(value)
|
||||
schema.properties[key] = stripOptionalNull(value)
|
||||
}
|
||||
}
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
schema.additionalProperties = normalizeRequestSchema(schema.additionalProperties)
|
||||
schema.additionalProperties = stripOptionalNull(schema.additionalProperties)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | undefined {
|
||||
return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item])
|
||||
function isEmptyObjectUnion(schema: OpenApiSchema) {
|
||||
const options = schema.anyOf ?? schema.oneOf
|
||||
return options?.length === 2 && options.some(isBareObjectSchema) && options.some(isBareArraySchema)
|
||||
}
|
||||
|
||||
function isFiniteNumberOption(schema: OpenApiSchema) {
|
||||
if (schema.type === "number") return true
|
||||
return schema.type === "string" && schema.enum?.every((value) => FiniteNumberValues.has(value)) === true
|
||||
function isBareObjectSchema(schema: OpenApiSchema) {
|
||||
return schema.type === "object" && !schema.properties && !schema.additionalProperties
|
||||
}
|
||||
|
||||
function isBareArraySchema(schema: OpenApiSchema) {
|
||||
return schema.type === "array" && !schema.items && !schema.prefixItems
|
||||
}
|
||||
|
||||
function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | undefined {
|
||||
return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item])
|
||||
}
|
||||
|
||||
function normalizeParameter(param: OpenApiParameter, route: string) {
|
||||
@@ -205,28 +483,10 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
param.schema = normalizeRequestSchema(param.schema)
|
||||
param.schema = stripOptionalNull(param.schema)
|
||||
}
|
||||
|
||||
export const PublicApi = HttpApi.make("opencode")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(GlobalApi)
|
||||
.addHttpApi(EventApi)
|
||||
.addHttpApi(ConfigApi)
|
||||
.addHttpApi(ExperimentalApi)
|
||||
.addHttpApi(FileApi)
|
||||
.addHttpApi(InstanceApi)
|
||||
.addHttpApi(McpApi)
|
||||
.addHttpApi(PermissionApi)
|
||||
.addHttpApi(ProjectApi)
|
||||
.addHttpApi(ProviderApi)
|
||||
.addHttpApi(PtyApi)
|
||||
.addHttpApi(PtyConnectApi)
|
||||
.addHttpApi(QuestionApi)
|
||||
.addHttpApi(SessionApi)
|
||||
.addHttpApi(SyncApi)
|
||||
.addHttpApi(TuiApi)
|
||||
.addHttpApi(WorkspaceApi)
|
||||
export const PublicApi = OpenCodeHttpApi
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode",
|
||||
|
||||
Reference in New Issue
Block a user