Skip to content

Commit 67a8272

Browse files
Copilotericcurtin
andcommitted
Add Swagger UI API documentation as homepage
- Create OpenAPI 3.0.3 specification documenting OpenAI, Anthropic, and Ollama APIs - Create swagger package with embedded OpenAPI spec and Swagger UI handler - Update main.go to serve Swagger UI at the homepage (/) - Add comprehensive API documentation for all endpoints - Add unit tests for swagger handler Co-authored-by: ericcurtin <[email protected]>
1 parent 0ce28f0 commit 67a8272

File tree

4 files changed

+1848
-6
lines changed

4 files changed

+1848
-6
lines changed

main.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/docker/model-runner/pkg/ollama"
2626
"github.com/docker/model-runner/pkg/responses"
2727
"github.com/docker/model-runner/pkg/routing"
28+
"github.com/docker/model-runner/pkg/swagger"
2829
"github.com/sirupsen/logrus"
2930
)
3031

@@ -196,15 +197,15 @@ func main() {
196197
anthropicHandler := anthropic.NewHandler(log, schedulerHTTP, nil, modelManager)
197198
router.Handle(anthropic.APIPrefix+"/", anthropicHandler)
198199

199-
// Register root handler LAST - it will only catch exact "/" requests that don't match other patterns
200+
// Register Swagger UI handler for API documentation at the homepage
201+
swaggerHandler := swagger.NewHandler()
200202
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
201-
// Only respond to exact root path
202-
if r.URL.Path != "/" {
203-
http.NotFound(w, r)
203+
// Only respond to root path and OpenAPI spec
204+
if r.URL.Path == "/" || r.URL.Path == "/index.html" || r.URL.Path == "/openapi.yaml" {
205+
swaggerHandler.ServeHTTP(w, r)
204206
return
205207
}
206-
w.WriteHeader(http.StatusOK)
207-
_, _ = w.Write([]byte("Docker Model Runner is running"))
208+
http.NotFound(w, r)
208209
})
209210

210211
// Add metrics endpoint if enabled

pkg/swagger/handler.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Package swagger provides a Swagger UI handler for API documentation.
2+
package swagger
3+
4+
import (
5+
_ "embed"
6+
"net/http"
7+
"strings"
8+
)
9+
10+
//go:embed openapi.yaml
11+
var openapiSpec []byte
12+
13+
// Handler serves the Swagger UI and OpenAPI specification.
14+
type Handler struct{}
15+
16+
// NewHandler creates a new Swagger UI handler.
17+
func NewHandler() *Handler {
18+
return &Handler{}
19+
}
20+
21+
// ServeHTTP serves the Swagger UI or OpenAPI specification.
22+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
23+
path := strings.TrimPrefix(r.URL.Path, "/")
24+
25+
switch path {
26+
case "", "index.html":
27+
h.serveSwaggerUI(w, r)
28+
case "openapi.yaml":
29+
h.serveOpenAPISpec(w, r)
30+
default:
31+
http.NotFound(w, r)
32+
}
33+
}
34+
35+
// serveOpenAPISpec serves the OpenAPI specification file.
36+
func (h *Handler) serveOpenAPISpec(w http.ResponseWriter, _ *http.Request) {
37+
w.Header().Set("Content-Type", "application/yaml")
38+
w.Header().Set("Access-Control-Allow-Origin", "*")
39+
_, _ = w.Write(openapiSpec)
40+
}
41+
42+
// serveSwaggerUI serves the Swagger UI HTML page.
43+
func (h *Handler) serveSwaggerUI(w http.ResponseWriter, _ *http.Request) {
44+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
45+
_, _ = w.Write([]byte(swaggerUIHTML))
46+
}
47+
48+
// swaggerUIHTML is the HTML template for Swagger UI.
49+
const swaggerUIHTML = `<!DOCTYPE html>
50+
<html lang="en">
51+
<head>
52+
<meta charset="UTF-8">
53+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
54+
<title>Docker Model Runner API Documentation</title>
55+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
56+
<style>
57+
html {
58+
box-sizing: border-box;
59+
overflow: -moz-scrollbars-vertical;
60+
overflow-y: scroll;
61+
}
62+
*,
63+
*:before,
64+
*:after {
65+
box-sizing: inherit;
66+
}
67+
body {
68+
margin: 0;
69+
background: #fafafa;
70+
}
71+
.swagger-ui .topbar {
72+
background-color: #1d63ed;
73+
}
74+
.swagger-ui .topbar .download-url-wrapper .select-label {
75+
display: flex;
76+
align-items: center;
77+
width: 100%;
78+
max-width: 600px;
79+
}
80+
.swagger-ui .info .title {
81+
color: #1d63ed;
82+
}
83+
.swagger-ui .opblock.opblock-post {
84+
border-color: #49cc90;
85+
background: rgba(73, 204, 144, .1);
86+
}
87+
.swagger-ui .opblock.opblock-get {
88+
border-color: #61affe;
89+
background: rgba(97, 175, 254, .1);
90+
}
91+
.swagger-ui .opblock.opblock-delete {
92+
border-color: #f93e3e;
93+
background: rgba(249, 62, 62, .1);
94+
}
95+
.custom-header {
96+
background: linear-gradient(135deg, #1d63ed 0%, #0d47a1 100%);
97+
color: white;
98+
padding: 20px 40px;
99+
text-align: center;
100+
}
101+
.custom-header h1 {
102+
margin: 0 0 10px 0;
103+
font-size: 28px;
104+
font-weight: 600;
105+
}
106+
.custom-header p {
107+
margin: 0;
108+
opacity: 0.9;
109+
font-size: 14px;
110+
}
111+
.custom-header a {
112+
color: white;
113+
text-decoration: underline;
114+
}
115+
</style>
116+
</head>
117+
<body>
118+
<div class="custom-header">
119+
<h1>🐳 Docker Model Runner API</h1>
120+
<p>Run AI models locally with OpenAI, Anthropic, and Ollama compatible APIs</p>
121+
</div>
122+
<div id="swagger-ui"></div>
123+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
124+
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
125+
<script>
126+
window.onload = function() {
127+
window.ui = SwaggerUIBundle({
128+
url: "/openapi.yaml",
129+
dom_id: '#swagger-ui',
130+
deepLinking: true,
131+
presets: [
132+
SwaggerUIBundle.presets.apis,
133+
SwaggerUIStandalonePreset
134+
],
135+
plugins: [
136+
SwaggerUIBundle.plugins.DownloadUrl
137+
],
138+
layout: "StandaloneLayout",
139+
defaultModelsExpandDepth: 1,
140+
defaultModelExpandDepth: 1,
141+
docExpansion: "list",
142+
filter: true,
143+
showExtensions: true,
144+
showCommonExtensions: true,
145+
tryItOutEnabled: true
146+
});
147+
};
148+
</script>
149+
</body>
150+
</html>
151+
`

pkg/swagger/handler_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package swagger
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestHandler_ServeHTTP_Root(t *testing.T) {
11+
handler := NewHandler()
12+
13+
req := httptest.NewRequest(http.MethodGet, "/", nil)
14+
w := httptest.NewRecorder()
15+
16+
handler.ServeHTTP(w, req)
17+
18+
if w.Code != http.StatusOK {
19+
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
20+
}
21+
22+
contentType := w.Header().Get("Content-Type")
23+
if !strings.Contains(contentType, "text/html") {
24+
t.Errorf("expected Content-Type to contain text/html, got %s", contentType)
25+
}
26+
27+
body := w.Body.String()
28+
if !strings.Contains(body, "Docker Model Runner") {
29+
t.Error("expected body to contain 'Docker Model Runner'")
30+
}
31+
if !strings.Contains(body, "swagger-ui") {
32+
t.Error("expected body to contain 'swagger-ui'")
33+
}
34+
}
35+
36+
func TestHandler_ServeHTTP_IndexHTML(t *testing.T) {
37+
handler := NewHandler()
38+
39+
req := httptest.NewRequest(http.MethodGet, "/index.html", nil)
40+
w := httptest.NewRecorder()
41+
42+
handler.ServeHTTP(w, req)
43+
44+
if w.Code != http.StatusOK {
45+
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
46+
}
47+
48+
contentType := w.Header().Get("Content-Type")
49+
if !strings.Contains(contentType, "text/html") {
50+
t.Errorf("expected Content-Type to contain text/html, got %s", contentType)
51+
}
52+
}
53+
54+
func TestHandler_ServeHTTP_OpenAPISpec(t *testing.T) {
55+
handler := NewHandler()
56+
57+
req := httptest.NewRequest(http.MethodGet, "/openapi.yaml", nil)
58+
w := httptest.NewRecorder()
59+
60+
handler.ServeHTTP(w, req)
61+
62+
if w.Code != http.StatusOK {
63+
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
64+
}
65+
66+
contentType := w.Header().Get("Content-Type")
67+
if contentType != "application/yaml" {
68+
t.Errorf("expected Content-Type 'application/yaml', got %s", contentType)
69+
}
70+
71+
body := w.Body.String()
72+
if !strings.Contains(body, "openapi: 3.0.3") {
73+
t.Error("expected body to contain OpenAPI version")
74+
}
75+
if !strings.Contains(body, "Docker Model Runner API") {
76+
t.Error("expected body to contain API title")
77+
}
78+
}
79+
80+
func TestHandler_ServeHTTP_NotFound(t *testing.T) {
81+
handler := NewHandler()
82+
83+
req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
84+
w := httptest.NewRecorder()
85+
86+
handler.ServeHTTP(w, req)
87+
88+
if w.Code != http.StatusNotFound {
89+
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
90+
}
91+
}
92+
93+
func TestOpenAPISpecContainsAllAPIs(t *testing.T) {
94+
spec := string(openapiSpec)
95+
96+
// Check for OpenAI API endpoints
97+
if !strings.Contains(spec, "/v1/chat/completions") {
98+
t.Error("expected OpenAPI spec to contain /v1/chat/completions endpoint")
99+
}
100+
if !strings.Contains(spec, "/v1/completions") {
101+
t.Error("expected OpenAPI spec to contain /v1/completions endpoint")
102+
}
103+
if !strings.Contains(spec, "/v1/embeddings") {
104+
t.Error("expected OpenAPI spec to contain /v1/embeddings endpoint")
105+
}
106+
107+
// Check for Anthropic API endpoints
108+
if !strings.Contains(spec, "/anthropic/v1/messages") {
109+
t.Error("expected OpenAPI spec to contain /anthropic/v1/messages endpoint")
110+
}
111+
112+
// Check for Ollama API endpoints
113+
if !strings.Contains(spec, "/api/chat") {
114+
t.Error("expected OpenAPI spec to contain /api/chat endpoint")
115+
}
116+
if !strings.Contains(spec, "/api/generate") {
117+
t.Error("expected OpenAPI spec to contain /api/generate endpoint")
118+
}
119+
if !strings.Contains(spec, "/api/tags") {
120+
t.Error("expected OpenAPI spec to contain /api/tags endpoint")
121+
}
122+
123+
// Check for tags
124+
if !strings.Contains(spec, "OpenAI API") {
125+
t.Error("expected OpenAPI spec to contain 'OpenAI API' tag")
126+
}
127+
if !strings.Contains(spec, "Anthropic API") {
128+
t.Error("expected OpenAPI spec to contain 'Anthropic API' tag")
129+
}
130+
if !strings.Contains(spec, "Ollama API") {
131+
t.Error("expected OpenAPI spec to contain 'Ollama API' tag")
132+
}
133+
}

0 commit comments

Comments
 (0)