@@ -184,6 +184,13 @@ func (s *ClassificationAPIServer) setupRoutes() *http.ServeMux {
184184 // Health check endpoint
185185 mux .HandleFunc ("GET /health" , s .handleHealth )
186186
187+ // API discovery endpoint
188+ mux .HandleFunc ("GET /api/v1" , s .handleAPIOverview )
189+
190+ // OpenAPI and documentation endpoints
191+ mux .HandleFunc ("GET /openapi.json" , s .handleOpenAPISpec )
192+ mux .HandleFunc ("GET /docs" , s .handleSwaggerUI )
193+
187194 // Classification endpoints
188195 mux .HandleFunc ("POST /api/v1/classify/intent" , s .handleIntentClassification )
189196 mux .HandleFunc ("POST /api/v1/classify/pii" , s .handlePIIDetection )
@@ -224,6 +231,323 @@ func (s *ClassificationAPIServer) handleHealth(w http.ResponseWriter, r *http.Re
224231 w .Write ([]byte (`{"status": "healthy", "service": "classification-api"}` ))
225232}
226233
234+ // APIOverviewResponse represents the response for GET /api/v1
235+ type APIOverviewResponse struct {
236+ Service string `json:"service"`
237+ Version string `json:"version"`
238+ Description string `json:"description"`
239+ Endpoints []EndpointInfo `json:"endpoints"`
240+ TaskTypes []TaskTypeInfo `json:"task_types"`
241+ Links map [string ]string `json:"links"`
242+ }
243+
244+ // EndpointInfo represents information about an API endpoint
245+ type EndpointInfo struct {
246+ Path string `json:"path"`
247+ Method string `json:"method"`
248+ Description string `json:"description"`
249+ }
250+
251+ // TaskTypeInfo represents information about a task type
252+ type TaskTypeInfo struct {
253+ Name string `json:"name"`
254+ Description string `json:"description"`
255+ }
256+
257+ // EndpointMetadata stores metadata about an endpoint for API documentation
258+ type EndpointMetadata struct {
259+ Path string
260+ Method string
261+ Description string
262+ }
263+
264+ // endpointRegistry is a centralized registry of all API endpoints with their metadata
265+ var endpointRegistry = []EndpointMetadata {
266+ {Path : "/health" , Method : "GET" , Description : "Health check endpoint" },
267+ {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" },
270+ {Path : "/api/v1/classify/intent" , Method : "POST" , Description : "Classify user queries into routing categories" },
271+ {Path : "/api/v1/classify/pii" , Method : "POST" , Description : "Detect personally identifiable information in text" },
272+ {Path : "/api/v1/classify/security" , Method : "POST" , Description : "Detect jailbreak attempts and security threats" },
273+ {Path : "/api/v1/classify/combined" , Method : "POST" , Description : "Perform combined classification (intent, PII, and security)" },
274+ {Path : "/api/v1/classify/batch" , Method : "POST" , Description : "Batch classification with configurable task_type parameter" },
275+ {Path : "/info/models" , Method : "GET" , Description : "Get information about loaded models" },
276+ {Path : "/info/classifier" , Method : "GET" , Description : "Get classifier information and status" },
277+ {Path : "/v1/models" , Method : "GET" , Description : "OpenAI-compatible model listing" },
278+ {Path : "/metrics/classification" , Method : "GET" , Description : "Get classification metrics and statistics" },
279+ {Path : "/config/classification" , Method : "GET" , Description : "Get classification configuration" },
280+ {Path : "/config/classification" , Method : "PUT" , Description : "Update classification configuration" },
281+ {Path : "/config/system-prompts" , Method : "GET" , Description : "Get system prompt configuration (requires explicit enablement)" },
282+ {Path : "/config/system-prompts" , Method : "PUT" , Description : "Update system prompt configuration (requires explicit enablement)" },
283+ }
284+
285+ // taskTypeRegistry is a centralized registry of all supported task types
286+ var taskTypeRegistry = []TaskTypeInfo {
287+ {Name : "intent" , Description : "Intent/category classification (default for batch endpoint)" },
288+ {Name : "pii" , Description : "Personally Identifiable Information detection" },
289+ {Name : "security" , Description : "Jailbreak and security threat detection" },
290+ {Name : "all" , Description : "All classification types combined" },
291+ }
292+
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+
365+ // handleAPIOverview handles GET /api/v1 for API discovery
366+ func (s * ClassificationAPIServer ) handleAPIOverview (w http.ResponseWriter , r * http.Request ) {
367+ // Build endpoints list from registry, filtering out disabled endpoints
368+ endpoints := make ([]EndpointInfo , 0 , len (endpointRegistry ))
369+ for _ , metadata := range endpointRegistry {
370+ // Filter out system prompt endpoints if they are disabled
371+ if ! s .enableSystemPromptAPI && (metadata .Path == "/config/system-prompts" ) {
372+ continue
373+ }
374+ endpoints = append (endpoints , EndpointInfo {
375+ Path : metadata .Path ,
376+ Method : metadata .Method ,
377+ Description : metadata .Description ,
378+ })
379+ }
380+
381+ response := APIOverviewResponse {
382+ Service : "Semantic Router Classification API" ,
383+ Version : "v1" ,
384+ Description : "API for intent classification, PII detection, and security analysis" ,
385+ Endpoints : endpoints ,
386+ TaskTypes : taskTypeRegistry ,
387+ Links : map [string ]string {
388+ "documentation" : "https://vllm-project.github.io/semantic-router/" ,
389+ "openapi_spec" : "/openapi.json" ,
390+ "swagger_ui" : "/docs" ,
391+ "models_info" : "/info/models" ,
392+ "health" : "/health" ,
393+ },
394+ }
395+
396+ s .writeJSONResponse (w , http .StatusOK , response )
397+ }
398+
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+
227551// handleIntentClassification handles intent classification requests
228552func (s * ClassificationAPIServer ) handleIntentClassification (w http.ResponseWriter , r * http.Request ) {
229553 var req services.IntentRequest
@@ -335,6 +659,13 @@ func (s *ClassificationAPIServer) handleBatchClassification(w http.ResponseWrite
335659 return
336660 }
337661
662+ // Validate task_type if provided
663+ if err := validateTaskType (req .TaskType ); err != nil {
664+ metrics .RecordBatchClassificationError ("unified" , "invalid_task_type" )
665+ s .writeErrorResponse (w , http .StatusBadRequest , "INVALID_TASK_TYPE" , err .Error ())
666+ return
667+ }
668+
338669 // Record the number of texts being processed
339670 metrics .RecordBatchClassificationTexts ("unified" , len (req .Texts ))
340671
@@ -622,6 +953,24 @@ func (s *ClassificationAPIServer) getSystemInfo() SystemInfo {
622953 }
623954}
624955
956+ // validateTaskType validates the task_type parameter for batch classification
957+ // Returns an error if the task_type is invalid, nil if valid or empty
958+ func validateTaskType (taskType string ) error {
959+ // Empty task_type defaults to "intent", so it's valid
960+ if taskType == "" {
961+ return nil
962+ }
963+
964+ validTaskTypes := []string {"intent" , "pii" , "security" , "all" }
965+ for _ , valid := range validTaskTypes {
966+ if taskType == valid {
967+ return nil
968+ }
969+ }
970+
971+ return fmt .Errorf ("invalid task_type '%s'. Supported values: %v" , taskType , validTaskTypes )
972+ }
973+
625974// extractRequestedResults converts unified results to batch format based on task type
626975func (s * ClassificationAPIServer ) extractRequestedResults (unifiedResults * services.UnifiedBatchResponse , taskType string , options * ClassificationOptions ) []BatchClassificationResult {
627976 // Determine the correct batch size based on task type
0 commit comments