diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70562669..bf424591 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: "1.24" + GO_VERSION: "1.25" # Restrict token permissions to minimum required permissions: diff --git a/CLAUDE.md b/CLAUDE.md index 448b53d4..ef3650c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Guidance for AI models (like Claude) working with this codebase. **GOModel** is a high-performance AI gateway in Go that routes requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI, Ollama). Drop-in LiteLLM replacement. -- **Module:** `gomodel` | **Go:** 1.24.0 | **Repo:** https://github.com/ENTERPILOT/GOModel +- **Module:** `gomodel` | **Go:** 1.25.0 | **Repo:** https://github.com/ENTERPILOT/GOModel - **Stage:** Development—backward compatibility is not a concern ## Commands @@ -24,6 +24,7 @@ make lint-fix # Auto-fix lint issues make tidy # go mod tidy make clean # Remove bin/ make record-api # Record API responses for contract tests +make swagger # Regenerate Swagger docs ``` **Single test:** `go test ./internal/providers -v -run TestName` diff --git a/Dockerfile b/Dockerfile index 37ec166f..bb1629e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage — run on the build host's native arch for speed, cross-compile for target -FROM --platform=$BUILDPLATFORM golang:1.24-alpine3.23 AS builder +FROM --platform=$BUILDPLATFORM golang:1.25-alpine3.23 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index 691ef49b..e8776a6d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build run clean tidy test test-e2e test-integration test-contract test-all lint lint-fix record-api +.PHONY: build run clean tidy test test-e2e test-integration test-contract test-all lint lint-fix record-api swagger # Get version info VERSION ?= $(shell git describe --tags --always --dirty) @@ -54,6 +54,12 @@ record-api: -output=tests/contract/testdata/openai/models.json @echo "Done! Golden files saved to tests/contract/testdata/" +swagger: + go run github.com/swaggo/swag/cmd/swag init --generalInfo main.go \ + --dir cmd/gomodel,internal \ + --output cmd/gomodel/docs \ + --outputTypes go,json + # Run linter lint: golangci-lint run ./... diff --git a/cmd/gomodel/docs/docs.go b/cmd/gomodel/docs/docs.go new file mode 100644 index 00000000..c6086f12 --- /dev/null +++ b/cmd/gomodel/docs/docs.go @@ -0,0 +1,766 @@ +// 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}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/admin/api/v1/models": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all registered models with provider info", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/providers.ModelWithProvider" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/admin/api/v1/usage/daily": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get usage breakdown by period", + "parameters": [ + { + "type": "integer", + "description": "Number of days (default 30)", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Grouping interval: daily, weekly, monthly, yearly (default daily)", + "name": "interval", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/usage.DailyUsage" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/admin/api/v1/usage/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get usage summary", + "parameters": [ + { + "type": "integer", + "description": "Number of days (default 30)", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/usage.UsageSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/v1/chat/completions": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "json text/event-stream" + ], + "tags": [ + "chat" + ], + "summary": "Create a chat completion", + "parameters": [ + { + "description": "Chat completion request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/core.ChatRequest" + } + } + ], + "responses": { + "200": { + "description": "JSON response or SSE stream when stream=true", + "schema": { + "$ref": "#/definitions/core.ChatResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/v1/models": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "models" + ], + "summary": "List available models", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/core.ModelsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/v1/responses": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "responses" + ], + "summary": "Create a model response (Responses API)", + "parameters": [ + { + "description": "Responses API request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/core.ResponsesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/core.ResponsesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + } + }, + "definitions": { + "core.ChatRequest": { + "type": "object", + "properties": { + "max_tokens": { + "type": "integer" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Message" + } + }, + "model": { + "type": "string" + }, + "reasoning": { + "$ref": "#/definitions/core.Reasoning" + }, + "stream": { + "type": "boolean" + }, + "stream_options": { + "$ref": "#/definitions/core.StreamOptions" + }, + "temperature": { + "type": "number" + } + } + }, + "core.ChatResponse": { + "type": "object", + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Choice" + } + }, + "created": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "usage": { + "$ref": "#/definitions/core.Usage" + } + } + }, + "core.Choice": { + "type": "object", + "properties": { + "finish_reason": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "message": { + "$ref": "#/definitions/core.Message" + } + } + }, + "core.ErrorType": { + "type": "string", + "enum": [ + "provider_error", + "rate_limit_error", + "invalid_request_error", + "authentication_error", + "not_found_error" + ], + "x-enum-varnames": [ + "ErrorTypeProvider", + "ErrorTypeRateLimit", + "ErrorTypeInvalidRequest", + "ErrorTypeAuthentication", + "ErrorTypeNotFound" + ] + }, + "core.GatewayError": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/core.ErrorType" + } + } + }, + "core.Message": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "core.Model": { + "type": "object", + "properties": { + "created": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + }, + "owned_by": { + "type": "string" + } + } + }, + "core.ModelsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Model" + } + }, + "object": { + "type": "string" + } + } + }, + "core.Reasoning": { + "type": "object", + "properties": { + "effort": { + "description": "Effort controls how much reasoning effort the model should use.\nValid values are \"low\", \"medium\", and \"high\".", + "type": "string" + } + } + }, + "core.ResponsesContentItem": { + "type": "object", + "properties": { + "annotations": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + }, + "type": { + "description": "\"output_text\", etc.", + "type": "string" + } + } + }, + "core.ResponsesError": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "core.ResponsesOutputItem": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/core.ResponsesContentItem" + } + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "description": "\"message\", \"function_call\", etc.", + "type": "string" + } + } + }, + "core.ResponsesRequest": { + "type": "object", + "properties": { + "input": { + "description": "string or []ResponsesInputItem — see docs for array form", + "type": "string", + "example": "Tell me a joke" + }, + "instructions": { + "type": "string" + }, + "max_output_tokens": { + "type": "integer" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "model": { + "type": "string" + }, + "reasoning": { + "$ref": "#/definitions/core.Reasoning" + }, + "stream": { + "type": "boolean" + }, + "stream_options": { + "$ref": "#/definitions/core.StreamOptions" + }, + "temperature": { + "type": "number" + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + } + } + }, + "core.ResponsesResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "error": { + "$ref": "#/definitions/core.ResponsesError" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "description": "\"response\"", + "type": "string" + }, + "output": { + "type": "array", + "items": { + "$ref": "#/definitions/core.ResponsesOutputItem" + } + }, + "provider": { + "type": "string" + }, + "status": { + "description": "\"completed\", \"failed\", \"in_progress\"", + "type": "string" + }, + "usage": { + "$ref": "#/definitions/core.ResponsesUsage" + } + } + }, + "core.ResponsesUsage": { + "type": "object", + "properties": { + "input_tokens": { + "type": "integer" + }, + "output_tokens": { + "type": "integer" + }, + "raw_usage": { + "type": "object", + "additionalProperties": {} + }, + "total_tokens": { + "type": "integer" + } + } + }, + "core.StreamOptions": { + "type": "object", + "properties": { + "include_usage": { + "description": "IncludeUsage requests token usage information in streaming responses.\nWhen true, the final streaming chunk will include usage statistics.", + "type": "boolean" + } + } + }, + "core.Usage": { + "type": "object", + "properties": { + "completion_tokens": { + "type": "integer" + }, + "prompt_tokens": { + "type": "integer" + }, + "raw_usage": { + "type": "object", + "additionalProperties": {} + }, + "total_tokens": { + "type": "integer" + } + } + }, + "providers.ModelWithProvider": { + "type": "object", + "properties": { + "model": { + "$ref": "#/definitions/core.Model" + }, + "provider_type": { + "type": "string" + } + } + }, + "usage.DailyUsage": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "input_tokens": { + "type": "integer" + }, + "output_tokens": { + "type": "integer" + }, + "requests": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + }, + "usage.UsageSummary": { + "type": "object", + "properties": { + "total_input_tokens": { + "type": "integer" + }, + "total_output_tokens": { + "type": "integer" + }, + "total_requests": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/", + Schemes: []string{"http"}, + Title: "GOModel API", + Description: "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI, Ollama). Drop-in OpenAI-compatible API.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/cmd/gomodel/docs/swagger.json b/cmd/gomodel/docs/swagger.json new file mode 100644 index 00000000..42b16c14 --- /dev/null +++ b/cmd/gomodel/docs/swagger.json @@ -0,0 +1,744 @@ +{ + "schemes": [ + "http" + ], + "swagger": "2.0", + "info": { + "description": "High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI, Ollama). Drop-in OpenAI-compatible API.", + "title": "GOModel API", + "contact": {}, + "version": "1.0" + }, + "basePath": "/", + "paths": { + "/admin/api/v1/models": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List all registered models with provider info", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/providers.ModelWithProvider" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/admin/api/v1/usage/daily": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get usage breakdown by period", + "parameters": [ + { + "type": "integer", + "description": "Number of days (default 30)", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + }, + { + "type": "string", + "description": "Grouping interval: daily, weekly, monthly, yearly (default daily)", + "name": "interval", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/usage.DailyUsage" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/admin/api/v1/usage/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get usage summary", + "parameters": [ + { + "type": "integer", + "description": "Number of days (default 30)", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Start date (YYYY-MM-DD)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (YYYY-MM-DD)", + "name": "end_date", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/usage.UsageSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "system" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/v1/chat/completions": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "json text/event-stream" + ], + "tags": [ + "chat" + ], + "summary": "Create a chat completion", + "parameters": [ + { + "description": "Chat completion request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/core.ChatRequest" + } + } + ], + "responses": { + "200": { + "description": "JSON response or SSE stream when stream=true", + "schema": { + "$ref": "#/definitions/core.ChatResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/v1/models": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "models" + ], + "summary": "List available models", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/core.ModelsResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + }, + "/v1/responses": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "responses" + ], + "summary": "Create a model response (Responses API)", + "parameters": [ + { + "description": "Responses API request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/core.ResponsesRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/core.ResponsesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/core.GatewayError" + } + } + } + } + } + }, + "definitions": { + "core.ChatRequest": { + "type": "object", + "properties": { + "max_tokens": { + "type": "integer" + }, + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Message" + } + }, + "model": { + "type": "string" + }, + "reasoning": { + "$ref": "#/definitions/core.Reasoning" + }, + "stream": { + "type": "boolean" + }, + "stream_options": { + "$ref": "#/definitions/core.StreamOptions" + }, + "temperature": { + "type": "number" + } + } + }, + "core.ChatResponse": { + "type": "object", + "properties": { + "choices": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Choice" + } + }, + "created": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "usage": { + "$ref": "#/definitions/core.Usage" + } + } + }, + "core.Choice": { + "type": "object", + "properties": { + "finish_reason": { + "type": "string" + }, + "index": { + "type": "integer" + }, + "message": { + "$ref": "#/definitions/core.Message" + } + } + }, + "core.ErrorType": { + "type": "string", + "enum": [ + "provider_error", + "rate_limit_error", + "invalid_request_error", + "authentication_error", + "not_found_error" + ], + "x-enum-varnames": [ + "ErrorTypeProvider", + "ErrorTypeRateLimit", + "ErrorTypeInvalidRequest", + "ErrorTypeAuthentication", + "ErrorTypeNotFound" + ] + }, + "core.GatewayError": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "type": { + "$ref": "#/definitions/core.ErrorType" + } + } + }, + "core.Message": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "core.Model": { + "type": "object", + "properties": { + "created": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "object": { + "type": "string" + }, + "owned_by": { + "type": "string" + } + } + }, + "core.ModelsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/core.Model" + } + }, + "object": { + "type": "string" + } + } + }, + "core.Reasoning": { + "type": "object", + "properties": { + "effort": { + "description": "Effort controls how much reasoning effort the model should use.\nValid values are \"low\", \"medium\", and \"high\".", + "type": "string" + } + } + }, + "core.ResponsesContentItem": { + "type": "object", + "properties": { + "annotations": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + }, + "type": { + "description": "\"output_text\", etc.", + "type": "string" + } + } + }, + "core.ResponsesError": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "core.ResponsesOutputItem": { + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "$ref": "#/definitions/core.ResponsesContentItem" + } + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "description": "\"message\", \"function_call\", etc.", + "type": "string" + } + } + }, + "core.ResponsesRequest": { + "type": "object", + "properties": { + "input": { + "description": "string or []ResponsesInputItem — see docs for array form", + "type": "string", + "example": "Tell me a joke" + }, + "instructions": { + "type": "string" + }, + "max_output_tokens": { + "type": "integer" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "model": { + "type": "string" + }, + "reasoning": { + "$ref": "#/definitions/core.Reasoning" + }, + "stream": { + "type": "boolean" + }, + "stream_options": { + "$ref": "#/definitions/core.StreamOptions" + }, + "temperature": { + "type": "number" + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } + } + } + }, + "core.ResponsesResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "integer" + }, + "error": { + "$ref": "#/definitions/core.ResponsesError" + }, + "id": { + "type": "string" + }, + "model": { + "type": "string" + }, + "object": { + "description": "\"response\"", + "type": "string" + }, + "output": { + "type": "array", + "items": { + "$ref": "#/definitions/core.ResponsesOutputItem" + } + }, + "provider": { + "type": "string" + }, + "status": { + "description": "\"completed\", \"failed\", \"in_progress\"", + "type": "string" + }, + "usage": { + "$ref": "#/definitions/core.ResponsesUsage" + } + } + }, + "core.ResponsesUsage": { + "type": "object", + "properties": { + "input_tokens": { + "type": "integer" + }, + "output_tokens": { + "type": "integer" + }, + "raw_usage": { + "type": "object", + "additionalProperties": {} + }, + "total_tokens": { + "type": "integer" + } + } + }, + "core.StreamOptions": { + "type": "object", + "properties": { + "include_usage": { + "description": "IncludeUsage requests token usage information in streaming responses.\nWhen true, the final streaming chunk will include usage statistics.", + "type": "boolean" + } + } + }, + "core.Usage": { + "type": "object", + "properties": { + "completion_tokens": { + "type": "integer" + }, + "prompt_tokens": { + "type": "integer" + }, + "raw_usage": { + "type": "object", + "additionalProperties": {} + }, + "total_tokens": { + "type": "integer" + } + } + }, + "providers.ModelWithProvider": { + "type": "object", + "properties": { + "model": { + "$ref": "#/definitions/core.Model" + }, + "provider_type": { + "type": "string" + } + } + }, + "usage.DailyUsage": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "input_tokens": { + "type": "integer" + }, + "output_tokens": { + "type": "integer" + }, + "requests": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + }, + "usage.UsageSummary": { + "type": "object", + "properties": { + "total_input_tokens": { + "type": "integer" + }, + "total_output_tokens": { + "type": "integer" + }, + "total_requests": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/cmd/gomodel/main.go b/cmd/gomodel/main.go index 2e75ec59..cbbf536d 100644 --- a/cmd/gomodel/main.go +++ b/cmd/gomodel/main.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + _ "gomodel/cmd/gomodel/docs" "gomodel/config" "gomodel/internal/app" "gomodel/internal/observability" @@ -28,6 +29,14 @@ import ( "golang.org/x/term" ) +// @title GOModel API +// @version 1.0 +// @description High-performance AI gateway routing requests to multiple LLM providers (OpenAI, Anthropic, Gemini, Groq, xAI, Ollama). Drop-in OpenAI-compatible API. +// @BasePath / +// @schemes http +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization func main() { versionFlag := flag.Bool("version", false, "Print version information") flag.Parse() diff --git a/config/config.go b/config/config.go index a554c68c..46bc5704 100644 --- a/config/config.go +++ b/config/config.go @@ -276,9 +276,10 @@ type RedisConfig struct { // ServerConfig holds HTTP server configuration type ServerConfig struct { - Port string `yaml:"port" env:"PORT"` - MasterKey string `yaml:"master_key" env:"GOMODEL_MASTER_KEY"` // Optional: Master key for authentication - BodySizeLimit string `yaml:"body_size_limit" env:"BODY_SIZE_LIMIT"` // Max request body size (e.g., "10M", "1024K") + Port string `yaml:"port" env:"PORT"` + MasterKey string `yaml:"master_key" env:"GOMODEL_MASTER_KEY"` // Optional: Master key for authentication + BodySizeLimit string `yaml:"body_size_limit" env:"BODY_SIZE_LIMIT"` // Max request body size (e.g., "10M", "1024K") + SwaggerEnabled bool `yaml:"swagger_enabled" env:"SWAGGER_ENABLED"` // Whether to expose the Swagger UI at /swagger/index.html } // MetricsConfig holds observability configuration for Prometheus metrics @@ -339,7 +340,7 @@ type ResilienceConfig struct { // buildDefaultConfig returns the single source of truth for all configuration defaults. func buildDefaultConfig() *Config { return &Config{ - Server: ServerConfig{Port: "8080"}, + Server: ServerConfig{Port: "8080", SwaggerEnabled: true}, Cache: CacheConfig{ Type: "local", CacheDir: ".cache", diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md index 7d5a7dd2..2da72da8 100644 --- a/docs/TESTING_STRATEGY.md +++ b/docs/TESTING_STRATEGY.md @@ -211,7 +211,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version: "1.25" # Unit + E2E (no external dependencies) - run: make test-all diff --git a/go.mod b/go.mod index 76768531..66880364 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gomodel -go 1.24.0 +go 1.25.0 require ( github.com/andybalholm/brotli v1.2.0 @@ -12,6 +12,8 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 + github.com/swaggo/echo-swagger v1.4.1 + github.com/swaggo/swag v1.16.6 github.com/testcontainers/testcontainers-go v0.40.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 @@ -24,6 +26,7 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -33,6 +36,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect @@ -42,18 +46,25 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -75,10 +86,14 @@ require ( github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/urfave/cli/v2 v2.3.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -94,15 +109,19 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.46.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index fb01b688..5d4823ea 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,9 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -28,6 +31,9 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -49,6 +55,8 @@ github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0o github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -56,6 +64,19 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -77,12 +98,18 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -99,6 +126,11 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -127,6 +159,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -151,17 +184,34 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +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/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= +github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +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/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= github.com/testcontainers/testcontainers-go/modules/mongodb v0.40.0 h1:z/1qHeliTLDKNaJ7uOHOx1FjwghbcbYfga4dTFkF0hU= @@ -172,6 +222,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -219,18 +271,18 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 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/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= @@ -257,15 +309,15 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e h1:Ao9GzfUMPH3zjVfzXG5rlWlk+Q8MXWKwWpwVQE1MXfw= @@ -278,9 +330,16 @@ google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFN google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= @@ -313,3 +372,5 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/admin/handler.go b/internal/admin/handler.go index f33d7192..c587d67e 100644 --- a/internal/admin/handler.go +++ b/internal/admin/handler.go @@ -113,6 +113,18 @@ func handleError(c echo.Context, err error) error { } // UsageSummary handles GET /admin/api/v1/usage/summary +// +// @Summary Get usage summary +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Param days query int false "Number of days (default 30)" +// @Param start_date query string false "Start date (YYYY-MM-DD)" +// @Param end_date query string false "End date (YYYY-MM-DD)" +// @Success 200 {object} usage.UsageSummary +// @Failure 400 {object} core.GatewayError +// @Failure 401 {object} core.GatewayError +// @Router /admin/api/v1/usage/summary [get] func (h *Handler) UsageSummary(c echo.Context) error { if h.usageReader == nil { return c.JSON(http.StatusOK, usage.UsageSummary{}) @@ -132,6 +144,19 @@ func (h *Handler) UsageSummary(c echo.Context) error { } // DailyUsage handles GET /admin/api/v1/usage/daily +// +// @Summary Get usage breakdown by period +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Param days query int false "Number of days (default 30)" +// @Param start_date query string false "Start date (YYYY-MM-DD)" +// @Param end_date query string false "End date (YYYY-MM-DD)" +// @Param interval query string false "Grouping interval: daily, weekly, monthly, yearly (default daily)" +// @Success 200 {array} usage.DailyUsage +// @Failure 400 {object} core.GatewayError +// @Failure 401 {object} core.GatewayError +// @Router /admin/api/v1/usage/daily [get] func (h *Handler) DailyUsage(c echo.Context) error { if h.usageReader == nil { return c.JSON(http.StatusOK, []usage.DailyUsage{}) @@ -155,6 +180,14 @@ func (h *Handler) DailyUsage(c echo.Context) error { } // ListModels handles GET /admin/api/v1/models +// +// @Summary List all registered models with provider info +// @Tags admin +// @Produce json +// @Security BearerAuth +// @Success 200 {array} providers.ModelWithProvider +// @Failure 401 {object} core.GatewayError +// @Router /admin/api/v1/models [get] func (h *Handler) ListModels(c echo.Context) error { if h.registry == nil { return c.JSON(http.StatusOK, []providers.ModelWithProvider{}) diff --git a/internal/app/app.go b/internal/app/app.go index b70d5e58..b9006bf9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -131,6 +131,7 @@ func New(ctx context.Context, cfg Config) (*App, error) { AuditLogger: auditResult.Logger, UsageLogger: usageResult.Logger, LogOnlyModelInteractions: appCfg.Logging.OnlyModelInteractions, + SwaggerEnabled: appCfg.Server.SwaggerEnabled, } // Initialize admin API and dashboard (behind separate feature flags) @@ -157,6 +158,10 @@ func New(ctx context.Context, cfg Config) (*App, error) { slog.Info("admin API disabled") } + if appCfg.Server.SwaggerEnabled { + slog.Info("swagger UI enabled", "path", "/swagger/index.html") + } + app.server = server.New(provider, serverCfg) return app, nil diff --git a/internal/core/responses.go b/internal/core/responses.go index 3e48ea78..9babee96 100644 --- a/internal/core/responses.go +++ b/internal/core/responses.go @@ -4,7 +4,7 @@ package core // This is the OpenAI-compatible /v1/responses endpoint. type ResponsesRequest struct { Model string `json:"model"` - Input interface{} `json:"input"` // Can be string or []ResponsesInputItem + Input interface{} `json:"input" swaggertype:"string" example:"Tell me a joke"` // string or []ResponsesInputItem — see docs for array form Instructions string `json:"instructions,omitempty"` Tools []map[string]any `json:"tools,omitempty"` Temperature *float64 `json:"temperature,omitempty"` diff --git a/internal/server/handlers.go b/internal/server/handlers.go index ac1f8551..aa36370e 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -79,6 +79,19 @@ func (h *Handler) handleStreamingResponse(c echo.Context, model, provider string } // ChatCompletion handles POST /v1/chat/completions +// +// @Summary Create a chat completion +// @Tags chat +// @Accept json +// @Produce json text/event-stream +// @Security BearerAuth +// @Param request body core.ChatRequest true "Chat completion request" +// @Success 200 {object} core.ChatResponse "JSON response or SSE stream when stream=true" +// @Failure 400 {object} core.GatewayError +// @Failure 401 {object} core.GatewayError +// @Failure 429 {object} core.GatewayError +// @Failure 502 {object} core.GatewayError +// @Router /v1/chat/completions [post] func (h *Handler) ChatCompletion(c echo.Context) error { var req core.ChatRequest if err := c.Bind(&req); err != nil { @@ -129,11 +142,26 @@ func (h *Handler) ChatCompletion(c echo.Context) error { } // Health handles GET /health +// +// @Summary Health check +// @Tags system +// @Produce json +// @Success 200 {object} map[string]string +// @Router /health [get] func (h *Handler) Health(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) } // ListModels handles GET /v1/models +// +// @Summary List available models +// @Tags models +// @Produce json +// @Security BearerAuth +// @Success 200 {object} core.ModelsResponse +// @Failure 401 {object} core.GatewayError +// @Failure 502 {object} core.GatewayError +// @Router /v1/models [get] func (h *Handler) ListModels(c echo.Context) error { // Create context with request ID for provider requestID := c.Request().Header.Get("X-Request-ID") @@ -148,6 +176,19 @@ func (h *Handler) ListModels(c echo.Context) error { } // Responses handles POST /v1/responses +// +// @Summary Create a model response (Responses API) +// @Tags responses +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body core.ResponsesRequest true "Responses API request" +// @Success 200 {object} core.ResponsesResponse +// @Failure 400 {object} core.GatewayError +// @Failure 401 {object} core.GatewayError +// @Failure 429 {object} core.GatewayError +// @Failure 502 {object} core.GatewayError +// @Router /v1/responses [post] func (h *Handler) Responses(c echo.Context) error { var req core.ResponsesRequest if err := c.Bind(&req); err != nil { diff --git a/internal/server/http.go b/internal/server/http.go index ace9c7a1..3ad2a9f9 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -16,6 +16,8 @@ import ( "gomodel/internal/auditlog" "gomodel/internal/core" "gomodel/internal/usage" + + echoswagger "github.com/swaggo/echo-swagger" ) // Server wraps the Echo server @@ -37,6 +39,7 @@ type Config struct { AdminUIEnabled bool // Whether admin dashboard UI is enabled AdminHandler *admin.Handler // Admin API handler (nil if disabled) DashboardHandler *dashboard.Handler // Dashboard UI handler (nil if disabled) + SwaggerEnabled bool // Whether to expose the Swagger UI at /swagger/index.html } // New creates a new HTTP server @@ -78,6 +81,9 @@ func New(provider core.RoutableProvider, cfg *Config) *Server { if cfg != nil && cfg.AdminUIEnabled && cfg.DashboardHandler != nil { authSkipPaths = append(authSkipPaths, "/admin/dashboard", "/admin/dashboard/*", "/admin/static/*") } + if cfg != nil && cfg.SwaggerEnabled { + authSkipPaths = append(authSkipPaths, "/swagger/*") + } // Global middleware stack (order matters) // Request logger with optional filtering for model-only interactions @@ -135,6 +141,9 @@ func New(provider core.RoutableProvider, cfg *Config) *Server { // Public routes e.GET("/health", handler.Health) + if cfg != nil && cfg.SwaggerEnabled { + e.GET("/swagger/*", echoswagger.WrapHandler) + } if cfg != nil && cfg.MetricsEnabled { e.GET(metricsPath, echo.WrapHandler(promhttp.Handler())) } diff --git a/internal/server/http_test.go b/internal/server/http_test.go index 5a7142ea..1eb6826e 100644 --- a/internal/server/http_test.go +++ b/internal/server/http_test.go @@ -8,6 +8,8 @@ import ( "gomodel/internal/admin" "gomodel/internal/admin/dashboard" + + _ "gomodel/cmd/gomodel/docs" ) func TestMetricsEndpoint(t *testing.T) { @@ -399,3 +401,76 @@ func TestHealthEndpointAlwaysAvailable(t *testing.T) { }) } } + +func TestSwaggerEndpoint_Enabled(t *testing.T) { + mock := &mockProvider{} + srv := New(mock, &Config{SwaggerEnabled: true}) + + req := httptest.NewRequest(http.MethodGet, "/swagger/index.html", nil) + rec := httptest.NewRecorder() + + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } + + contentType := rec.Header().Get("Content-Type") + if !strings.Contains(contentType, "text/html") { + t.Errorf("expected text/html Content-Type, got %s", contentType) + } + + if !strings.Contains(rec.Body.String(), "swagger") { + t.Errorf("expected body to contain swagger UI content, got: %s", rec.Body.String()[:min(200, len(rec.Body.String()))]) + } +} + +func TestSwaggerEndpoint_Disabled(t *testing.T) { + mock := &mockProvider{} + srv := New(mock, &Config{SwaggerEnabled: false}) + + req := httptest.NewRequest(http.MethodGet, "/swagger/index.html", nil) + rec := httptest.NewRecorder() + + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", rec.Code) + } +} + +func TestSwaggerEndpoint_NilConfig(t *testing.T) { + mock := &mockProvider{} + srv := New(mock, nil) + + req := httptest.NewRequest(http.MethodGet, "/swagger/index.html", nil) + rec := httptest.NewRecorder() + + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Errorf("expected status 404, got %d", rec.Code) + } +} + +func TestSwaggerDocJson_ReturnsExpectedContent(t *testing.T) { + mock := &mockProvider{} + srv := New(mock, &Config{SwaggerEnabled: true}) + + req := httptest.NewRequest(http.MethodGet, "/swagger/doc.json", nil) + rec := httptest.NewRecorder() + + srv.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, "GOModel") { + t.Errorf("expected doc.json to contain GOModel API title, got: %s", body[:min(300, len(body))]) + } + if !strings.Contains(body, "swagger") { + t.Errorf("expected doc.json to contain swagger spec, got: %s", body[:min(300, len(body))]) + } +}