rnilkyway commited on
Commit
06c8c6a
Β·
1 Parent(s): e0f8aa6

feat(docs): branded /docs page replacing default Swagger UI

Browse files

- New app/templates/docs.html in the landing.html visual language
(Inter + JetBrains Mono, black bg, gray-900 cards, grid pattern).
- TOC sidebar with IntersectionObserver-driven active-link tracking.
- Quick-start, auth, base URL, endpoints, errors & limits, SDK examples
in both Python and JavaScript.
- Veo 3 section documents every parameter (duration_seconds 4/6/8,
resolution 720p/1080p, aspect_ratio 16:9/9:16, image_url for
image-to-video) sourced from the upstream OpenAPI spec.
- Gemini 3 image section documents flash/pro variants and aspect_ratio
options (1:1, 16:9, 9:16, 4:3, 3:4).
- Live models catalog widget fetches /v1/models on load and supports
All/Chat/Image/Video filters; each tile copies the model id.
- Auto-injects window.location.origin into every 'base URL' placeholder
so curl snippets are runnable as-is.

Backend wiring:
- FastAPI Swagger UI moved to /openapi-docs (docs_url='/openapi-docs').
- New GET /docs route serves the branded docs page (include_in_schema
false), with no-cache headers so users always see the latest.

Files changed (2) hide show
  1. app/index.py +16 -1
  2. app/templates/docs.html +699 -0
app/index.py CHANGED
@@ -471,7 +471,9 @@ Admin endpoints require the master key.
471
  - **Redoc:** [/redoc](/redoc)
472
  - **OpenAPI JSON:** [/openapi.json](/openapi.json)
473
  """,
474
- docs_url="/docs",
 
 
475
  redoc_url="/redoc",
476
  lifespan=lifespan,
477
  )
@@ -945,6 +947,19 @@ async def root():
945
  return HTMLResponse(landing_html())
946
 
947
 
 
 
 
 
 
 
 
 
 
 
 
 
 
948
  @app.get("/health")
949
  async def health():
950
  return {"status": "ok", "timestamp": int(time.time())}
 
471
  - **Redoc:** [/redoc](/redoc)
472
  - **OpenAPI JSON:** [/openapi.json](/openapi.json)
473
  """,
474
+ # /docs is owned by our custom documentation page (templates/docs.html);
475
+ # Swagger UI is served at /openapi-docs instead.
476
+ docs_url="/openapi-docs",
477
  redoc_url="/redoc",
478
  lifespan=lifespan,
479
  )
 
947
  return HTMLResponse(landing_html())
948
 
949
 
950
+ @app.get("/docs", include_in_schema=False)
951
+ async def custom_docs():
952
+ """Branded API documentation page (replaces the default Swagger UI).
953
+
954
+ Swagger UI is still available at ``/openapi-docs`` for users who prefer
955
+ the interactive playground.
956
+ """
957
+ return HTMLResponse(
958
+ templates.load("docs.html"),
959
+ headers={"Cache-Control": "no-store, no-cache, must-revalidate"},
960
+ )
961
+
962
+
963
  @app.get("/health")
964
  async def health():
965
  return {"status": "ok", "timestamp": int(time.time())}
app/templates/docs.html ADDED
@@ -0,0 +1,699 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="antialiased scroll-smooth">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>APIarium | API Documentation</title>
7
+ <meta name="description" content="APIarium API documentation β€” OpenAI-compatible endpoints, supported models (Veo 3 video, Gemini 3 image, GPT, Claude, etc.), and code examples.">
8
+
9
+ <!-- Professional Typography -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
13
+
14
+ <!-- Tailwind CSS -->
15
+ <script src="https://cdn.tailwindcss.com"></script>
16
+ <script>
17
+ tailwind.config = {
18
+ theme: {
19
+ extend: {
20
+ fontFamily: {
21
+ sans: ['Inter', 'sans-serif'],
22
+ mono: ['JetBrains Mono', 'monospace'],
23
+ },
24
+ colors: {
25
+ gray: {
26
+ 900: '#0A0A0A',
27
+ 800: '#171717',
28
+ 700: '#262626',
29
+ 600: '#404040',
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ </script>
36
+
37
+ <style>
38
+ body {
39
+ background-color: #000000;
40
+ color: #ededed;
41
+ }
42
+
43
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
44
+ ::-webkit-scrollbar-track { background: transparent; }
45
+ ::-webkit-scrollbar-thumb { background: #262626; border-radius: 4px; }
46
+ ::-webkit-scrollbar-thumb:hover { background: #404040; }
47
+
48
+ @keyframes slideUp {
49
+ from { transform: translate(-50%, 100%); opacity: 0; }
50
+ to { transform: translate(-50%, 0); opacity: 1; }
51
+ }
52
+ .toast-enter { animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
53
+
54
+ .bg-grid-pattern {
55
+ background-image: linear-gradient(to right, #ffffff05 1px, transparent 1px),
56
+ linear-gradient(to bottom, #ffffff05 1px, transparent 1px);
57
+ background-size: 40px 40px;
58
+ mask-image: radial-gradient(circle at center, black, transparent 80%);
59
+ -webkit-mask-image: radial-gradient(circle at center, black, transparent 80%);
60
+ }
61
+
62
+ /* Code block styling */
63
+ pre {
64
+ font-family: 'JetBrains Mono', monospace;
65
+ font-size: 12.5px;
66
+ line-height: 1.55;
67
+ white-space: pre;
68
+ overflow-x: auto;
69
+ color: #d4d4d4;
70
+ }
71
+ pre .tk-key { color: #93c5fd; } /* JSON keys β†’ light blue */
72
+ pre .tk-str { color: #86efac; } /* strings β†’ green */
73
+ pre .tk-num { color: #fbbf24; } /* numbers β†’ amber */
74
+ pre .tk-bool { color: #f0abfc; } /* booleans β†’ pink */
75
+ pre .tk-cmt { color: #6b7280; font-style: italic; }
76
+ pre .tk-fn { color: #fbbf24; } /* function names */
77
+ pre .tk-kw { color: #c4b5fd; } /* keywords */
78
+
79
+ /* Anchor offset under sticky header */
80
+ section[id] { scroll-margin-top: 80px; }
81
+
82
+ /* TOC active state */
83
+ .toc-link.active {
84
+ color: #fff;
85
+ background: rgba(255,255,255,0.05);
86
+ border-left-color: #fbbf24;
87
+ }
88
+ </style>
89
+ </head>
90
+ <body class="min-h-screen flex flex-col relative">
91
+
92
+ <div class="absolute inset-0 bg-grid-pattern pointer-events-none z-0"></div>
93
+
94
+ <header class="sticky top-0 z-50 bg-black/80 backdrop-blur-md border-b border-white/10">
95
+ <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
96
+ <a href="/" class="flex items-center gap-3 group">
97
+ <span class="text-2xl" aria-label="APIarium">🐝</span>
98
+ <span class="font-semibold text-lg tracking-tight group-hover:text-amber-300 transition-colors">APIarium</span>
99
+ <span class="ml-2 px-2 py-0.5 text-[10px] font-mono font-medium bg-white/10 text-white rounded-full border border-white/20">v1.0.0</span>
100
+ </a>
101
+ <nav class="hidden sm:flex items-center gap-6 text-sm font-medium text-gray-400">
102
+ <a href="/" class="hover:text-white transition-colors">Home</a>
103
+ <a href="/docs" class="text-white">Documentation</a>
104
+ <a href="/openapi-docs" class="hover:text-white transition-colors">Swagger</a>
105
+ <a href="/admin" class="hover:text-white transition-colors">Admin</a>
106
+ </nav>
107
+ </div>
108
+ </header>
109
+
110
+ <main class="flex-grow w-full max-w-7xl mx-auto px-6 py-12 relative z-10">
111
+
112
+ <!-- Hero -->
113
+ <section class="pb-10 border-b border-gray-800 mb-10">
114
+ <div class="flex items-center gap-2 text-xs font-mono uppercase tracking-wider text-amber-400 mb-3">
115
+ <span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
116
+ Reference
117
+ </div>
118
+ <h1 class="text-4xl sm:text-5xl font-semibold tracking-tight text-white mb-4 leading-tight">
119
+ API Documentation
120
+ </h1>
121
+ <p class="text-lg text-gray-400 leading-relaxed max-w-3xl">
122
+ Everything you need to integrate with APIarium β€” an OpenAI-compatible gateway that fans out to GPT, Claude, Gemini, DeepSeek, Veo 3 video, Gemini 3 image and more, behind a single Bearer token.
123
+ </p>
124
+
125
+ <div class="flex flex-wrap gap-2 mt-6">
126
+ <span class="px-3 py-1 text-xs font-mono bg-blue-500/10 text-blue-300 border border-blue-500/20 rounded-md">OpenAI-compatible</span>
127
+ <span class="px-3 py-1 text-xs font-mono bg-emerald-500/10 text-emerald-300 border border-emerald-500/20 rounded-md">Streaming</span>
128
+ <span class="px-3 py-1 text-xs font-mono bg-amber-500/10 text-amber-300 border border-amber-500/20 rounded-md">Veo 3 video</span>
129
+ <span class="px-3 py-1 text-xs font-mono bg-fuchsia-500/10 text-fuchsia-300 border border-fuchsia-500/20 rounded-md">Gemini 3 image</span>
130
+ <span class="px-3 py-1 text-xs font-mono bg-white/5 text-gray-300 border border-white/10 rounded-md">IP-bound keys</span>
131
+ </div>
132
+ </section>
133
+
134
+ <div class="grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-12">
135
+
136
+ <!-- TOC sidebar -->
137
+ <aside class="hidden lg:block">
138
+ <nav class="sticky top-24 space-y-1 text-sm">
139
+ <p class="text-[10px] font-mono uppercase tracking-wider text-gray-500 mb-3 px-3">On this page</p>
140
+ <a href="#quickstart" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Quick start</a>
141
+ <a href="#auth" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Authentication</a>
142
+ <a href="#base-url" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Base URL</a>
143
+ <a href="#endpoints" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Endpoints</a>
144
+ <a href="#models" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Models catalog</a>
145
+ <a href="#chat" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Chat completions</a>
146
+ <a href="#veo3" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Veo 3 β€” video</a>
147
+ <a href="#gemini3-image" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Gemini 3 β€” image</a>
148
+ <a href="#gemini3-text" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Gemini 3 β€” text</a>
149
+ <a href="#images-openai" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">/v1/images</a>
150
+ <a href="#errors" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">Errors &amp; limits</a>
151
+ <a href="#sdks" class="toc-link block px-3 py-1.5 border-l border-gray-800 text-gray-400 hover:text-white transition-colors">SDK examples</a>
152
+ </nav>
153
+ </aside>
154
+
155
+ <!-- Content -->
156
+ <article class="space-y-16 min-w-0">
157
+
158
+ <!-- QUICK START -->
159
+ <section id="quickstart">
160
+ <h2 class="text-2xl font-semibold text-white mb-2">Quick start</h2>
161
+ <p class="text-gray-400 mb-6">Three steps and you're sending requests.</p>
162
+
163
+ <ol class="space-y-5">
164
+ <li class="flex gap-4">
165
+ <span class="shrink-0 w-7 h-7 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs font-mono flex items-center justify-center">1</span>
166
+ <div>
167
+ <p class="text-sm font-medium text-white mb-1">Grab an API key</p>
168
+ <p class="text-sm text-gray-400">Visit <a class="text-amber-300 hover:underline" href="/">the homepage</a> β€” a key is automatically generated for your IP. Keys are bound to that IP.</p>
169
+ </div>
170
+ </li>
171
+ <li class="flex gap-4">
172
+ <span class="shrink-0 w-7 h-7 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs font-mono flex items-center justify-center">2</span>
173
+ <div>
174
+ <p class="text-sm font-medium text-white mb-1">Point your OpenAI SDK at us</p>
175
+ <p class="text-sm text-gray-400">Override <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">base_url</code> and you're done β€” everything else is identical to OpenAI.</p>
176
+ </div>
177
+ </li>
178
+ <li class="flex gap-4">
179
+ <span class="shrink-0 w-7 h-7 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-300 text-xs font-mono flex items-center justify-center">3</span>
180
+ <div>
181
+ <p class="text-sm font-medium text-white mb-1">Pick any model</p>
182
+ <p class="text-sm text-gray-400">From <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gpt-5</code> to <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">veo-3</code>. See the <a class="text-amber-300 hover:underline" href="#models">full catalog</a>.</p>
183
+ </div>
184
+ </li>
185
+ </ol>
186
+
187
+ <div class="mt-8 bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
188
+ <div class="px-4 py-2.5 border-b border-gray-800 flex items-center justify-between">
189
+ <span class="text-[10px] font-mono uppercase tracking-wider text-gray-500">curl Β· Generate a Veo 3 video</span>
190
+ <button class="copy-btn text-[10px] font-mono uppercase tracking-wider text-gray-500 hover:text-white transition-colors px-2 py-1 rounded border border-gray-800">Copy</button>
191
+ </div>
192
+ <pre class="px-4 py-4"><span class="tk-cmt"># Returns a Markdown link + raw URL to the generated MP4</span>
193
+ curl <span class="tk-str">"<span id="base-url-1">https://your-apiarium.example.com</span>/v1/chat/completions"</span> \
194
+ -H <span class="tk-str">"Authorization: Bearer api-xxxxxxxxxxxxxxxx"</span> \
195
+ -H <span class="tk-str">"Content-Type: application/json"</span> \
196
+ -d <span class="tk-str">'{
197
+ "model": "veo-3",
198
+ "messages": [{"role":"user","content":"a red apple rotating on white, studio lighting"}],
199
+ "duration_seconds": "8",
200
+ "resolution": "1080p",
201
+ "aspect_ratio": "16:9"
202
+ }'</span></pre>
203
+ </div>
204
+ </section>
205
+
206
+ <!-- AUTH -->
207
+ <section id="auth">
208
+ <h2 class="text-2xl font-semibold text-white mb-2">Authentication</h2>
209
+ <p class="text-gray-400 mb-4">Every request to <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">/v1/*</code> needs a Bearer token.</p>
210
+ <div class="bg-gray-900 border border-gray-800 rounded-xl p-4 mb-4">
211
+ <pre><span class="tk-kw">Authorization:</span> Bearer <span class="tk-str">api-xxxxxxxxxxxxxxxx</span></pre>
212
+ </div>
213
+ <ul class="text-sm text-gray-400 space-y-2 leading-relaxed">
214
+ <li>πŸ”’ Each key is <strong class="text-white font-medium">bound to one IP address</strong>. Requests from a different network are rejected with HTTP 401.</li>
215
+ <li>⏱️ Default rate limit: <strong class="text-white font-medium">4 RPM</strong> per key. The master key bypasses limits.</li>
216
+ <li>πŸͺͺ Lose your key? Visit <a class="text-amber-300 hover:underline" href="/">the homepage</a> again β€” your IP will get the same key back.</li>
217
+ </ul>
218
+ </section>
219
+
220
+ <!-- BASE URL -->
221
+ <section id="base-url">
222
+ <h2 class="text-2xl font-semibold text-white mb-2">Base URL</h2>
223
+ <p class="text-gray-400 mb-4">All endpoints below are relative to:</p>
224
+ <div class="bg-gray-900 border border-gray-800 rounded-xl p-4 flex items-center justify-between">
225
+ <code class="font-mono text-sm text-amber-300" id="base-url-display">https://your-apiarium.example.com</code>
226
+ <button id="copy-base-url" class="text-[10px] font-mono uppercase tracking-wider text-gray-500 hover:text-white transition-colors px-2 py-1 rounded border border-gray-800">Copy</button>
227
+ </div>
228
+ <p class="text-xs text-gray-500 mt-2 font-mono">↑ this page auto-detects the running host</p>
229
+ </section>
230
+
231
+ <!-- ENDPOINTS -->
232
+ <section id="endpoints">
233
+ <h2 class="text-2xl font-semibold text-white mb-6">Endpoints</h2>
234
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
235
+ <table class="w-full text-sm">
236
+ <thead class="bg-gray-900/50 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">
237
+ <tr>
238
+ <th class="text-left px-4 py-3 w-16">Method</th>
239
+ <th class="text-left px-4 py-3">Path</th>
240
+ <th class="text-left px-4 py-3">Purpose</th>
241
+ </tr>
242
+ </thead>
243
+ <tbody class="divide-y divide-gray-800">
244
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-blue-500/10 text-blue-400 rounded">GET</span></td> <td class="px-4 py-3 font-mono text-gray-300">/v1/models</td> <td class="px-4 py-3 text-gray-400">List all available model aliases</td></tr>
245
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-amber-500/10 text-amber-400 rounded">POST</span></td><td class="px-4 py-3 font-mono text-gray-300">/v1/chat/completions</td> <td class="px-4 py-3 text-gray-400">Chat with any text/image/video model</td></tr>
246
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-amber-500/10 text-amber-400 rounded">POST</span></td><td class="px-4 py-3 font-mono text-gray-300">/v1/completions</td> <td class="px-4 py-3 text-gray-400">Legacy text completions</td></tr>
247
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-amber-500/10 text-amber-400 rounded">POST</span></td><td class="px-4 py-3 font-mono text-gray-300">/v1/images/generations</td><td class="px-4 py-3 text-gray-400">OpenAI-style image generation (gpt-image)</td></tr>
248
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-blue-500/10 text-blue-400 rounded">GET</span></td> <td class="px-4 py-3 font-mono text-gray-300">/v1/keys/me</td> <td class="px-4 py-3 text-gray-400">Get / auto-generate the key bound to your IP</td></tr>
249
+ <tr><td class="px-4 py-3"><span class="px-1.5 py-0.5 text-[10px] font-mono font-bold bg-blue-500/10 text-blue-400 rounded">GET</span></td> <td class="px-4 py-3 font-mono text-gray-300">/health</td> <td class="px-4 py-3 text-gray-400">Health check</td></tr>
250
+ </tbody>
251
+ </table>
252
+ </div>
253
+ </section>
254
+
255
+ <!-- MODELS -->
256
+ <section id="models">
257
+ <h2 class="text-2xl font-semibold text-white mb-2">Models catalog</h2>
258
+ <p class="text-gray-400 mb-6">Live snapshot from <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">GET /v1/models</code>. Click any tile to copy the model id.</p>
259
+
260
+ <div class="mb-4 flex items-center justify-between">
261
+ <div class="flex flex-wrap gap-2 text-xs font-mono">
262
+ <button data-filter="all" class="model-filter px-3 py-1.5 rounded-md border border-gray-700 bg-gray-900 text-gray-200 hover:border-amber-500/50 transition-colors active-filter">All</button>
263
+ <button data-filter="chat" class="model-filter px-3 py-1.5 rounded-md border border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-600 hover:text-white transition-colors">Chat / text</button>
264
+ <button data-filter="image" class="model-filter px-3 py-1.5 rounded-md border border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-600 hover:text-white transition-colors">Image</button>
265
+ <button data-filter="video" class="model-filter px-3 py-1.5 rounded-md border border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-600 hover:text-white transition-colors">Video</button>
266
+ </div>
267
+ <span id="docs-models-badge" class="text-xs text-gray-500 font-mono">Loading…</span>
268
+ </div>
269
+
270
+ <div id="docs-models-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
271
+ <div class="col-span-full py-10 text-center text-gray-500 text-sm font-mono">Loading model registry…</div>
272
+ </div>
273
+ </section>
274
+
275
+ <!-- CHAT -->
276
+ <section id="chat">
277
+ <h2 class="text-2xl font-semibold text-white mb-2">Chat completions</h2>
278
+ <p class="text-gray-400 mb-6">Drop-in replacement for <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">POST https://api.openai.com/v1/chat/completions</code>. Streaming via <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">"stream": true</code> works out of the box.</p>
279
+
280
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
281
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
282
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">Request body</div>
283
+ <pre class="px-4 py-4">{
284
+ <span class="tk-key">"model"</span>: <span class="tk-str">"gpt-5"</span>,
285
+ <span class="tk-key">"messages"</span>: [
286
+ {<span class="tk-key">"role"</span>: <span class="tk-str">"user"</span>, <span class="tk-key">"content"</span>: <span class="tk-str">"Hello"</span>}
287
+ ],
288
+ <span class="tk-key">"stream"</span>: <span class="tk-bool">true</span>,
289
+ <span class="tk-key">"temperature"</span>: <span class="tk-num">0.7</span>
290
+ }</pre>
291
+ </div>
292
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
293
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">Response (non-stream)</div>
294
+ <pre class="px-4 py-4">{
295
+ <span class="tk-key">"id"</span>: <span class="tk-str">"chatcmpl-…"</span>,
296
+ <span class="tk-key">"object"</span>: <span class="tk-str">"chat.completion"</span>,
297
+ <span class="tk-key">"model"</span>: <span class="tk-str">"gpt-5"</span>,
298
+ <span class="tk-key">"choices"</span>: [{
299
+ <span class="tk-key">"index"</span>: <span class="tk-num">0</span>,
300
+ <span class="tk-key">"message"</span>: {
301
+ <span class="tk-key">"role"</span>: <span class="tk-str">"assistant"</span>,
302
+ <span class="tk-key">"content"</span>: <span class="tk-str">"Hi! How can I help?"</span>
303
+ },
304
+ <span class="tk-key">"finish_reason"</span>: <span class="tk-str">"stop"</span>
305
+ }]
306
+ }</pre>
307
+ </div>
308
+ </div>
309
+ </section>
310
+
311
+ <!-- VEO 3 -->
312
+ <section id="veo3">
313
+ <div class="flex items-center gap-2 text-xs font-mono uppercase tracking-wider text-amber-400 mb-2">
314
+ <span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
315
+ Video generation
316
+ </div>
317
+ <h2 class="text-2xl font-semibold text-white mb-2">Veo 3 β€” video</h2>
318
+ <p class="text-gray-400 mb-6">
319
+ Send a <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">chat/completions</code> request with model
320
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">veo-3</code>,
321
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">veo-3-fast</code> or
322
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">veo-3.1-fast</code>.
323
+ The assistant message in the response contains a Markdown link and the raw URL to the generated MP4.
324
+ </p>
325
+
326
+ <h3 class="text-sm font-medium text-white uppercase tracking-wider mb-3">Supported parameters</h3>
327
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden mb-6">
328
+ <table class="w-full text-sm">
329
+ <thead class="bg-gray-900/50 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">
330
+ <tr>
331
+ <th class="text-left px-4 py-3">Field</th>
332
+ <th class="text-left px-4 py-3">Type</th>
333
+ <th class="text-left px-4 py-3">Default</th>
334
+ <th class="text-left px-4 py-3">Notes</th>
335
+ </tr>
336
+ </thead>
337
+ <tbody class="divide-y divide-gray-800 text-gray-300">
338
+ <tr>
339
+ <td class="px-4 py-3 font-mono text-amber-300">messages[].content</td>
340
+ <td class="px-4 py-3 text-gray-400 font-mono">string</td>
341
+ <td class="px-4 py-3 text-gray-500 font-mono">β€”</td>
342
+ <td class="px-4 py-3 text-gray-400">The video prompt. We flatten all user/system turns into one string.</td>
343
+ </tr>
344
+ <tr>
345
+ <td class="px-4 py-3 font-mono text-amber-300">duration_seconds</td>
346
+ <td class="px-4 py-3 text-gray-400 font-mono">"4" | "6" | "8"</td>
347
+ <td class="px-4 py-3 text-gray-500 font-mono">"8"</td>
348
+ <td class="px-4 py-3 text-gray-400">Even seconds only. Longer = more credits.</td>
349
+ </tr>
350
+ <tr>
351
+ <td class="px-4 py-3 font-mono text-amber-300">resolution</td>
352
+ <td class="px-4 py-3 text-gray-400 font-mono">"720p" | "1080p"</td>
353
+ <td class="px-4 py-3 text-gray-500 font-mono">"720p"</td>
354
+ <td class="px-4 py-3 text-gray-400">1080p costs more.</td>
355
+ </tr>
356
+ <tr>
357
+ <td class="px-4 py-3 font-mono text-amber-300">aspect_ratio</td>
358
+ <td class="px-4 py-3 text-gray-400 font-mono">"16:9" | "9:16"</td>
359
+ <td class="px-4 py-3 text-gray-500 font-mono">"16:9"</td>
360
+ <td class="px-4 py-3 text-gray-400">Landscape or portrait.</td>
361
+ </tr>
362
+ <tr>
363
+ <td class="px-4 py-3 font-mono text-amber-300">image_url</td>
364
+ <td class="px-4 py-3 text-gray-400 font-mono">string</td>
365
+ <td class="px-4 py-3 text-gray-500 font-mono">β€”</td>
366
+ <td class="px-4 py-3 text-gray-400">Image-to-video: seed frame that gets animated.</td>
367
+ </tr>
368
+ </tbody>
369
+ </table>
370
+ </div>
371
+
372
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
373
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
374
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">Request</div>
375
+ <pre class="px-4 py-4">{
376
+ <span class="tk-key">"model"</span>: <span class="tk-str">"veo-3"</span>,
377
+ <span class="tk-key">"messages"</span>: [
378
+ {<span class="tk-key">"role"</span>: <span class="tk-str">"user"</span>,
379
+ <span class="tk-key">"content"</span>: <span class="tk-str">"a red apple rotating on white, studio lighting"</span>}
380
+ ],
381
+ <span class="tk-key">"duration_seconds"</span>: <span class="tk-str">"8"</span>,
382
+ <span class="tk-key">"resolution"</span>: <span class="tk-str">"1080p"</span>,
383
+ <span class="tk-key">"aspect_ratio"</span>: <span class="tk-str">"16:9"</span>
384
+ }</pre>
385
+ </div>
386
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
387
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">Response Β· assistant message</div>
388
+ <pre class="px-4 py-4">🎬 Generated with **veo-3.1-fast-generate-preview**
389
+ (1080p, 16:9, 8s)
390
+
391
+ [β–Ά Video](https://…/videos/vid_xxx.mp4)
392
+
393
+ https://…/videos/vid_xxx.mp4</pre>
394
+ </div>
395
+ </div>
396
+
397
+ <div class="mt-4 p-4 rounded-xl border border-amber-500/20 bg-amber-500/5 text-sm text-amber-200">
398
+ ⚠️ Video generation takes <strong>30 s – 2 min</strong>. Set an HTTP timeout of at least <strong>10 minutes</strong> client-side. Veo 3's safety filter may reject some prompts β€” you'll get a <code class="font-mono bg-amber-500/10 px-1 rounded">400</code> with a clear error.
399
+ </div>
400
+ </section>
401
+
402
+ <!-- GEMINI 3 IMAGE -->
403
+ <section id="gemini3-image">
404
+ <div class="flex items-center gap-2 text-xs font-mono uppercase tracking-wider text-fuchsia-300 mb-2">
405
+ <span class="w-1.5 h-1.5 rounded-full bg-fuchsia-300"></span>
406
+ Image generation
407
+ </div>
408
+ <h2 class="text-2xl font-semibold text-white mb-2">Gemini 3 β€” image</h2>
409
+ <p class="text-gray-400 mb-6">
410
+ Models:
411
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3-image</code>,
412
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3-flash-image</code>,
413
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3.1-flash-image</code>.
414
+ Append <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">-pro</code> to any of them to switch to the slower, higher-quality pipeline (~2 min).
415
+ </p>
416
+
417
+ <h3 class="text-sm font-medium text-white uppercase tracking-wider mb-3">Supported parameters</h3>
418
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden mb-6">
419
+ <table class="w-full text-sm">
420
+ <thead class="bg-gray-900/50 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">
421
+ <tr>
422
+ <th class="text-left px-4 py-3">Field</th>
423
+ <th class="text-left px-4 py-3">Type</th>
424
+ <th class="text-left px-4 py-3">Default</th>
425
+ <th class="text-left px-4 py-3">Notes</th>
426
+ </tr>
427
+ </thead>
428
+ <tbody class="divide-y divide-gray-800 text-gray-300">
429
+ <tr><td class="px-4 py-3 font-mono text-amber-300">aspect_ratio</td><td class="px-4 py-3 text-gray-400 font-mono">"1:1" | "16:9" | "9:16" | "4:3" | "3:4"</td><td class="px-4 py-3 text-gray-500 font-mono">"1:1"</td><td class="px-4 py-3 text-gray-400">Output image shape.</td></tr>
430
+ </tbody>
431
+ </table>
432
+ </div>
433
+
434
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
435
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">curl</div>
436
+ <pre class="px-4 py-4">curl <span class="tk-str">"<span class="base-url-inline">https://your-apiarium.example.com</span>/v1/chat/completions"</span> \
437
+ -H <span class="tk-str">"Authorization: Bearer api-xxxxxxxxxxxxxxxx"</span> \
438
+ -H <span class="tk-str">"Content-Type: application/json"</span> \
439
+ -d <span class="tk-str">'{
440
+ "model": "gemini-3-image",
441
+ "messages": [{"role":"user","content":"a glass of orange juice on a sunny kitchen counter"}],
442
+ "aspect_ratio": "1:1"
443
+ }'</span></pre>
444
+ </div>
445
+ </section>
446
+
447
+ <!-- GEMINI 3 TEXT -->
448
+ <section id="gemini3-text">
449
+ <h2 class="text-2xl font-semibold text-white mb-2">Gemini 3 β€” text (RAG)</h2>
450
+ <p class="text-gray-400 mb-4">
451
+ Models: <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3</code>,
452
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3-flash</code>,
453
+ <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gemini-3-pro</code>.
454
+ </p>
455
+ <p class="text-sm text-gray-500 mb-6">
456
+ These route to Gemini Hub's RAG-grounded <code class="font-mono">/query</code> endpoint. For free-form chat without a knowledge base, prefer
457
+ <code class="font-mono text-amber-300">gemini-2.5-pro</code>, <code class="font-mono text-amber-300">gemini-3.1-pro</code> or one of the GPT/Claude aliases.
458
+ </p>
459
+ </section>
460
+
461
+ <!-- /v1/images -->
462
+ <section id="images-openai">
463
+ <h2 class="text-2xl font-semibold text-white mb-2">OpenAI <code class="font-mono text-amber-300">/v1/images/generations</code></h2>
464
+ <p class="text-gray-400 mb-6">For OpenAI SDK compatibility we also expose a direct image endpoint backed by <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gpt-image-1</code> / <code class="font-mono bg-gray-900 px-1.5 py-0.5 rounded text-amber-300">gpt-image-2</code>. Returns OpenAI-shaped <code class="font-mono">{"data": [{"url": "…"}]}</code>.</p>
465
+
466
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
467
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">Request</div>
468
+ <pre class="px-4 py-4">POST /v1/images/generations
469
+ {
470
+ <span class="tk-key">"model"</span>: <span class="tk-str">"gpt-image-1"</span>,
471
+ <span class="tk-key">"prompt"</span>: <span class="tk-str">"a watercolor of Istanbul rooftops"</span>,
472
+ <span class="tk-key">"n"</span>: <span class="tk-num">1</span>,
473
+ <span class="tk-key">"size"</span>: <span class="tk-str">"1024x1024"</span>
474
+ }</pre>
475
+ </div>
476
+ </section>
477
+
478
+ <!-- ERRORS -->
479
+ <section id="errors">
480
+ <h2 class="text-2xl font-semibold text-white mb-2">Errors &amp; limits</h2>
481
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
482
+ <table class="w-full text-sm">
483
+ <thead class="bg-gray-900/50 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500">
484
+ <tr>
485
+ <th class="text-left px-4 py-3 w-20">Status</th>
486
+ <th class="text-left px-4 py-3">Meaning</th>
487
+ <th class="text-left px-4 py-3">Fix</th>
488
+ </tr>
489
+ </thead>
490
+ <tbody class="divide-y divide-gray-800 text-gray-300">
491
+ <tr><td class="px-4 py-3 font-mono text-red-400">401</td> <td class="px-4 py-3">IP mismatch or missing/invalid key</td><td class="px-4 py-3 text-gray-400">Hit <a class="text-amber-300 hover:underline" href="/v1/keys/me">/v1/keys/me</a> from your real IP.</td></tr>
492
+ <tr><td class="px-4 py-3 font-mono text-amber-400">403</td><td class="px-4 py-3">VPN / proxy detected</td> <td class="px-4 py-3 text-gray-400">Disable VPN, retry.</td></tr>
493
+ <tr><td class="px-4 py-3 font-mono text-amber-400">429</td><td class="px-4 py-3">Rate limit exceeded (4 RPM)</td> <td class="px-4 py-3 text-gray-400">Back off &amp; retry; <code class="font-mono">content</code> field shows wait time.</td></tr>
494
+ <tr><td class="px-4 py-3 font-mono text-red-400">502</td> <td class="px-4 py-3">Upstream provider unreachable</td> <td class="px-4 py-3 text-gray-400">Try again or pick another model.</td></tr>
495
+ </tbody>
496
+ </table>
497
+ </div>
498
+ </section>
499
+
500
+ <!-- SDK EXAMPLES -->
501
+ <section id="sdks">
502
+ <h2 class="text-2xl font-semibold text-white mb-6">SDK examples</h2>
503
+
504
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
505
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
506
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500 flex items-center justify-between">
507
+ <span>🐍 Python · openai</span>
508
+ <button class="copy-btn text-[10px] font-mono uppercase tracking-wider text-gray-500 hover:text-white transition-colors px-2 py-1 rounded border border-gray-800">Copy</button>
509
+ </div>
510
+ <pre class="px-4 py-4"><span class="tk-kw">from</span> openai <span class="tk-kw">import</span> OpenAI
511
+
512
+ client = <span class="tk-fn">OpenAI</span>(
513
+ base_url=<span class="tk-str">"<span class="base-url-inline">https://your-apiarium.example.com</span>/v1"</span>,
514
+ api_key=<span class="tk-str">"api-xxxxxxxxxxxxxxxx"</span>,
515
+ )
516
+
517
+ <span class="tk-cmt"># 1) chat</span>
518
+ chat = client.chat.completions.<span class="tk-fn">create</span>(
519
+ model=<span class="tk-str">"gpt-5"</span>,
520
+ messages=[{<span class="tk-str">"role"</span>: <span class="tk-str">"user"</span>, <span class="tk-str">"content"</span>: <span class="tk-str">"hi"</span>}],
521
+ )
522
+ <span class="tk-fn">print</span>(chat.choices[<span class="tk-num">0</span>].message.content)
523
+
524
+ <span class="tk-cmt"># 2) video (Veo 3)</span>
525
+ vid = client.chat.completions.<span class="tk-fn">create</span>(
526
+ model=<span class="tk-str">"veo-3"</span>,
527
+ messages=[{<span class="tk-str">"role"</span>: <span class="tk-str">"user"</span>, <span class="tk-str">"content"</span>: <span class="tk-str">"a red apple rotating on white"</span>}],
528
+ extra_body={<span class="tk-str">"duration_seconds"</span>: <span class="tk-str">"8"</span>,
529
+ <span class="tk-str">"resolution"</span>: <span class="tk-str">"1080p"</span>,
530
+ <span class="tk-str">"aspect_ratio"</span>: <span class="tk-str">"16:9"</span>},
531
+ timeout=<span class="tk-num">600</span>,
532
+ )
533
+ <span class="tk-fn">print</span>(vid.choices[<span class="tk-num">0</span>].message.content)</pre>
534
+ </div>
535
+
536
+ <div class="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden">
537
+ <div class="px-4 py-2.5 border-b border-gray-800 text-[10px] font-mono uppercase tracking-wider text-gray-500 flex items-center justify-between">
538
+ <span>🟨 JavaScript · openai</span>
539
+ <button class="copy-btn text-[10px] font-mono uppercase tracking-wider text-gray-500 hover:text-white transition-colors px-2 py-1 rounded border border-gray-800">Copy</button>
540
+ </div>
541
+ <pre class="px-4 py-4"><span class="tk-kw">import</span> OpenAI <span class="tk-kw">from</span> <span class="tk-str">"openai"</span>;
542
+
543
+ <span class="tk-kw">const</span> client = <span class="tk-kw">new</span> <span class="tk-fn">OpenAI</span>({
544
+ baseURL: <span class="tk-str">"<span class="base-url-inline">https://your-apiarium.example.com</span>/v1"</span>,
545
+ apiKey: <span class="tk-str">"api-xxxxxxxxxxxxxxxx"</span>,
546
+ });
547
+
548
+ <span class="tk-kw">const</span> vid = <span class="tk-kw">await</span> client.chat.completions.<span class="tk-fn">create</span>({
549
+ model: <span class="tk-str">"veo-3"</span>,
550
+ messages: [{ role: <span class="tk-str">"user"</span>,
551
+ content: <span class="tk-str">"a serene mountain sunset timelapse"</span> }],
552
+ duration_seconds: <span class="tk-str">"8"</span>,
553
+ resolution: <span class="tk-str">"1080p"</span>,
554
+ aspect_ratio: <span class="tk-str">"16:9"</span>,
555
+ });
556
+
557
+ <span class="tk-fn">console.log</span>(vid.choices[<span class="tk-num">0</span>].message.content);</pre>
558
+ </div>
559
+ </div>
560
+ </section>
561
+
562
+ </article>
563
+ </div>
564
+
565
+ </main>
566
+
567
+ <!-- Footer -->
568
+ <footer class="border-t border-gray-900 bg-black mt-20 relative z-10">
569
+ <div class="max-w-7xl mx-auto px-6 py-8 flex flex-col sm:flex-row justify-between items-center gap-4">
570
+ <div class="flex items-center gap-2 text-gray-500 text-sm">
571
+ <span class="text-base">🐝</span>
572
+ &copy; 2026 APIarium Systems. All rights reserved.
573
+ </div>
574
+ <div class="flex items-center gap-4 text-sm text-gray-500">
575
+ <a href="/" class="hover:text-gray-300 transition-colors">Home</a>
576
+ <a href="/openapi-docs" class="hover:text-gray-300 transition-colors">Swagger</a>
577
+ <a href="/openapi.json" class="hover:text-gray-300 transition-colors">OpenAPI JSON</a>
578
+ </div>
579
+ </div>
580
+ </footer>
581
+
582
+ <div id="toast" class="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 bg-white text-black px-4 py-3 rounded-lg shadow-2xl font-medium text-sm flex items-center gap-2 hidden">
583
+ <svg class="w-5 h-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
584
+ <span id="toast-text">Copied to clipboard</span>
585
+ </div>
586
+
587
+ <script>
588
+ // ── Auto-inject the current host into every "base-url" placeholder ─
589
+ const origin = window.location.origin;
590
+ document.querySelectorAll('#base-url-display, #base-url-1, .base-url-inline').forEach(el => el.textContent = origin);
591
+
592
+ // ── Copy buttons ──────────────────────────────────────────────────
593
+ function flash(msg) {
594
+ const t = document.getElementById('toast');
595
+ const txt = document.getElementById('toast-text');
596
+ txt.textContent = msg || 'Copied to clipboard';
597
+ t.classList.remove('hidden');
598
+ t.classList.add('toast-enter');
599
+ setTimeout(() => { t.classList.add('hidden'); t.classList.remove('toast-enter'); }, 1800);
600
+ }
601
+ function copy(text, label) {
602
+ const done = () => flash(label || 'Copied');
603
+ if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done);
604
+ else {
605
+ const ta = document.createElement('textarea');
606
+ ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px';
607
+ document.body.appendChild(ta); ta.select();
608
+ try { document.execCommand('copy'); done(); } catch (e) {}
609
+ ta.remove();
610
+ }
611
+ }
612
+ document.getElementById('copy-base-url').addEventListener('click', () => copy(origin, 'Base URL copied'));
613
+ document.querySelectorAll('.copy-btn').forEach(btn => btn.addEventListener('click', (e) => {
614
+ const pre = e.target.closest('div').nextElementSibling || e.target.closest('div').parentElement.querySelector('pre');
615
+ if (pre) copy(pre.textContent.trim(), 'Snippet copied');
616
+ }));
617
+
618
+ // ── TOC active-link tracking via IntersectionObserver ─────────────
619
+ const tocLinks = document.querySelectorAll('.toc-link');
620
+ const sections = Array.from(tocLinks).map(a => document.querySelector(a.getAttribute('href'))).filter(Boolean);
621
+ const obs = new IntersectionObserver((entries) => {
622
+ entries.forEach(e => {
623
+ if (e.isIntersecting) {
624
+ tocLinks.forEach(l => l.classList.remove('active'));
625
+ const active = document.querySelector('.toc-link[href="#' + e.target.id + '"]');
626
+ if (active) active.classList.add('active');
627
+ }
628
+ });
629
+ }, { rootMargin: '-30% 0px -60% 0px' });
630
+ sections.forEach(s => obs.observe(s));
631
+
632
+ // ── Models catalog (live from /v1/models) ─────────────────────────
633
+ const grid = document.getElementById('docs-models-grid');
634
+ const badge = document.getElementById('docs-models-badge');
635
+ let allModels = [];
636
+ let currentFilter = 'all';
637
+
638
+ const TYPE_RULES = [
639
+ { key: 'video', test: id => /^veo-/i.test(id) },
640
+ { key: 'image', test: id => /image/i.test(id) || /^gpt-image-/i.test(id) },
641
+ { key: 'chat', test: () => true }, // catch-all last
642
+ ];
643
+ function typeOf(id) {
644
+ for (const r of TYPE_RULES) if (r.test(id)) return r.key;
645
+ return 'chat';
646
+ }
647
+ function badgeClass(t) {
648
+ if (t === 'video') return 'bg-amber-500/10 text-amber-300 border-amber-500/30';
649
+ if (t === 'image') return 'bg-fuchsia-500/10 text-fuchsia-300 border-fuchsia-500/30';
650
+ return 'bg-blue-500/10 text-blue-300 border-blue-500/30';
651
+ }
652
+
653
+ function renderModels() {
654
+ const filtered = allModels.filter(m => currentFilter === 'all' || typeOf(m.id) === currentFilter);
655
+ badge.textContent = `${filtered.length} of ${allModels.length} model${allModels.length === 1 ? '' : 's'}`;
656
+ if (filtered.length === 0) {
657
+ grid.innerHTML = `<div class="col-span-full py-10 text-center text-gray-500 text-sm font-mono">No models match this filter.</div>`;
658
+ return;
659
+ }
660
+ grid.innerHTML = filtered.map(m => {
661
+ const t = typeOf(m.id);
662
+ return `
663
+ <button onclick="copy('${m.id.replace(/'/g, "&#39;")}', 'Model id copied')"
664
+ class="text-left bg-gray-900/50 border border-gray-800 hover:border-amber-500/40 rounded-xl p-4 transition-colors group">
665
+ <div class="flex items-start justify-between gap-2 mb-2">
666
+ <code class="font-mono text-sm text-gray-100 group-hover:text-amber-200 truncate">${m.id}</code>
667
+ <span class="shrink-0 px-2 py-0.5 text-[10px] font-mono border rounded ${badgeClass(t)}">${t}</span>
668
+ </div>
669
+ <div class="text-[10px] font-mono uppercase tracking-wider text-gray-500 truncate">${m.owned_by || 'system'}</div>
670
+ </button>`;
671
+ }).join('');
672
+ }
673
+
674
+ document.querySelectorAll('.model-filter').forEach(btn => {
675
+ btn.addEventListener('click', (e) => {
676
+ document.querySelectorAll('.model-filter').forEach(b => b.classList.remove('active-filter', 'border-gray-700', 'text-gray-200'));
677
+ document.querySelectorAll('.model-filter').forEach(b => b.classList.add('border-gray-800', 'text-gray-400'));
678
+ e.currentTarget.classList.add('active-filter', 'border-gray-700', 'text-gray-200');
679
+ e.currentTarget.classList.remove('border-gray-800', 'text-gray-400');
680
+ currentFilter = e.currentTarget.getAttribute('data-filter');
681
+ renderModels();
682
+ });
683
+ });
684
+
685
+ (async function loadModels() {
686
+ try {
687
+ const r = await fetch('/v1/models');
688
+ const d = await r.json();
689
+ allModels = (d.data || []).sort((a, b) => a.id.localeCompare(b.id));
690
+ renderModels();
691
+ } catch (e) {
692
+ grid.innerHTML = `<div class="col-span-full py-10 text-center text-red-400 text-sm font-mono">Failed to load model registry.</div>`;
693
+ badge.textContent = 'error';
694
+ }
695
+ })();
696
+ </script>
697
+
698
+ </body>
699
+ </html>