@@ -21,11 +21,12 @@ type OpenApiParameter = {
2121 name : string
2222 in : string
2323 required ?: boolean
24- schema ?: unknown
24+ schema ?: OpenApiSchema
2525}
2626
2727type OpenApiOperation = {
2828 parameters ?: OpenApiParameter [ ]
29+ responses ?: Record < string , unknown >
2930 requestBody ?: {
3031 required ?: boolean
3132 content ?: Record < string , { schema ?: OpenApiSchema } >
@@ -46,8 +47,12 @@ type OpenApiSchema = {
4647 additionalProperties ?: OpenApiSchema | boolean
4748 allOf ?: OpenApiSchema [ ]
4849 anyOf ?: OpenApiSchema [ ]
50+ enum ?: string [ ]
4951 items ?: OpenApiSchema
52+ maximum ?: number
53+ minimum ?: number
5054 oneOf ?: OpenApiSchema [ ]
55+ prefixItems ?: OpenApiSchema [ ]
5156 properties ?: Record < string , OpenApiSchema >
5257 type ?: string
5358}
@@ -68,6 +73,13 @@ const InstanceQueryParameters = [
6873] satisfies OpenApiParameter [ ]
6974
7075const LegacyBodyRefParameters = new Set ( [ "Auth" , "Config" , "Part" , "WorktreeRemoveInput" , "WorktreeResetInput" ] )
76+ const FiniteNumberValues = new Set ( [ "Infinity" , "-Infinity" , "NaN" ] )
77+ const QueryNumberParameters = new Set ( [ "start" , "cursor" , "limit" , "method" ] )
78+ const QueryBooleanParameters = new Set ( [ "roots" , "archived" ] )
79+ const QueryParameterSchemas = {
80+ "GET /find/file limit" : { type : "integer" , minimum : 1 , maximum : 200 } ,
81+ "GET /session/{sessionID}/message limit" : { type : "integer" , minimum : 0 , maximum : Number . MAX_SAFE_INTEGER } ,
82+ } satisfies Record < string , OpenApiSchema >
7183
7284function matchLegacyOpenApi ( input : Record < string , unknown > ) {
7385 const spec = input as OpenApiSpec
@@ -87,6 +99,45 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
8799 }
88100 if ( media . schema ) media . schema = normalizeRequestSchema ( media . schema )
89101 }
102+ if ( path === "/experimental/workspace" && method === "post" ) {
103+ const properties = operation . requestBody . content ?. [ "application/json" ] ?. schema ?. properties
104+ if ( properties ?. branch ) properties . branch = { anyOf : [ properties . branch , { type : "null" } ] }
105+ if ( properties ?. extra ) properties . extra = { anyOf : [ properties . extra , { type : "null" } ] }
106+ }
107+ if ( path === "/tui/publish" && method === "post" && spec . components ?. schemas ) {
108+ const schema = operation . requestBody . content ?. [ "application/json" ] ?. schema
109+ const anyOf = schema ?. anyOf
110+ if ( anyOf ?. length === 4 ) {
111+ spec . components . schemas . EventTuiPromptAppend = anyOf [ 0 ]
112+ spec . components . schemas . EventTuiCommandExecute = anyOf [ 1 ]
113+ spec . components . schemas . EventTuiToastShow = anyOf [ 2 ]
114+ spec . components . schemas . EventTuiSessionSelect = anyOf [ 3 ]
115+ operation . requestBody . content ! [ "application/json" ] ! . schema = {
116+ anyOf : [
117+ { $ref : "#/components/schemas/EventTuiPromptAppend" } ,
118+ { $ref : "#/components/schemas/EventTuiCommandExecute" } ,
119+ { $ref : "#/components/schemas/EventTuiToastShow" } ,
120+ { $ref : "#/components/schemas/EventTuiSessionSelect" } ,
121+ ] ,
122+ }
123+ }
124+ }
125+ if ( path === "/sync/replay" && method === "post" && spec . components ?. schemas ?. SyncReplayEvent ) {
126+ const events = operation . requestBody . content ?. [ "application/json" ] ?. schema ?. properties ?. events
127+ if ( events ?. items ?. $ref === "#/components/schemas/SyncReplayEvent" ) {
128+ events . items = normalizeRequestSchema ( structuredClone ( spec . components . schemas . SyncReplayEvent ) )
129+ }
130+ }
131+ }
132+ if ( ( path === "/event" || path === "/global/event" ) && method === "get" ) {
133+ operation . responses ! [ "200" ] = {
134+ description : "Event stream" ,
135+ content : {
136+ "text/event-stream" : {
137+ schema : path === "/event" ? { } : { $ref : "#/components/schemas/GlobalEvent" } ,
138+ } ,
139+ } ,
140+ }
90141 }
91142 if ( ! isInstanceRoute ) continue
92143 operation . parameters = [
@@ -95,22 +146,27 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
95146 ( param ) => param . in !== "query" || ( param . name !== "directory" && param . name !== "workspace" ) ,
96147 ) ,
97148 ]
149+ for ( const param of operation . parameters ) normalizeParameter ( param , `${ method . toUpperCase ( ) } ${ path } ` )
98150 }
99151 }
100152 return input
101153}
102154
103155function normalizeRequestSchema ( schema : OpenApiSchema ) : OpenApiSchema {
104- const options = schema . anyOf ?? schema . oneOf
156+ const options = flattenOptions ( schema . anyOf ?? schema . oneOf )
105157 if ( options ) {
106158 const withoutNull = options . filter ( ( item ) => item . type !== "null" )
107159 const finite = withoutNull . find ( ( item ) => item . type === "number" )
108- if ( finite && withoutNull . every ( ( item ) => item . type === "number" || item . type === "string" ) ) return finite
160+ if ( finite && withoutNull . every ( isFiniteNumberOption ) ) return { type : "number" }
109161 if ( withoutNull . length === 1 ) return normalizeRequestSchema ( withoutNull [ 0 ] )
110162 if ( schema . anyOf ) schema . anyOf = withoutNull . map ( normalizeRequestSchema )
111163 if ( schema . oneOf ) schema . oneOf = withoutNull . map ( normalizeRequestSchema )
112164 }
113- if ( schema . allOf ) schema . allOf = schema . allOf . map ( normalizeRequestSchema )
165+ if ( schema . allOf ) {
166+ if ( schema . type ) delete schema . allOf
167+ else schema . allOf = schema . allOf . map ( normalizeRequestSchema )
168+ }
169+ if ( schema . prefixItems && schema . items ) delete schema . prefixItems
114170 if ( schema . items ) schema . items = normalizeRequestSchema ( schema . items )
115171 if ( schema . properties ) {
116172 for ( const [ key , value ] of Object . entries ( schema . properties ) ) {
@@ -123,6 +179,35 @@ function normalizeRequestSchema(schema: OpenApiSchema): OpenApiSchema {
123179 return schema
124180}
125181
182+ function flattenOptions ( options : OpenApiSchema [ ] | undefined ) : OpenApiSchema [ ] | undefined {
183+ return options ?. flatMap ( ( item ) => flattenOptions ( item . anyOf ?? item . oneOf ) ?? [ item ] )
184+ }
185+
186+ function isFiniteNumberOption ( schema : OpenApiSchema ) {
187+ if ( schema . type === "number" ) return true
188+ return schema . type === "string" && schema . enum ?. every ( ( value ) => FiniteNumberValues . has ( value ) ) === true
189+ }
190+
191+ function normalizeParameter ( param : OpenApiParameter , route : string ) {
192+ if ( param . in !== "query" || ! param . schema || typeof param . schema !== "object" ) return
193+ const override = QueryParameterSchemas [ `${ route } ${ param . name } ` as keyof typeof QueryParameterSchemas ]
194+ if ( override ) {
195+ param . schema = override
196+ return
197+ }
198+ if ( QueryNumberParameters . has ( param . name ) ) {
199+ param . schema = { type : "number" }
200+ return
201+ }
202+ if ( QueryBooleanParameters . has ( param . name ) ) {
203+ param . schema = {
204+ anyOf : [ { type : "boolean" } , { type : "string" , enum : [ "true" , "false" ] } ] ,
205+ }
206+ return
207+ }
208+ param . schema = normalizeRequestSchema ( param . schema )
209+ }
210+
126211export const PublicApi = HttpApi . make ( "opencode" )
127212 . addHttpApi ( ControlApi )
128213 . addHttpApi ( GlobalApi )
0 commit comments