@@ -184,6 +184,13 @@ func (s *ClassificationAPIServer) setupRoutes() *http.ServeMux {
184
184
// Health check endpoint
185
185
mux .HandleFunc ("GET /health" , s .handleHealth )
186
186
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
+
187
194
// Classification endpoints
188
195
mux .HandleFunc ("POST /api/v1/classify/intent" , s .handleIntentClassification )
189
196
mux .HandleFunc ("POST /api/v1/classify/pii" , s .handlePIIDetection )
@@ -224,6 +231,323 @@ func (s *ClassificationAPIServer) handleHealth(w http.ResponseWriter, r *http.Re
224
231
w .Write ([]byte (`{"status": "healthy", "service": "classification-api"}` ))
225
232
}
226
233
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
+
227
551
// handleIntentClassification handles intent classification requests
228
552
func (s * ClassificationAPIServer ) handleIntentClassification (w http.ResponseWriter , r * http.Request ) {
229
553
var req services.IntentRequest
@@ -335,6 +659,13 @@ func (s *ClassificationAPIServer) handleBatchClassification(w http.ResponseWrite
335
659
return
336
660
}
337
661
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
+
338
669
// Record the number of texts being processed
339
670
metrics .RecordBatchClassificationTexts ("unified" , len (req .Texts ))
340
671
@@ -622,6 +953,24 @@ func (s *ClassificationAPIServer) getSystemInfo() SystemInfo {
622
953
}
623
954
}
624
955
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
+
625
974
// extractRequestedResults converts unified results to batch format based on task type
626
975
func (s * ClassificationAPIServer ) extractRequestedResults (unifiedResults * services.UnifiedBatchResponse , taskType string , options * ClassificationOptions ) []BatchClassificationResult {
627
976
// Determine the correct batch size based on task type
0 commit comments