Skip to content

Commit bb6f7d2

Browse files
Copilotrootfs
andcommitted
Add OpenAPI 3.0 spec generation and Swagger UI
- Implement OpenAPI 3.0 specification structures - Add generateOpenAPISpec() to dynamically generate spec from registry - Add /openapi.json endpoint serving OpenAPI 3.0 spec - Add /docs endpoint serving interactive Swagger UI - Update endpoint registry to include new documentation endpoints - Add openapi_spec and swagger_ui links to API overview - Automatically filter system prompt endpoints in spec based on config - Add comprehensive tests for OpenAPI and Swagger UI endpoints - Tests verify spec structure, filtering, and UI rendering Co-authored-by: rootfs <[email protected]>
1 parent 34001e2 commit bb6f7d2

File tree

2 files changed

+409
-0
lines changed

2 files changed

+409
-0
lines changed

src/semantic-router/pkg/api/server.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ func (s *ClassificationAPIServer) setupRoutes() *http.ServeMux {
187187
// API discovery endpoint
188188
mux.HandleFunc("GET /api/v1", s.handleAPIOverview)
189189

190+
// OpenAPI and documentation endpoints
191+
mux.HandleFunc("GET /openapi.json", s.handleOpenAPISpec)
192+
mux.HandleFunc("GET /docs", s.handleSwaggerUI)
193+
190194
// Classification endpoints
191195
mux.HandleFunc("POST /api/v1/classify/intent", s.handleIntentClassification)
192196
mux.HandleFunc("POST /api/v1/classify/pii", s.handlePIIDetection)
@@ -261,6 +265,8 @@ type EndpointMetadata struct {
261265
var endpointRegistry = []EndpointMetadata{
262266
{Path: "/health", Method: "GET", Description: "Health check endpoint"},
263267
{Path: "/api/v1", Method: "GET", Description: "API discovery and documentation"},
268+
{Path: "/openapi.json", Method: "GET", Description: "OpenAPI 3.0 specification"},
269+
{Path: "/docs", Method: "GET", Description: "Interactive Swagger UI documentation"},
264270
{Path: "/api/v1/classify/intent", Method: "POST", Description: "Classify user queries into routing categories"},
265271
{Path: "/api/v1/classify/pii", Method: "POST", Description: "Detect personally identifiable information in text"},
266272
{Path: "/api/v1/classify/security", Method: "POST", Description: "Detect jailbreak attempts and security threats"},
@@ -284,6 +290,78 @@ var taskTypeRegistry = []TaskTypeInfo{
284290
{Name: "all", Description: "All classification types combined"},
285291
}
286292

293+
// OpenAPI 3.0 spec structures
294+
295+
// OpenAPISpec represents an OpenAPI 3.0 specification
296+
type OpenAPISpec struct {
297+
OpenAPI string `json:"openapi"`
298+
Info OpenAPIInfo `json:"info"`
299+
Servers []OpenAPIServer `json:"servers"`
300+
Paths map[string]OpenAPIPath `json:"paths"`
301+
Components OpenAPIComponents `json:"components,omitempty"`
302+
}
303+
304+
// OpenAPIInfo contains API metadata
305+
type OpenAPIInfo struct {
306+
Title string `json:"title"`
307+
Description string `json:"description"`
308+
Version string `json:"version"`
309+
}
310+
311+
// OpenAPIServer describes a server
312+
type OpenAPIServer struct {
313+
URL string `json:"url"`
314+
Description string `json:"description"`
315+
}
316+
317+
// OpenAPIPath represents operations for a path
318+
type OpenAPIPath struct {
319+
Get *OpenAPIOperation `json:"get,omitempty"`
320+
Post *OpenAPIOperation `json:"post,omitempty"`
321+
Put *OpenAPIOperation `json:"put,omitempty"`
322+
Delete *OpenAPIOperation `json:"delete,omitempty"`
323+
}
324+
325+
// OpenAPIOperation describes an API operation
326+
type OpenAPIOperation struct {
327+
Summary string `json:"summary"`
328+
Description string `json:"description,omitempty"`
329+
OperationID string `json:"operationId,omitempty"`
330+
Responses map[string]OpenAPIResponse `json:"responses"`
331+
RequestBody *OpenAPIRequestBody `json:"requestBody,omitempty"`
332+
}
333+
334+
// OpenAPIResponse describes a response
335+
type OpenAPIResponse struct {
336+
Description string `json:"description"`
337+
Content map[string]OpenAPIMedia `json:"content,omitempty"`
338+
}
339+
340+
// OpenAPIRequestBody describes a request body
341+
type OpenAPIRequestBody struct {
342+
Description string `json:"description,omitempty"`
343+
Required bool `json:"required,omitempty"`
344+
Content map[string]OpenAPIMedia `json:"content"`
345+
}
346+
347+
// OpenAPIMedia describes media type content
348+
type OpenAPIMedia struct {
349+
Schema *OpenAPISchema `json:"schema,omitempty"`
350+
}
351+
352+
// OpenAPISchema describes a schema
353+
type OpenAPISchema struct {
354+
Type string `json:"type,omitempty"`
355+
Properties map[string]OpenAPISchema `json:"properties,omitempty"`
356+
Items *OpenAPISchema `json:"items,omitempty"`
357+
Ref string `json:"$ref,omitempty"`
358+
}
359+
360+
// OpenAPIComponents contains reusable components
361+
type OpenAPIComponents struct {
362+
Schemas map[string]OpenAPISchema `json:"schemas,omitempty"`
363+
}
364+
287365
// handleAPIOverview handles GET /api/v1 for API discovery
288366
func (s *ClassificationAPIServer) handleAPIOverview(w http.ResponseWriter, r *http.Request) {
289367
// Build endpoints list from registry, filtering out disabled endpoints
@@ -308,6 +386,8 @@ func (s *ClassificationAPIServer) handleAPIOverview(w http.ResponseWriter, r *ht
308386
TaskTypes: taskTypeRegistry,
309387
Links: map[string]string{
310388
"documentation": "https://vllm-project.github.io/semantic-router/",
389+
"openapi_spec": "/openapi.json",
390+
"swagger_ui": "/docs",
311391
"models_info": "/info/models",
312392
"health": "/health",
313393
},
@@ -316,6 +396,158 @@ func (s *ClassificationAPIServer) handleAPIOverview(w http.ResponseWriter, r *ht
316396
s.writeJSONResponse(w, http.StatusOK, response)
317397
}
318398

399+
// generateOpenAPISpec generates an OpenAPI 3.0 specification from the endpoint registry
400+
func (s *ClassificationAPIServer) generateOpenAPISpec() OpenAPISpec {
401+
spec := OpenAPISpec{
402+
OpenAPI: "3.0.0",
403+
Info: OpenAPIInfo{
404+
Title: "Semantic Router Classification API",
405+
Description: "API for intent classification, PII detection, and security analysis",
406+
Version: "v1",
407+
},
408+
Servers: []OpenAPIServer{
409+
{
410+
URL: "/",
411+
Description: "Classification API Server",
412+
},
413+
},
414+
Paths: make(map[string]OpenAPIPath),
415+
}
416+
417+
// Generate paths from endpoint registry
418+
for _, endpoint := range endpointRegistry {
419+
// Filter out system prompt endpoints if they are disabled
420+
if !s.enableSystemPromptAPI && endpoint.Path == "/config/system-prompts" {
421+
continue
422+
}
423+
424+
path, ok := spec.Paths[endpoint.Path]
425+
if !ok {
426+
path = OpenAPIPath{}
427+
}
428+
429+
operation := &OpenAPIOperation{
430+
Summary: endpoint.Description,
431+
Description: endpoint.Description,
432+
OperationID: fmt.Sprintf("%s_%s", endpoint.Method, endpoint.Path),
433+
Responses: map[string]OpenAPIResponse{
434+
"200": {
435+
Description: "Successful response",
436+
Content: map[string]OpenAPIMedia{
437+
"application/json": {
438+
Schema: &OpenAPISchema{
439+
Type: "object",
440+
},
441+
},
442+
},
443+
},
444+
"400": {
445+
Description: "Bad request",
446+
Content: map[string]OpenAPIMedia{
447+
"application/json": {
448+
Schema: &OpenAPISchema{
449+
Type: "object",
450+
Properties: map[string]OpenAPISchema{
451+
"error": {
452+
Type: "object",
453+
Properties: map[string]OpenAPISchema{
454+
"code": {Type: "string"},
455+
"message": {Type: "string"},
456+
"timestamp": {Type: "string"},
457+
},
458+
},
459+
},
460+
},
461+
},
462+
},
463+
},
464+
},
465+
}
466+
467+
// Add request body for POST and PUT methods
468+
if endpoint.Method == "POST" || endpoint.Method == "PUT" {
469+
operation.RequestBody = &OpenAPIRequestBody{
470+
Required: true,
471+
Content: map[string]OpenAPIMedia{
472+
"application/json": {
473+
Schema: &OpenAPISchema{
474+
Type: "object",
475+
},
476+
},
477+
},
478+
}
479+
}
480+
481+
// Map operation to the appropriate method
482+
switch endpoint.Method {
483+
case "GET":
484+
path.Get = operation
485+
case "POST":
486+
path.Post = operation
487+
case "PUT":
488+
path.Put = operation
489+
case "DELETE":
490+
path.Delete = operation
491+
}
492+
493+
spec.Paths[endpoint.Path] = path
494+
}
495+
496+
return spec
497+
}
498+
499+
// handleOpenAPISpec serves the OpenAPI 3.0 specification at /openapi.json
500+
func (s *ClassificationAPIServer) handleOpenAPISpec(w http.ResponseWriter, r *http.Request) {
501+
spec := s.generateOpenAPISpec()
502+
s.writeJSONResponse(w, http.StatusOK, spec)
503+
}
504+
505+
// handleSwaggerUI serves the Swagger UI at /docs
506+
func (s *ClassificationAPIServer) handleSwaggerUI(w http.ResponseWriter, r *http.Request) {
507+
// Serve a simple HTML page that loads Swagger UI from CDN
508+
html := `<!DOCTYPE html>
509+
<html lang="en">
510+
<head>
511+
<meta charset="UTF-8">
512+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
513+
<title>Semantic Router API Documentation</title>
514+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/swagger-ui.css">
515+
<style>
516+
body {
517+
margin: 0;
518+
padding: 0;
519+
}
520+
</style>
521+
</head>
522+
<body>
523+
<div id="swagger-ui"></div>
524+
<script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js"></script>
525+
<script src="https://unpkg.com/[email protected]/swagger-ui-standalone-preset.js"></script>
526+
<script>
527+
window.onload = function() {
528+
window.ui = SwaggerUIBundle({
529+
url: "/openapi.json",
530+
dom_id: '#swagger-ui',
531+
deepLinking: true,
532+
presets: [
533+
SwaggerUIBundle.presets.apis,
534+
SwaggerUIStandalonePreset
535+
],
536+
plugins: [
537+
SwaggerUIBundle.plugins.DownloadUrl
538+
],
539+
layout: "StandaloneLayout"
540+
});
541+
};
542+
</script>
543+
</body>
544+
</html>`
545+
546+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
547+
w.WriteHeader(http.StatusOK)
548+
w.Write([]byte(html))
549+
}
550+
319551
// handleIntentClassification handles intent classification requests
320552
func (s *ClassificationAPIServer) handleIntentClassification(w http.ResponseWriter, r *http.Request) {
321553
var req services.IntentRequest

0 commit comments

Comments
 (0)