diff --git a/cmd/server/main.go b/cmd/server/main.go index 34a2a4b..7b11c2e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + _ "github.com/ez-api/ez-api/docs" "github.com/ez-api/ez-api/internal/api" "github.com/ez-api/ez-api/internal/config" "github.com/ez-api/ez-api/internal/middleware" @@ -16,10 +17,35 @@ import ( "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" "gorm.io/driver/postgres" "gorm.io/gorm" ) +// @title EZ-API Control Plane +// @version 0.0.1 +// @description Management API for EZ-API Gateway system. +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url http://www.swagger.io/support +// @contact.email support@swagger.io + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8080 +// @BasePath / + +// @securityDefinitions.apikey AdminAuth +// @in header +// @name Authorization + +// @securityDefinitions.apikey MasterAuth +// @in header +// @name Authorization + func main() { // 1. Load Configuration cfg, err := config.Load() @@ -113,6 +139,9 @@ func main() { c.JSON(httpStatus, status) }) + // Swagger Documentation + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + // API Routes // Admin Routes adminGroup := r.Group("/admin") diff --git a/docker-compose.yml b/docker-compose.yml index cc99432..b62d6b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,7 @@ services: EZ_REDIS_ADDR: ${EZ_REDIS_ADDR:-redis:6379} EZ_REDIS_PASSWORD: ${EZ_REDIS_PASSWORD} EZ_REDIS_DB: ${EZ_REDIS_DB:-0} + EZ_ADMIN_TOKEN: ${EZ_ADMIN_TOKEN:-admin123} depends_on: postgres: condition: service_healthy diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..9938e67 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,687 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/admin/masters": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Create a new master account (tenant)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create a new master tenant", + "parameters": [ + { + "description": "Master Info", + "name": "master", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_api.CreateMasterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/models": { + "get": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Get a list of all registered models", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all models", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Register a supported model with its capabilities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Register a new model", + "parameters": [ + { + "description": "Model Info", + "name": "model", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/models/{id}": { + "put": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Update an existing model's configuration", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update a model", + "parameters": [ + { + "type": "integer", + "description": "Model ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Model Info", + "name": "model", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/providers": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Register a new upstream AI provider", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create a new provider", + "parameters": [ + { + "description": "Provider Info", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ProviderDTO" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Provider" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/sync/snapshot": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Force full synchronization of DB state to Redis", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Force sync snapshot", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/logs": { + "post": { + "description": "Internal endpoint for ingesting logs from Balancer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Ingest logs", + "parameters": [ + { + "description": "Log Record", + "name": "log", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.LogRecord" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/v1/tokens": { + "post": { + "security": [ + { + "MasterAuth": [] + } + ], + "description": "Issue a new access token (child key) for the authenticated master", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "master" + ], + "summary": "Issue a child key", + "parameters": [ + { + "description": "Key Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_api.IssueChildKeyRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "github_com_ez-api_ez-api_internal_dto.ModelDTO": { + "type": "object", + "properties": { + "context_window": { + "type": "integer" + }, + "cost_per_token": { + "type": "number" + }, + "max_output_tokens": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "supports_fim": { + "type": "boolean" + }, + "supports_functions": { + "type": "boolean" + }, + "supports_tool_choice": { + "type": "boolean" + }, + "supports_vision": { + "type": "boolean" + } + } + }, + "github_com_ez-api_ez-api_internal_dto.ProviderDTO": { + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "base_url": { + "type": "string" + }, + "group": { + "type": "string" + }, + "models": { + "description": "List of supported model names", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.LogRecord": { + "type": "object", + "properties": { + "audit_reason": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "error_message": { + "type": "string" + }, + "group": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key_id": { + "type": "integer" + }, + "latency_ms": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "request_body": { + "description": "optional, only when audit triggered", + "type": "string" + }, + "request_size": { + "type": "integer" + }, + "response_body": { + "description": "optional, only when audit triggered", + "type": "string" + }, + "response_size": { + "type": "integer" + }, + "status_code": { + "type": "integer" + }, + "tokens_in": { + "type": "integer" + }, + "tokens_out": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.Model": { + "type": "object", + "properties": { + "context_window": { + "type": "integer" + }, + "cost_per_token": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "max_output_tokens": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "supports_fim": { + "type": "boolean" + }, + "supports_functions": { + "type": "boolean" + }, + "supports_tool_choice": { + "type": "boolean" + }, + "supports_vision": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.Provider": { + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "base_url": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "group": { + "description": "routing group/tier", + "type": "string" + }, + "id": { + "type": "integer" + }, + "models": { + "description": "comma-separated list of supported models (e.g. \"gpt-4,gpt-3.5-turbo\")", + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "description": "openai, anthropic, etc.", + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "internal_api.CreateMasterRequest": { + "type": "object", + "required": [ + "group", + "name" + ], + "properties": { + "global_qps": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "max_child_keys": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "internal_api.IssueChildKeyRequest": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "scopes": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "AdminAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "MasterAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.0.1", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{}, + Title: "EZ-API Control Plane", + Description: "Management API for EZ-API Gateway system.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..d20f8c1 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,663 @@ +{ + "swagger": "2.0", + "info": { + "description": "Management API for EZ-API Gateway system.", + "title": "EZ-API Control Plane", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "0.0.1" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/admin/masters": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Create a new master account (tenant)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create a new master tenant", + "parameters": [ + { + "description": "Master Info", + "name": "master", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_api.CreateMasterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/models": { + "get": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Get a list of all registered models", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all models", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + }, + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Register a supported model with its capabilities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Register a new model", + "parameters": [ + { + "description": "Model Info", + "name": "model", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/models/{id}": { + "put": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Update an existing model's configuration", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Update a model", + "parameters": [ + { + "type": "integer", + "description": "Model ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Model Info", + "name": "model", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Model" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/providers": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Register a new upstream AI provider", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create a new provider", + "parameters": [ + { + "description": "Provider Info", + "name": "provider", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_dto.ProviderDTO" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.Provider" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/admin/sync/snapshot": { + "post": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "Force full synchronization of DB state to Redis", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Force sync snapshot", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/logs": { + "post": { + "description": "Internal endpoint for ingesting logs from Balancer", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Ingest logs", + "parameters": [ + { + "description": "Log Record", + "name": "log", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ez-api_ez-api_internal_model.LogRecord" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + }, + "/v1/tokens": { + "post": { + "security": [ + { + "MasterAuth": [] + } + ], + "description": "Issue a new access token (child key) for the authenticated master", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "master" + ], + "summary": "Issue a child key", + "parameters": [ + { + "description": "Key Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_api.IssueChildKeyRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/gin.H" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/gin.H" + } + } + } + } + } + }, + "definitions": { + "gin.H": { + "type": "object", + "additionalProperties": {} + }, + "github_com_ez-api_ez-api_internal_dto.ModelDTO": { + "type": "object", + "properties": { + "context_window": { + "type": "integer" + }, + "cost_per_token": { + "type": "number" + }, + "max_output_tokens": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "supports_fim": { + "type": "boolean" + }, + "supports_functions": { + "type": "boolean" + }, + "supports_tool_choice": { + "type": "boolean" + }, + "supports_vision": { + "type": "boolean" + } + } + }, + "github_com_ez-api_ez-api_internal_dto.ProviderDTO": { + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "base_url": { + "type": "string" + }, + "group": { + "type": "string" + }, + "models": { + "description": "List of supported model names", + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.LogRecord": { + "type": "object", + "properties": { + "audit_reason": { + "type": "string" + }, + "client_ip": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "error_message": { + "type": "string" + }, + "group": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "key_id": { + "type": "integer" + }, + "latency_ms": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "request_body": { + "description": "optional, only when audit triggered", + "type": "string" + }, + "request_size": { + "type": "integer" + }, + "response_body": { + "description": "optional, only when audit triggered", + "type": "string" + }, + "response_size": { + "type": "integer" + }, + "status_code": { + "type": "integer" + }, + "tokens_in": { + "type": "integer" + }, + "tokens_out": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.Model": { + "type": "object", + "properties": { + "context_window": { + "type": "integer" + }, + "cost_per_token": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "id": { + "type": "integer" + }, + "max_output_tokens": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "supports_fim": { + "type": "boolean" + }, + "supports_functions": { + "type": "boolean" + }, + "supports_tool_choice": { + "type": "boolean" + }, + "supports_vision": { + "type": "boolean" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_ez-api_ez-api_internal_model.Provider": { + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "base_url": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "group": { + "description": "routing group/tier", + "type": "string" + }, + "id": { + "type": "integer" + }, + "models": { + "description": "comma-separated list of supported models (e.g. \"gpt-4,gpt-3.5-turbo\")", + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "description": "openai, anthropic, etc.", + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "internal_api.CreateMasterRequest": { + "type": "object", + "required": [ + "group", + "name" + ], + "properties": { + "global_qps": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "max_child_keys": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "internal_api.IssueChildKeyRequest": { + "type": "object", + "properties": { + "group": { + "type": "string" + }, + "scopes": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "AdminAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "MasterAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..30577a4 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,431 @@ +basePath: / +definitions: + gin.H: + additionalProperties: {} + type: object + github_com_ez-api_ez-api_internal_dto.ModelDTO: + properties: + context_window: + type: integer + cost_per_token: + type: number + max_output_tokens: + type: integer + name: + type: string + supports_fim: + type: boolean + supports_functions: + type: boolean + supports_tool_choice: + type: boolean + supports_vision: + type: boolean + type: object + github_com_ez-api_ez-api_internal_dto.ProviderDTO: + properties: + api_key: + type: string + base_url: + type: string + group: + type: string + models: + description: List of supported model names + items: + type: string + type: array + name: + type: string + type: + type: string + type: object + github_com_ez-api_ez-api_internal_model.LogRecord: + properties: + audit_reason: + type: string + client_ip: + type: string + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + error_message: + type: string + group: + type: string + id: + type: integer + key_id: + type: integer + latency_ms: + type: integer + model: + type: string + request_body: + description: optional, only when audit triggered + type: string + request_size: + type: integer + response_body: + description: optional, only when audit triggered + type: string + response_size: + type: integer + status_code: + type: integer + tokens_in: + type: integer + tokens_out: + type: integer + updatedAt: + type: string + type: object + github_com_ez-api_ez-api_internal_model.Model: + properties: + context_window: + type: integer + cost_per_token: + type: number + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + id: + type: integer + max_output_tokens: + type: integer + name: + type: string + supports_fim: + type: boolean + supports_functions: + type: boolean + supports_tool_choice: + type: boolean + supports_vision: + type: boolean + updatedAt: + type: string + type: object + github_com_ez-api_ez-api_internal_model.Provider: + properties: + api_key: + type: string + base_url: + type: string + createdAt: + type: string + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + group: + description: routing group/tier + type: string + id: + type: integer + models: + description: comma-separated list of supported models (e.g. "gpt-4,gpt-3.5-turbo") + type: string + name: + type: string + type: + description: openai, anthropic, etc. + type: string + updatedAt: + type: string + type: object + gorm.DeletedAt: + properties: + time: + type: string + valid: + description: Valid is true if Time is not NULL + type: boolean + type: object + internal_api.CreateMasterRequest: + properties: + global_qps: + type: integer + group: + type: string + max_child_keys: + type: integer + name: + type: string + required: + - group + - name + type: object + internal_api.IssueChildKeyRequest: + properties: + group: + type: string + scopes: + type: string + type: object +host: localhost:8080 +info: + contact: + email: support@swagger.io + name: API Support + url: http://www.swagger.io/support + description: Management API for EZ-API Gateway system. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: EZ-API Control Plane + version: 0.0.1 +paths: + /admin/masters: + post: + consumes: + - application/json + description: Create a new master account (tenant) + parameters: + - description: Master Info + in: body + name: master + required: true + schema: + $ref: '#/definitions/internal_api.CreateMasterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/gin.H' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: Create a new master tenant + tags: + - admin + /admin/models: + get: + description: Get a list of all registered models + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_model.Model' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: List all models + tags: + - admin + post: + consumes: + - application/json + description: Register a supported model with its capabilities + parameters: + - description: Model Info + in: body + name: model + required: true + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_model.Model' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: Register a new model + tags: + - admin + /admin/models/{id}: + put: + consumes: + - application/json + description: Update an existing model's configuration + parameters: + - description: Model ID + in: path + name: id + required: true + type: integer + - description: Model Info + in: body + name: model + required: true + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_dto.ModelDTO' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_model.Model' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "404": + description: Not Found + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: Update a model + tags: + - admin + /admin/providers: + post: + consumes: + - application/json + description: Register a new upstream AI provider + parameters: + - description: Provider Info + in: body + name: provider + required: true + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_dto.ProviderDTO' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_model.Provider' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: Create a new provider + tags: + - admin + /admin/sync/snapshot: + post: + description: Force full synchronization of DB state to Redis + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - AdminAuth: [] + summary: Force sync snapshot + tags: + - admin + /logs: + post: + consumes: + - application/json + description: Internal endpoint for ingesting logs from Balancer + parameters: + - description: Log Record + in: body + name: log + required: true + schema: + $ref: '#/definitions/github_com_ez-api_ez-api_internal_model.LogRecord' + produces: + - application/json + responses: + "202": + description: Accepted + schema: + $ref: '#/definitions/gin.H' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + summary: Ingest logs + tags: + - system + /v1/tokens: + post: + consumes: + - application/json + description: Issue a new access token (child key) for the authenticated master + parameters: + - description: Key Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_api.IssueChildKeyRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/gin.H' + "400": + description: Bad Request + schema: + $ref: '#/definitions/gin.H' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/gin.H' + "403": + description: Forbidden + schema: + $ref: '#/definitions/gin.H' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/gin.H' + security: + - MasterAuth: [] + summary: Issue a child key + tags: + - master +securityDefinitions: + AdminAuth: + in: header + name: Authorization + type: apiKey + MasterAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod index d42bc65..2fda778 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,16 @@ go 1.24.5 require ( github.com/gin-gonic/gin v1.11.0 github.com/redis/go-redis/v9 v9.17.2 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + golang.org/x/crypto v0.44.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -17,12 +22,21 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.22.3 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/spec v0.22.1 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -31,9 +45,10 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect @@ -41,13 +56,13 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index f6511a8..c21b213 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -10,6 +12,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,10 +20,39 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= +github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= +github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -33,8 +65,6 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -54,12 +84,17 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -72,6 +107,8 @@ github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQB github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -82,32 +119,69 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/admin_handler.go b/internal/api/admin_handler.go index 049e371..454550e 100644 --- a/internal/api/admin_handler.go +++ b/internal/api/admin_handler.go @@ -23,6 +23,18 @@ type CreateMasterRequest struct { GlobalQPS int `json:"global_qps"` } +// CreateMaster godoc +// @Summary Create a new master tenant +// @Description Create a new master account (tenant) +// @Tags admin +// @Accept json +// @Produce json +// @Security AdminAuth +// @Param master body CreateMasterRequest true "Master Info" +// @Success 201 {object} gin.H +// @Failure 400 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /admin/masters [post] func (h *AdminHandler) CreateMaster(c *gin.Context) { var req CreateMasterRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/api/handler.go b/internal/api/handler.go index d924509..54bbfb9 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -24,6 +24,18 @@ func NewHandler(db *gorm.DB, sync *service.SyncService, logger *service.LogWrite // CreateKey is now handled by MasterHandler +// CreateProvider godoc +// @Summary Create a new provider +// @Description Register a new upstream AI provider +// @Tags admin +// @Accept json +// @Produce json +// @Security AdminAuth +// @Param provider body dto.ProviderDTO true "Provider Info" +// @Success 201 {object} model.Provider +// @Failure 400 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /admin/providers [post] func (h *Handler) CreateProvider(c *gin.Context) { var req dto.ProviderDTO if err := c.ShouldBindJSON(&req); err != nil { @@ -58,6 +70,18 @@ func (h *Handler) CreateProvider(c *gin.Context) { c.JSON(http.StatusCreated, provider) } +// CreateModel godoc +// @Summary Register a new model +// @Description Register a supported model with its capabilities +// @Tags admin +// @Accept json +// @Produce json +// @Security AdminAuth +// @Param model body dto.ModelDTO true "Model Info" +// @Success 201 {object} model.Model +// @Failure 400 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /admin/models [post] func (h *Handler) CreateModel(c *gin.Context) { var req dto.ModelDTO if err := c.ShouldBindJSON(&req); err != nil { @@ -89,6 +113,15 @@ func (h *Handler) CreateModel(c *gin.Context) { c.JSON(http.StatusCreated, modelReq) } +// ListModels godoc +// @Summary List all models +// @Description Get a list of all registered models +// @Tags admin +// @Produce json +// @Security AdminAuth +// @Success 200 {array} model.Model +// @Failure 500 {object} gin.H +// @Router /admin/models [get] func (h *Handler) ListModels(c *gin.Context) { var models []model.Model if err := h.db.Find(&models).Error; err != nil { @@ -98,6 +131,20 @@ func (h *Handler) ListModels(c *gin.Context) { c.JSON(http.StatusOK, models) } +// UpdateModel godoc +// @Summary Update a model +// @Description Update an existing model's configuration +// @Tags admin +// @Accept json +// @Produce json +// @Security AdminAuth +// @Param id path int true "Model ID" +// @Param model body dto.ModelDTO true "Model Info" +// @Success 200 {object} model.Model +// @Failure 400 {object} gin.H +// @Failure 404 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /admin/models/{id} [put] func (h *Handler) UpdateModel(c *gin.Context) { idParam := c.Param("id") id, err := strconv.Atoi(idParam) @@ -140,6 +187,15 @@ func (h *Handler) UpdateModel(c *gin.Context) { c.JSON(http.StatusOK, existing) } +// SyncSnapshot godoc +// @Summary Force sync snapshot +// @Description Force full synchronization of DB state to Redis +// @Tags admin +// @Produce json +// @Security AdminAuth +// @Success 200 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /admin/sync/snapshot [post] func (h *Handler) SyncSnapshot(c *gin.Context) { if err := h.sync.SyncAll(h.db); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync snapshots", "details": err.Error()}) @@ -149,6 +205,15 @@ func (h *Handler) SyncSnapshot(c *gin.Context) { } // IngestLog accepts log records from data plane or other services. +// @Summary Ingest logs +// @Description Internal endpoint for ingesting logs from Balancer +// @Tags system +// @Accept json +// @Produce json +// @Param log body model.LogRecord true "Log Record" +// @Success 202 {object} gin.H +// @Failure 400 {object} gin.H +// @Router /logs [post] func (h *Handler) IngestLog(c *gin.Context) { var rec model.LogRecord if err := c.ShouldBindJSON(&rec); err != nil { diff --git a/internal/api/master_handler.go b/internal/api/master_handler.go index 6ed8e59..6f7103c 100644 --- a/internal/api/master_handler.go +++ b/internal/api/master_handler.go @@ -23,6 +23,20 @@ type IssueChildKeyRequest struct { Scopes string `json:"scopes"` } +// IssueChildKey godoc +// @Summary Issue a child key +// @Description Issue a new access token (child key) for the authenticated master +// @Tags master +// @Accept json +// @Produce json +// @Security MasterAuth +// @Param request body IssueChildKeyRequest true "Key Request" +// @Success 201 {object} gin.H +// @Failure 400 {object} gin.H +// @Failure 401 {object} gin.H +// @Failure 403 {object} gin.H +// @Failure 500 {object} gin.H +// @Router /v1/tokens [post] func (h *MasterHandler) IssueChildKey(c *gin.Context) { master, exists := c.Get("master") if !exists {