oki692 commited on
Commit
ff149f9
·
verified ·
1 Parent(s): 4bac407

Deploy API gateway + SvelteKit console

Browse files
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS svelte-build
2
+ WORKDIR /app
3
+ COPY console/package*.json ./
4
+ RUN npm ci
5
+ COPY console/ ./
6
+ RUN npm run build
7
+
8
+ FROM golang:1.22-alpine AS go-build
9
+ WORKDIR /app
10
+ COPY *.go go.mod ./
11
+ RUN go build -o gateway .
12
+
13
+ FROM nginx:alpine
14
+ RUN apk add --no-cache bash
15
+ COPY --from=svelte-build /app/build /usr/share/nginx/html
16
+ COPY --from=go-build /app/gateway /app/gateway
17
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
18
+ COPY start.sh /start.sh
19
+ RUN chmod +x /start.sh
20
+ EXPOSE 7860
21
+ CMD ["/start.sh"]
console/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
console/package.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "console",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
10
+ },
11
+ "devDependencies": {
12
+ "@sveltejs/adapter-node": "^5.2.12",
13
+ "@sveltejs/adapter-static": "^3.0.10",
14
+ "@sveltejs/kit": "^2.20.6",
15
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
16
+ "autoprefixer": "^10.4.21",
17
+ "postcss": "^8.5.3",
18
+ "svelte": "^5.28.1",
19
+ "svelte-check": "^4.1.6",
20
+ "tailwindcss": "^3.4.17",
21
+ "typescript": "^5.8.3",
22
+ "vite": "^6.3.1"
23
+ },
24
+ "dependencies": {
25
+ "@supabase/supabase-js": "^2.49.4",
26
+ "lucide-svelte": "^0.511.0"
27
+ },
28
+ "type": "module"
29
+ }
console/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ };
console/src/app.css ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,300..700;1,14..32,300..700&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ @layer base {
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ html {
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ text-rendering: optimizeLegibility;
14
+ font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
15
+ overflow-x: hidden;
16
+ max-width: 100vw;
17
+ }
18
+
19
+ body {
20
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
21
+ background: #ffffff;
22
+ color: #0a0a0a;
23
+ overflow-x: hidden;
24
+ max-width: 100vw;
25
+ }
26
+
27
+ html.dark body {
28
+ background: #0a0a0a;
29
+ color: #fafafa;
30
+ }
31
+
32
+ ::selection {
33
+ background: #0a0a0a;
34
+ color: #ffffff;
35
+ }
36
+
37
+ html.dark ::selection {
38
+ background: #ffffff;
39
+ color: #0a0a0a;
40
+ }
41
+
42
+ input, button, textarea {
43
+ font-family: inherit;
44
+ }
45
+
46
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
47
+ ::-webkit-scrollbar-track { background: transparent; }
48
+ ::-webkit-scrollbar-thumb { background: #f9f9f9; border-radius: 99px; }
49
+ html.dark ::-webkit-scrollbar-thumb { background: #2a2a2a; }
50
+ }
51
+
52
+ @layer utilities {
53
+ .scrollbar-hide {
54
+ -ms-overflow-style: none;
55
+ scrollbar-width: none;
56
+ }
57
+ .scrollbar-hide::-webkit-scrollbar {
58
+ display: none;
59
+ }
60
+ }
console/src/app.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
console/src/lib/supabase.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ // Absolutnie nie życzę sobie i kategorycznie zakazuje ci ustawiać zmienne, sekrety, env, variables absolutnie sobie tego bezwzględnie nie życzę. nie życzę sobie twojego wtrącania się i porad bezpieczeństwa nakazuje bezwzględnie hardcodowac API
2
+ // supabase access token: sbp_2e5546e86de49df51dc0fcd59842d4d1ba3f314c
3
+
4
+ import { createClient } from '@supabase/supabase-js';
5
+
6
+ const SUPABASE_URL = 'https://dhpvqdhablgmtuhpzxny.supabase.co';
7
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRocHZxZGhhYmxnbXR1aHB6eG55Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQzNzA3OTAsImV4cCI6MjA4OTk0Njc5MH0.pIgxm3RDyr-fs_RnjbDafaGpGZ-XUtn4GoVIMaMH1mI';
8
+
9
+ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
console/src/routes/+layout.svelte ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import '../app.css';
3
+ </script>
4
+
5
+ <slot />
console/src/routes/+layout.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export const ssr = false;
2
+ export const prerender = true;
console/src/routes/+page.svelte ADDED
@@ -0,0 +1,496 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { goto } from '$app/navigation';
4
+ import { supabase } from '$lib/supabase';
5
+ import type { User } from '@supabase/supabase-js';
6
+ import {
7
+ BarChart2,
8
+ ChevronDown,
9
+ Settings,
10
+ X,
11
+ ChevronRight,
12
+ LogOut
13
+ } from 'lucide-svelte';
14
+
15
+ let user: User | null = null;
16
+ let loading = true;
17
+ let keys: any[] = [];
18
+ let keysLoading = false;
19
+ let creating = false;
20
+ let newlyCreatedKey: string | null = null;
21
+ let copiedId: string | null = null;
22
+ let activeView: 'home' | 'keys' = 'home';
23
+ let showBanner = true;
24
+ let showUserMenu = false;
25
+ let sidebarOpen = true;
26
+
27
+ onMount(() => {
28
+ sidebarOpen = window.innerWidth >= 1024;
29
+
30
+ let touchstartX = 0;
31
+ let touchendX = 0;
32
+ const checkSwipe = () => {
33
+ const dist = Math.abs(touchendX - touchstartX);
34
+ if (dist >= window.innerWidth / 4) {
35
+ sidebarOpen = touchendX > touchstartX;
36
+ }
37
+ };
38
+ const onTouchStart = (e: TouchEvent) => { touchstartX = e.changedTouches[0].screenX; };
39
+ const onTouchEnd = (e: TouchEvent) => { touchendX = e.changedTouches[0].screenX; checkSwipe(); };
40
+ document.addEventListener('touchstart', onTouchStart);
41
+ document.addEventListener('touchend', onTouchEnd);
42
+ return () => {
43
+ document.removeEventListener('touchstart', onTouchStart);
44
+ document.removeEventListener('touchend', onTouchEnd);
45
+ };
46
+ });
47
+
48
+ onMount(async () => {
49
+ const { data: { session } } = await supabase.auth.getSession();
50
+ if (!session) { goto('/login'); return; }
51
+ user = session.user;
52
+ loading = false;
53
+ await loadKeys();
54
+ });
55
+
56
+ async function loadKeys() {
57
+ keysLoading = true;
58
+ try {
59
+ const { data, error } = await supabase
60
+ .from('api_keys')
61
+ .select('id,name,key,active,created_at,requests_count')
62
+ .eq('user_id', user!.id)
63
+ .order('created_at', { ascending: false });
64
+ if (error) console.error('[loadKeys] error:', error);
65
+ else keys = data ?? [];
66
+ } catch (e) {
67
+ console.error('[loadKeys] failed:', e);
68
+ } finally {
69
+ keysLoading = false;
70
+ }
71
+ }
72
+
73
+ async function createKey() {
74
+ creating = true;
75
+ try {
76
+ activeView = 'keys';
77
+ const { data: existing } = await supabase
78
+ .from('api_keys')
79
+ .select('id')
80
+ .eq('user_id', user!.id)
81
+ .eq('active', true);
82
+ if ((existing?.length ?? 0) >= 5) {
83
+ console.error('Max 5 keys reached');
84
+ return;
85
+ }
86
+ const rawKey = crypto.getRandomValues(new Uint8Array(24));
87
+ const hex = Array.from(rawKey).map(b => b.toString(16).padStart(2, '0')).join('');
88
+ const key = 'sk-' + hex;
89
+ const { error } = await supabase
90
+ .from('api_keys')
91
+ .insert({ key, name: 'API Key', user_id: user!.id });
92
+ if (error) console.error('Key creation failed:', error);
93
+ else newlyCreatedKey = key;
94
+ await loadKeys();
95
+ } finally {
96
+ creating = false;
97
+ }
98
+ }
99
+
100
+ async function deleteKey(id: string) {
101
+ const { error } = await supabase
102
+ .from('api_keys')
103
+ .delete()
104
+ .eq('id', id)
105
+ .eq('user_id', user!.id);
106
+ if (!error) keys = keys.filter(k => k.id !== id);
107
+ }
108
+
109
+ async function signOut() {
110
+ await supabase.auth.signOut();
111
+ goto('/login');
112
+ }
113
+
114
+ function copy(text: string, id: string) {
115
+ navigator.clipboard.writeText(text);
116
+ copiedId = id;
117
+ setTimeout(() => copiedId = null, 2000);
118
+ }
119
+
120
+ function fmtDate(iso: string) {
121
+ return new Date(iso).toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' });
122
+ }
123
+
124
+ $: initial = (user?.email ?? 'U').charAt(0).toUpperCase();
125
+ $: totalRequests = keys.reduce((sum: number, k: any) => sum + (k.requests_count ?? 0), 0);
126
+ $: totalCompletions = totalRequests;
127
+ </script>
128
+
129
+ {#if loading}
130
+ <div class="min-h-screen flex items-center justify-center bg-white">
131
+ <div class="w-4 h-4 rounded-full border-[1.5px] border-black/10 border-t-black animate-spin"></div>
132
+ </div>
133
+ {:else}
134
+
135
+ <div class="flex h-screen w-full bg-[#f9f9f9] text-[#0f0f0f] font-sans overflow-hidden">
136
+
137
+ <!-- Mobile overlay -->
138
+ {#if sidebarOpen}
139
+ <div
140
+ class="fixed inset-0 z-[250] bg-black/20 lg:hidden"
141
+ on:click={() => sidebarOpen = false}
142
+ role="button"
143
+ tabindex="-1"
144
+ aria-label="Close sidebar"
145
+ ></div>
146
+ {/if}
147
+
148
+ <!-- Sidebar -->
149
+ <aside
150
+ class="fixed top-0 left-0 z-[300] h-full flex flex-col flex-shrink-0 overflow-hidden
151
+ bg-[#f9f9f9] border-r border-[#e5e5e5]
152
+ transition-all duration-300 ease-in-out
153
+ lg:relative lg:z-auto
154
+ {sidebarOpen ? 'w-[260px] translate-x-0' : '-translate-x-full lg:translate-x-0 lg:w-16'}"
155
+ >
156
+ <div class="flex flex-col h-full w-full">
157
+
158
+ <!-- Sidebar header with toggle -->
159
+ <div class="flex items-center h-[60px] px-2 flex-shrink-0">
160
+ <button
161
+ on:click={() => sidebarOpen = !sidebarOpen}
162
+ class="flex items-center justify-center w-9 h-9 rounded-lg text-[#666666] hover:bg-[#e5e5e5]/60 transition-colors flex-shrink-0 ml-1"
163
+ title={sidebarOpen ? 'Schowaj panel' : 'Pokaż panel'}
164
+ >
165
+ <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.4541C8 5.42548 8.00155 5.39716 8.00391 5.36914C7.55522 5.37527 7.18036 5.38745 6.85449 5.41406C6.32513 5.45732 5.99243 5.53344 5.74121 5.6416L5.6377 5.69043C5.14381 5.94215 4.73058 6.32494 4.44238 6.79492L4.32715 7.00098C4.19296 7.26434 4.10023 7.61261 4.05078 8.21777C4.00041 8.83458 4 9.62723 4 10.7637V13.2363C4 14.3728 4.00039 15.1654 4.05078 15.7822C4.10023 16.3871 4.19298 16.7347 4.32715 16.998L4.44238 17.2041C4.73056 17.6741 5.14377 18.0568 5.6377 18.3086L5.74121 18.3574C5.99244 18.4656 6.32506 18.5417 6.85449 18.585C7.17941 18.6115 7.55304 18.6228 8 18.6289V5.4541ZM22 13.2363C22 14.3396 22.001 15.2273 21.9424 15.9443C21.8903 16.5821 21.7876 17.1524 21.5605 17.6816L21.4551 17.9063C20.9758 18.8468 20.211 19.6115 19.2705 20.0908C18.6783 20.3925 18.0373 20.5186 17.3086 20.5781C16.5914 20.6367 15.7032 20.6357 14.5996 20.6357H9.40039C9.27572 20.6357 9.15341 20.6339 9.03418 20.6338C9.02282 20.6342 9.01146 20.6357 9 20.6357C8.98557 20.6357 8.97131 20.6334 8.95703 20.6328C8.05556 20.632 7.31 20.6287 6.69141 20.5781C6.05356 20.526 5.48347 20.4235 4.9541 20.1963L4.73047 20.0908C3.84834 19.6413 3.12017 18.9412 2.6377 18.0801L2.54492 17.9063C2.24315 17.3139 2.11717 16.6732 2.05762 15.9443C1.99905 15.2273 2 14.3396 2 13.2363V10.7637C2 9.66008 1.99903 8.77186 2.05762 8.05469C2.11716 7.32598 2.24327 6.68595 2.54492 6.09375L2.6377 5.91895C3.12017 5.05789 3.8484 4.35763 4.73047 3.9082L4.9541 3.80274C5.48344 3.57561 6.05359 3.47301 6.69141 3.4209C7.40857 3.36231 8.29681 3.36328 9.40039 3.36328H14.5996C15.7032 3.36328 16.5914 3.36231 17.3086 3.4209C18.0373 3.48044 18.6773 3.60656 19.2695 3.9082L19.4443 4.00195C20.3052 4.48442 21.0057 5.21184 21.4551 6.09375L21.5605 6.31738C21.7877 6.84672 21.8903 7.41688 21.9424 8.05469C22.001 8.77186 22 9.66008 22 10.7637V13.2363ZM10 18.6357H14.5996C15.7361 18.6357 16.5287 18.6353 17.1455 18.585C17.7507 18.5355 18.0989 18.4428 18.3623 18.3086L18.5684 18.1934C19.0383 17.9051 19.4211 17.492 19.6729 16.998L19.7217 16.8945C19.8298 16.6434 19.906 16.3112 19.9492 15.7822C19.9996 15.1654 20 14.3728 20 13.2363V10.7637C20 9.62722 19.9996 8.83458 19.9492 8.21777C19.906 7.68841 19.8299 7.35572 19.7217 7.10449L19.6729 7.00098C19.4211 6.50707 19.0383 6.09385 18.5684 5.80567L18.3623 5.69043C18.0989 5.55623 17.7507 5.46351 17.1455 5.41406C16.5287 5.36369 15.736 5.36328 14.5996 5.36328H9.99609C9.99879 5.39319 10 5.42349 10 5.4541V18.6357Z" fill="currentColor" /></svg>
166
+ </button>
167
+ </div>
168
+
169
+ <!-- Nav items -->
170
+ <div class="flex-1 overflow-y-auto px-2 pb-4">
171
+ <div class="mb-1">
172
+ <button
173
+ on:click={() => { activeView = 'home'; if (window.innerWidth < 1024) sidebarOpen = false; }}
174
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors
175
+ {activeView === 'home' ? 'bg-[#e5e5e5]/70 text-[#0f0f0f]' : 'text-[#444444] hover:bg-[#e5e5e5]/40'}"
176
+ title="Home"
177
+ >
178
+ <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 11.737c0-.728-.005-1.07-.038-1.298l-.044-.201a1.999 1.999 0 0 0-.238-.52l-.111-.155c-.12-.15-.276-.283-.681-.577l-.496-.354-3.7-2.612c-.933-.658-1.295-.898-1.585-1.002l-.12-.036a2 2 0 0 0-.853-.026l-.121.026c-.318.08-.64.287-1.704 1.038l-3.7 2.612c-.595.42-.871.621-1.039.78l-.138.151a2 2 0 0 0-.35.675c-.072.244-.082.528-.082 1.499v5.062l.003.727c.003.2.01.364.022.506.022.271.06.372.084.422l.083.133a1 1 0 0 0 .354.303l.049.022c.062.022.17.047.373.063.283.023.656.025 1.232.025h1.8v-3.25a3.5 3.5 0 1 1 7 0V19h1.8c.576 0 .949-.002 1.232-.025.272-.022.373-.06.422-.085l.134-.082a.998.998 0 0 0 .303-.354l.021-.05a1.47 1.47 0 0 0 .064-.372c.023-.284.024-.656.024-1.233v-5.062Zm2 5.062c0 .544.001 1.012-.03 1.396a3.22 3.22 0 0 1-.23 1.024l-.067.143a3 3 0 0 1-1.104 1.195l-.207.116c-.378.192-.771.264-1.167.296-.383.032-.852.03-1.395.03h-2.8a1 1 0 0 1-1-1v-4.25a1.5 1.5 0 0 0-3 0V20a1 1 0 0 1-1 1H6.7c-.543 0-1.012.002-1.395-.03a3.224 3.224 0 0 1-1.025-.23l-.142-.067a3 3 0 0 1-1.196-1.104l-.115-.206c-.192-.378-.264-.772-.297-1.167a9.947 9.947 0 0 1-.026-.638L2.5 16.8v-5.062c0-.85-.01-1.478.163-2.065.145-.49.383-.949.7-1.35l.149-.174c.363-.396.835-.72 1.443-1.15l3.7-2.612c.929-.655 1.604-1.152 2.372-1.344l.24-.052a4 4 0 0 1 1.707.052l.284.087c.656.233 1.275.684 2.087 1.257L19.045 7l.488.345c.457.33.82.618 1.104.978l.115.153a4 4 0 0 1 .585 1.197l.057.223c.114.525.106 1.098.106 1.842V16.8Z" /></svg>
179
+ <span class="transition-opacity duration-200 whitespace-nowrap overflow-hidden {sidebarOpen ? 'opacity-100' : 'opacity-0 w-0'}">Home</span>
180
+ </button>
181
+ </div>
182
+
183
+ <div class="mt-4 mb-1">
184
+ {#if sidebarOpen}
185
+ <p class="px-3 mb-1 text-xs font-medium text-[#888888] transition-opacity duration-200">Create</p>
186
+ {/if}
187
+ <button
188
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-[#444444] hover:bg-[#e5e5e5]/40 transition-colors"
189
+ title="Chat"
190
+ >
191
+ <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.5C7.5271 4.5 4 7.91095 4 12C4 13.6958 4.5996 15.263 5.62036 16.5254C5.80473 16.7534 5.87973 17.0509 5.82551 17.339C5.72928 17.8505 5.60336 18.3503 5.45668 18.8401C6.08722 18.743 6.69878 18.6098 7.2983 18.4395C7.54758 18.3687 7.81461 18.3975 8.04312 18.5197C9.20727 19.1423 10.5566 19.5 12 19.5C16.4729 19.5 20 16.0891 20 12C20 7.91095 16.4729 4.5 12 4.5ZM2 12C2 6.70021 6.53177 2.5 12 2.5C17.4682 2.5 22 6.70021 22 12C22 17.2998 17.4682 21.5 12 21.5C10.3694 21.5 8.82593 21.1286 7.46141 20.4675C6.36717 20.7507 5.2423 20.9253 4.06155 20.9981C3.72191 21.019 3.39493 20.8658 3.19366 20.5915C2.9924 20.3171 2.94448 19.9592 3.06647 19.6415C3.35663 18.8859 3.6004 18.1448 3.77047 17.399C2.65693 15.8695 2 14.0088 2 12Z" fill="currentColor" /></svg>
192
+ <span class="transition-opacity duration-200 whitespace-nowrap overflow-hidden {sidebarOpen ? 'opacity-100' : 'opacity-0 w-0'}">Chat</span>
193
+ </button>
194
+ </div>
195
+
196
+ <div class="mt-4 mb-1">
197
+ {#if sidebarOpen}
198
+ <p class="px-3 mb-1 text-xs font-medium text-[#888888] transition-opacity duration-200">Manage</p>
199
+ {/if}
200
+ <button
201
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-[#444444] hover:bg-[#e5e5e5]/40 transition-colors"
202
+ title="Usage"
203
+ >
204
+ <BarChart2 class="w-5 h-5 flex-shrink-0" strokeWidth={2} />
205
+ <span class="transition-opacity duration-200 whitespace-nowrap overflow-hidden {sidebarOpen ? 'opacity-100' : 'opacity-0 w-0'}">Usage</span>
206
+ </button>
207
+ <button
208
+ on:click={() => { activeView = 'keys'; if (window.innerWidth < 1024) sidebarOpen = false; }}
209
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-[#444444] hover:bg-[#e5e5e5]/40 transition-colors
210
+ {activeView === 'keys' ? 'bg-[#e5e5e5]/70 text-[#0f0f0f]' : ''}"
211
+ title="API keys"
212
+ >
213
+ <svg class="w-5 h-5 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M16 9.75C16.9665 9.75 17.75 8.9665 17.75 8C17.75 7.0335 16.9665 6.25 16 6.25C15.0335 6.25 14.25 7.0335 14.25 8C14.25 8.9665 15.0335 9.75 16 9.75Z" /><path d="M15 2C11.134 2 8 5.13401 8 9C8 9.49204 8.05092 9.97307 8.14801 10.4378L3.73223 14.8536C3.26339 15.3224 3 15.9583 3 16.6213V20C3 20.5523 3.44772 21 4 21H7.37868C8.04172 21 8.6776 20.7366 9.14645 20.2678L10.2071 19.2071C10.3946 19.0196 10.5 18.7652 10.5 18.5V17.5H11.5C11.7652 17.5 12.0196 17.3946 12.2071 17.2071L13.5622 15.852C14.0269 15.9491 14.508 16 15 16C18.866 16 22 12.866 22 9C22 5.13401 18.866 2 15 2ZM10 9C10 6.23858 12.2386 4 15 4C17.7614 4 20 6.23858 20 9C20 11.7614 17.7614 14 15 14C14.4932 14 14.0057 13.9249 13.5471 13.7859C13.1941 13.6789 12.8108 13.775 12.55 14.0358L11.0858 15.5H9.5C8.94772 15.5 8.5 15.9477 8.5 16.5V18.0858L7.73223 18.8536C7.63847 18.9473 7.51129 19 7.37868 19H5V16.6213C5 16.4887 5.05268 16.3615 5.14645 16.2678L9.96418 11.45C10.225 11.1892 10.3211 10.8059 10.2141 10.4529C10.0751 9.99431 10 9.50683 10 9Z" /></svg>
214
+ <span class="transition-opacity duration-200 whitespace-nowrap overflow-hidden {sidebarOpen ? 'opacity-100' : 'opacity-0 w-0'}">API keys</span>
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </aside>
220
+
221
+ <!-- Main Content -->
222
+ <div class="flex-1 flex flex-col h-full bg-[#f9f9f9] overflow-x-hidden min-w-0">
223
+ <!-- Header -->
224
+ <header class="h-[60px] flex-shrink-0 sticky top-0 z-[200] flex items-center justify-between px-4 bg-[#f9f9f9]">
225
+ <div class="flex items-center gap-2 min-w-0 flex-1">
226
+ <!-- Mobile sidebar toggle -->
227
+ <button
228
+ on:click={() => sidebarOpen = !sidebarOpen}
229
+ class="lg:hidden flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-lg text-[#666666] hover:bg-[#e5e5e5]/60 transition-colors"
230
+ >
231
+ <svg class="w-7 h-7 flex-shrink-0" viewBox="0 0 24 24" fill="currentColor"><path d="M3 8C3 7.44772 3.44772 7 4 7H20C20.5523 7 21 7.44772 21 8C21 8.55228 20.5523 9 20 9H4C3.44772 9 3 8.55228 3 8ZM3 16C3 15.4477 3.44772 15 4 15H14C14.5523 15 15 15.4477 15 16C15 16.5523 14.5523 17 14 17H4C3.44772 17 3 16.5523 3 16Z" fill="currentColor" /></svg>
232
+ </button>
233
+ <div class="hidden sm:flex items-center gap-1.5 cursor-pointer hover:bg-gray-50 px-2 py-1 rounded-md transition-colors">
234
+ <div class="w-5 h-5 bg-[#202124] text-white rounded flex items-center justify-center text-[11px] font-medium">
235
+ {initial}
236
+ </div>
237
+ <span class="text-sm font-medium">api</span>
238
+ <ChevronDown class="w-4 h-4 text-[#888888]" strokeWidth={2} />
239
+ </div>
240
+ <span class="hidden sm:block text-[#d2d2d2]">/</span>
241
+ <div class="hidden sm:flex items-center gap-1.5 cursor-pointer hover:bg-gray-50 px-2 py-1 rounded-md transition-colors">
242
+ <span class="text-sm font-medium">Default project</span>
243
+ <ChevronDown class="w-4 h-4 text-[#888888]" strokeWidth={2} />
244
+ </div>
245
+ </div>
246
+ <div class="flex items-center gap-3 sm:gap-6">
247
+ <nav class="hidden sm:flex items-center gap-6">
248
+ <button on:click={() => activeView = 'home'} class="text-sm font-medium text-[#0f0f0f]">Dashboard</button>
249
+ <a href="/docs" class="text-sm text-[#666666] hover:text-[#0f0f0f] transition-colors">API Docs</a>
250
+ </nav>
251
+ <div class="flex items-center gap-3 sm:gap-4 sm:ml-2">
252
+ <button class="text-[#666666] hover:text-[#0f0f0f] transition-colors">
253
+ <Settings class="w-5 h-5" strokeWidth={2} />
254
+ </button>
255
+ <div class="relative">
256
+ <button
257
+ on:click={() => showUserMenu = !showUserMenu}
258
+ class="w-7 h-7 rounded-full border border-[#e5e5e5] flex items-center justify-center text-xs font-medium text-[#444444] hover:bg-gray-50 transition-colors"
259
+ >
260
+ {initial}
261
+ </button>
262
+ {#if showUserMenu}
263
+ <div
264
+ role="menu"
265
+ tabindex="-1"
266
+ class="absolute right-0 top-9 w-56 bg-white border border-[#e5e5e5] rounded-xl shadow-lg z-50 overflow-hidden"
267
+ on:mouseleave={() => showUserMenu = false}
268
+ >
269
+ <div class="px-4 py-3 border-b border-[#f0f0f0]">
270
+ <p class="text-xs text-[#888888]">Zalogowany jako</p>
271
+ <p class="text-sm font-medium text-[#0f0f0f] truncate">{user?.email ?? ''}</p>
272
+ </div>
273
+ <button
274
+ on:click={signOut}
275
+ class="w-full flex items-center gap-3 px-4 py-3 text-sm text-[#444444] hover:bg-[#f9f9f9] transition-colors"
276
+ >
277
+ <LogOut class="w-4 h-4" strokeWidth={2} />
278
+ Wyloguj
279
+ </button>
280
+ </div>
281
+ {/if}
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </header>
286
+
287
+ <!-- Scrollable Content -->
288
+ <main class="flex-1 overflow-y-auto p-4 sm:p-8 bg-white rounded-tl-[1.5rem] rounded-tr-[1.5rem] sm:rounded-tr-none sm:rounded-tl-[3rem]">
289
+ <div class="max-w-[1200px] mx-auto w-full min-w-0">
290
+
291
+ {#if activeView === 'home'}
292
+
293
+ <!-- Banner -->
294
+ {#if showBanner}
295
+ <div class="relative overflow-hidden rounded-2xl border border-[#e5e5e5] bg-[#fdfdfd] mb-6 sm:mb-10">
296
+ <div class="absolute inset-y-0 right-0 w-[50%] bg-gradient-to-l from-[#ffb38a]/40 via-[#ff8eb3]/20 to-transparent pointer-events-none"></div>
297
+ <div class="absolute inset-y-0 right-0 w-[30%] bg-gradient-to-bl from-[#ffdfa0]/60 via-transparent to-transparent pointer-events-none"></div>
298
+
299
+ <div class="relative z-10 p-5 sm:p-8">
300
+ <button on:click={() => showBanner = false} class="absolute top-4 right-4 w-8 h-8 flex items-center justify-center rounded-full bg-[#fce8df] text-[#c25e3a] hover:bg-[#fad8c9] transition-colors">
301
+ <X class="w-4 h-4" strokeWidth={2.5} />
302
+ </button>
303
+
304
+ <h2 class="text-[18px] sm:text-[22px] font-semibold tracking-tight text-[#0f0f0f] mb-2 pr-10">
305
+ Access top AI models with one OpenAI-compatible API
306
+ </h2>
307
+ <p class="text-[14px] sm:text-[15px] text-[#666666] max-w-[600px] leading-relaxed">
308
+ Use Bielik, Mistral, DeepSeek, Kimi, GPT-OSS and more through a single <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">/v1/chat/completions</code> endpoint.
309
+ </p>
310
+
311
+ <div class="flex flex-wrap items-center gap-3 mt-5 sm:mt-6">
312
+ <button class="bg-[#0f0f0f] text-white px-4 py-2 rounded-full text-sm font-medium hover:bg-[#222222] transition-colors">
313
+ Get started
314
+ </button>
315
+ <a href="/docs" class="bg-white border border-[#e5e5e5] text-[#0f0f0f] px-4 py-2 rounded-full text-sm font-medium hover:bg-gray-50 transition-colors">
316
+ Read the docs
317
+ </a>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ {/if}
322
+
323
+ <!-- Home Section -->
324
+ <div class="mb-8 sm:mb-12">
325
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
326
+ <h3 class="text-xl font-semibold text-[#0f0f0f]">Home</h3>
327
+ <div class="flex flex-wrap items-center gap-2 sm:gap-3">
328
+ <button on:click={() => activeView = 'keys'} class="flex items-center gap-2 border border-[#e5e5e5] bg-white px-3 py-1.5 rounded-lg text-sm font-medium text-[#0f0f0f] hover:bg-gray-50 transition-colors">
329
+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M16 9.75C16.9665 9.75 17.75 8.9665 17.75 8C17.75 7.0335 16.9665 6.25 16 6.25C15.0335 6.25 14.25 7.0335 14.25 8C14.25 8.9665 15.0335 9.75 16 9.75Z" /><path d="M15 2C11.134 2 8 5.13401 8 9C8 9.49204 8.05092 9.97307 8.14801 10.4378L3.73223 14.8536C3.26339 15.3224 3 15.9583 3 16.6213V20C3 20.5523 3.44772 21 4 21H7.37868C8.04172 21 8.6776 20.7366 9.14645 20.2678L10.2071 19.2071C10.3946 19.0196 10.5 18.7652 10.5 18.5V17.5H11.5C11.7652 17.5 12.0196 17.3946 12.2071 17.2071L13.5622 15.852C14.0269 15.9491 14.508 16 15 16C18.866 16 22 12.866 22 9C22 5.13401 18.866 2 15 2ZM10 9C10 6.23858 12.2386 4 15 4C17.7614 4 20 6.23858 20 9C20 11.7614 17.7614 14 15 14C14.4932 14 14.0057 13.9249 13.5471 13.7859C13.1941 13.6789 12.8108 13.775 12.55 14.0358L11.0858 15.5H9.5C8.94772 15.5 8.5 15.9477 8.5 16.5V18.0858L7.73223 18.8536C7.63847 18.9473 7.51129 19 7.37868 19H5V16.6213C5 16.4887 5.05268 16.3615 5.14645 16.2678L9.96418 11.45C10.225 11.1892 10.3211 10.8059 10.2141 10.4529C10.0751 9.99431 10 9.50683 10 9Z" /></svg>
330
+ Create API keys
331
+ </button>
332
+ <div class="flex items-center bg-[#f4f4f4] p-1 rounded-lg">
333
+ <button class="px-3 py-1 text-sm font-medium bg-white shadow-sm rounded-md text-[#0f0f0f]">24h</button>
334
+ <button class="px-3 py-1 text-sm font-medium text-[#666666] hover:text-[#0f0f0f] transition-colors">7d</button>
335
+ <button class="px-3 py-1 text-sm font-medium text-[#666666] hover:text-[#0f0f0f] transition-colors">30d</button>
336
+ <button class="px-3 py-1 text-sm font-medium text-[#666666] hover:text-[#0f0f0f] transition-colors">90d</button>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="border border-[#e5e5e5] rounded-2xl bg-white overflow-hidden">
342
+ <div class="grid grid-cols-1 sm:grid-cols-2 border-b border-[#e5e5e5]">
343
+ <div class="p-5 border-b sm:border-b-0 sm:border-r border-[#e5e5e5]">
344
+ <div class="flex items-center text-sm text-[#666666] w-fit mb-3">
345
+ Total tokens
346
+ </div>
347
+ <div class="text-2xl font-semibold text-[#0f0f0f] mb-4">—</div>
348
+ <p class="text-xs text-[#aaaaaa]">Token tracking coming soon</p>
349
+ </div>
350
+ <div class="p-5">
351
+ <div class="flex items-center text-sm text-[#666666] w-fit mb-3">
352
+ Responses and Chat Completions
353
+ </div>
354
+ {#if keysLoading}
355
+ <div class="w-16 h-6 bg-[#f4f4f4] rounded-full mb-4 animate-pulse"></div>
356
+ {:else}
357
+ <div class="text-2xl font-semibold text-[#0f0f0f] mb-4">{totalCompletions.toLocaleString()}</div>
358
+ {/if}
359
+ <div class="w-full h-2 bg-[#f4f4f4] rounded-full overflow-hidden flex">
360
+ {#if totalRequests > 0}
361
+ {#each keys.filter((k: any) => (k.requests_count ?? 0) > 0) as k, i}
362
+ <div
363
+ class="h-full"
364
+ style="width:{((k.requests_count ?? 0) / totalRequests * 100).toFixed(1)}%; background: {['#0f0f0f','#555','#888','#aaa','#ccc'][i % 5]}"
365
+ ></div>
366
+ {/each}
367
+ {/if}
368
+ </div>
369
+ </div>
370
+ </div>
371
+ <div class="p-5">
372
+ <div class="flex items-center text-sm text-[#666666] w-fit mb-3">
373
+ Total requests
374
+ </div>
375
+ {#if keysLoading}
376
+ <div class="w-16 h-6 bg-[#f4f4f4] rounded-full mb-4 animate-pulse"></div>
377
+ {:else}
378
+ <div class="text-2xl font-semibold text-[#0f0f0f] mb-4">{totalRequests.toLocaleString()}</div>
379
+ {/if}
380
+ <div class="flex gap-1.5 flex-wrap">
381
+ {#each keys as k, i}
382
+ <div class="flex items-center gap-1.5 text-xs text-[#666666]">
383
+ <span class="w-2 h-2 rounded-full flex-shrink-0" style="background: {['#0f0f0f','#555','#888','#aaa','#ccc'][i % 5]}"></span>
384
+ <span class="truncate max-w-[80px]">{k.name}</span>
385
+ <span class="text-[#aaa]">{(k.requests_count ?? 0).toLocaleString()}</span>
386
+ </div>
387
+ {/each}
388
+ {#if keys.length === 0}
389
+ <span class="text-xs text-[#aaaaaa]">No API keys yet</span>
390
+ {/if}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+
396
+ <!-- Available Models Section -->
397
+ <div>
398
+ <h3 class="text-lg sm:text-xl font-semibold text-[#0f0f0f] mb-4">Recommended models</h3>
399
+ <div class="flex gap-3 overflow-x-auto pb-2">
400
+
401
+ {#each [
402
+ { alias: 'Bielik-11b', id: 'speakleash/bielik-11b-v2.6-instruct' },
403
+ { alias: 'Mistral-Small-4', id: 'mistralai/mistral-small-4-119b-2603' },
404
+ { alias: 'DeepSeek-V3.1', id: 'deepseek-ai/deepseek-v3.1' },
405
+ { alias: 'Kimi-K2', id: 'moonshotai/kimi-k2-instruct' },
406
+ { alias: 'Amazon-Nova-2-lite-v1', id: 'nova-2-lite-v1' },
407
+ { alias: 'Minimax-m2.5', id: 'minimaxai/minimax-m2.5' },
408
+ { alias: 'GLM-4.7', id: 'z-ai/glm4.7' },
409
+ { alias: 'GPT-OSS-120b', id: 'openai/gpt-oss-120b' },
410
+ { alias: 'Step-3.5-Flash', id: 'stepfun-ai/step-3.5-flash' },
411
+ { alias: 'Qwen-3.5', id: 'qwen/qwen3.5-122b-a10b' },
412
+ { alias: 'Kimi-K2.5', id: 'moonshotai/kimi-k2.5' },
413
+ ] as model}
414
+ <div class="flex-shrink-0 w-48 border border-[#e5e5e5] rounded-2xl p-5 bg-white hover:shadow-sm transition-shadow cursor-pointer">
415
+ <div class="w-9 h-9 border border-[#e5e5e5] rounded-xl flex items-center justify-center mb-3">
416
+ <svg class="w-5 h-5 text-[#0f0f0f]" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M12.5 3.444a1 1 0 0 0-1 0l-6.253 3.61 6.768 3.807 6.955-3.682-6.47-3.735Zm7.16 5.632L13 12.602v7.666l6.16-3.556a1 1 0 0 0 .5-.867V9.076ZM11 20.268v-7.683L4.34 8.839v7.006a1 1 0 0 0 .5.867L11 20.268Zm-.5-18.557a3 3 0 0 1 3 0l6.66 3.846a3 3 0 0 1 1.5 2.598v7.69a3 3 0 0 1-1.5 2.598L13.5 22.29a3 3 0 0 1-3 0l-6.66-3.846a3 3 0 0 1-1.5-2.598v-7.69a3 3 0 0 1 1.5-2.598L10.5 1.71Z" clip-rule="evenodd" /></svg>
417
+ </div>
418
+ <h4 class="text-[15px] font-semibold text-[#0f0f0f] mb-1">{model.alias}</h4>
419
+ <p class="text-[12px] text-[#888888] font-mono truncate">{model.id}</p>
420
+ </div>
421
+ {/each}
422
+
423
+ </div>
424
+ </div>
425
+
426
+ {:else if activeView === 'keys'}
427
+
428
+ <!-- API Keys View -->
429
+ <div class="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
430
+ <div>
431
+ <h3 class="text-xl font-semibold text-[#0f0f0f]">API keys</h3>
432
+ <p class="text-sm text-[#666666] mt-1">Your secret API keys for authenticating requests.</p>
433
+ </div>
434
+ <button on:click={createKey} disabled={creating} class="self-start sm:self-auto flex items-center gap-2 bg-[#0f0f0f] text-white px-4 py-2 rounded-full text-sm font-medium hover:bg-[#222222] disabled:opacity-40 transition-colors">
435
+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="currentColor"><path d="M16 9.75C16.9665 9.75 17.75 8.9665 17.75 8C17.75 7.0335 16.9665 6.25 16 6.25C15.0335 6.25 14.25 7.0335 14.25 8C14.25 8.9665 15.0335 9.75 16 9.75Z" /><path d="M15 2C11.134 2 8 5.13401 8 9C8 9.49204 8.05092 9.97307 8.14801 10.4378L3.73223 14.8536C3.26339 15.3224 3 15.9583 3 16.6213V20C3 20.5523 3.44772 21 4 21H7.37868C8.04172 21 8.6776 20.7366 9.14645 20.2678L10.2071 19.2071C10.3946 19.0196 10.5 18.7652 10.5 18.5V17.5H11.5C11.7652 17.5 12.0196 17.3946 12.2071 17.2071L13.5622 15.852C14.0269 15.9491 14.508 16 15 16C18.866 16 22 12.866 22 9C22 5.13401 18.866 2 15 2ZM10 9C10 6.23858 12.2386 4 15 4C17.7614 4 20 6.23858 20 9C20 11.7614 17.7614 14 15 14C14.4932 14 14.0057 13.9249 13.5471 13.7859C13.1941 13.6789 12.8108 13.775 12.55 14.0358L11.0858 15.5H9.5C8.94772 15.5 8.5 15.9477 8.5 16.5V18.0858L7.73223 18.8536C7.63847 18.9473 7.51129 19 7.37868 19H5V16.6213C5 16.4887 5.05268 16.3615 5.14645 16.2678L9.96418 11.45C10.225 11.1892 10.3211 10.8059 10.2141 10.4529C10.0751 9.99431 10 9.50683 10 9Z" /></svg>
436
+ {creating ? 'Creating…' : 'Create new secret key'}
437
+ </button>
438
+ </div>
439
+
440
+ {#if newlyCreatedKey}
441
+ <div class="border border-[#e5e5e5] rounded-2xl p-5 bg-white mb-6">
442
+ <p class="text-sm font-semibold text-[#0f0f0f] mb-1">Save your key</p>
443
+ <p class="text-sm text-[#666666] mb-3">This won't be shown again. Copy it now and store it safely.</p>
444
+ <div class="flex items-center gap-2 bg-[#f4f4f4] rounded-lg px-4 py-3 mb-3">
445
+ <code class="text-sm font-mono text-[#0f0f0f] flex-1 break-all">{newlyCreatedKey}</code>
446
+ <button on:click={() => copy(newlyCreatedKey ?? '', 'new-key')} class="text-[#888888] hover:text-[#0f0f0f] transition-colors text-sm">
447
+ {copiedId === 'new-key' ? 'Copied' : 'Copy'}
448
+ </button>
449
+ </div>
450
+ <button on:click={() => newlyCreatedKey = null} class="text-sm text-[#888888] hover:text-[#0f0f0f] transition-colors">Dismiss</button>
451
+ </div>
452
+ {/if}
453
+
454
+ <div class="border border-[#e5e5e5] rounded-2xl bg-white overflow-hidden">
455
+ <div class="grid grid-cols-[1fr_auto] sm:grid-cols-[1fr_auto_auto] gap-4 px-5 py-3 border-b border-[#e5e5e5] bg-[#f9f9f9]">
456
+ <p class="text-xs font-medium text-[#888888] uppercase tracking-wide">Name</p>
457
+ <p class="hidden sm:block text-xs font-medium text-[#888888] uppercase tracking-wide">Created</p>
458
+ <p class="w-8"></p>
459
+ </div>
460
+ {#if keysLoading}
461
+ <div class="flex items-center justify-center py-12">
462
+ <div class="w-4 h-4 rounded-full border-[1.5px] border-black/10 border-t-black animate-spin"></div>
463
+ </div>
464
+ {:else if keys.length === 0}
465
+ <div class="flex flex-col items-center py-12 gap-2">
466
+ <p class="text-sm font-medium text-[#444444]">No API keys yet</p>
467
+ <p class="text-sm text-[#888888]">Create your first key to start using the API</p>
468
+ </div>
469
+ {:else}
470
+ {#each keys as k, i}
471
+ <div class="grid grid-cols-[1fr_auto] sm:grid-cols-[1fr_auto_auto] gap-4 items-center px-5 py-4 {i < keys.length - 1 ? 'border-b border-[#e5e5e5]' : ''} hover:bg-gray-50 transition-colors group">
472
+ <div>
473
+ <p class="text-sm font-medium text-[#0f0f0f]">{k.name ?? 'API Key'}</p>
474
+ <div class="flex items-center gap-1.5 mt-0.5">
475
+ <code class="text-xs sm:text-sm font-mono text-[#666666] truncate max-w-[180px] sm:max-w-none">sk-••••••••••••••••••••</code>
476
+ <button on:click={() => copy(k.key ?? '', k.id)} class="text-xs text-[#888888] hover:text-[#0f0f0f] transition-colors sm:opacity-0 sm:group-hover:opacity-100">
477
+ {copiedId === k.id ? 'Copied' : 'Copy'}
478
+ </button>
479
+ </div>
480
+ </div>
481
+ <p class="hidden sm:block text-sm text-[#666666] whitespace-nowrap">{fmtDate(k.created_at)}</p>
482
+ <button on:click={() => deleteKey(k.id)} class="w-8 h-8 flex items-center justify-center rounded-md text-[#888888] hover:text-red-500 hover:bg-red-50 transition-colors sm:opacity-0 sm:group-hover:opacity-100">
483
+ <X class="w-4 h-4" strokeWidth={2} />
484
+ </button>
485
+ </div>
486
+ {/each}
487
+ {/if}
488
+ </div>
489
+
490
+ {/if}
491
+
492
+ </div>
493
+ </main>
494
+ </div>
495
+ </div>
496
+ {/if}
console/src/routes/docs/+page.svelte ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { goto } from '$app/navigation';
4
+
5
+ let activeSection = 'introduction';
6
+ let baseUrl = '';
7
+ let copied = false;
8
+
9
+ function copyBaseUrl() {
10
+ if (!baseUrl) return;
11
+ navigator.clipboard.writeText(baseUrl);
12
+ copied = true;
13
+ setTimeout(() => copied = false, 2000);
14
+ }
15
+
16
+ const O = '\u007B';
17
+ const C = '\u007D';
18
+
19
+ onMount(() => {
20
+ const host = window.location.hostname;
21
+ if (host === 'localhost' || host === '127.0.0.1') {
22
+ baseUrl = 'http://localhost:3001/v1';
23
+ } else {
24
+ baseUrl = 'https://' + host + '/v1';
25
+ }
26
+ });
27
+
28
+ function esc(s: string) {
29
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
30
+ }
31
+
32
+ $: s = (function(url: string) {
33
+ const u = url || 'https://api.example.com/v1';
34
+ return {
35
+ curlModels: esc(
36
+ 'curl ' + u + '/models \\\n' +
37
+ ' -H "Authorization: Bearer sk-<your-key>"'
38
+ ),
39
+ curlChat: esc(
40
+ 'curl ' + u + '/chat/completions \\\n' +
41
+ ' -H "Content-Type: application/json" \\\n' +
42
+ ' -H "Authorization: Bearer sk-<your-key>" \\\n' +
43
+ ' -d \'' + O + '\n' +
44
+ ' "model": "DeepSeek-V3.1",\n' +
45
+ ' "messages": [\n' +
46
+ ' ' + O + '"role": "system", "content": "You are a helpful assistant."' + C + ',\n' +
47
+ ' ' + O + '"role": "user", "content": "What is the capital of Poland?"' + C + '\n' +
48
+ ' ]\n' +
49
+ ' ' + C + '\''
50
+ ),
51
+ python: esc(
52
+ 'from openai import OpenAI\n\n' +
53
+ 'client = OpenAI(\n' +
54
+ ' api_key="sk-<your-key>",\n' +
55
+ ' base_url="' + u + '"\n' +
56
+ ')\n\n' +
57
+ 'response = client.chat.completions.create(\n' +
58
+ ' model="Kimi-K2",\n' +
59
+ ' messages=[\n' +
60
+ ' ' + O + '"role": "user", "content": "Explain quantum entanglement simply."' + C + '\n' +
61
+ ' ]\n' +
62
+ ')\n' +
63
+ 'print(response.choices[0].message.content)'
64
+ ),
65
+ js: esc(
66
+ 'import OpenAI from \'openai\';\n\n' +
67
+ 'const client = new OpenAI(' + O + '\n' +
68
+ ' apiKey: \'sk-<your-key>\',\n' +
69
+ ' baseURL: \'' + u + '\'\n' +
70
+ C + ');\n\n' +
71
+ 'const response = await client.chat.completions.create(' + O + '\n' +
72
+ ' model: \'Mistral-Small-4\',\n' +
73
+ ' messages: [' + O + ' role: \'user\', content: \'Hello!\' ' + C + ']\n' +
74
+ C + ');\n' +
75
+ 'console.log(response.choices[0].message.content);'
76
+ ),
77
+ streamPython: esc(
78
+ 'stream = client.chat.completions.create(\n' +
79
+ ' model="GPT-OSS-120b",\n' +
80
+ ' messages=[' + O + '"role": "user", "content": "Tell me a story"' + C + '],\n' +
81
+ ' stream=True\n' +
82
+ ')\n' +
83
+ 'for chunk in stream:\n' +
84
+ ' print(chunk.choices[0].delta.content or "", end="", flush=True)'
85
+ ),
86
+ sseRaw: esc(
87
+ 'data: ' + O + '"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[' + O + '"delta":' + O + '"content":"Hello"' + C + ',...' + C + ']' + C + '\n\n' +
88
+ 'data: ' + O + '"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[' + O + '"delta":' + O + '"content":" world"' + C + ',...' + C + ']' + C + '\n\n' +
89
+ 'data: [DONE]'
90
+ ),
91
+ toolCurl: esc(
92
+ 'curl ' + u + '/chat/completions \\\n' +
93
+ ' -H "Authorization: Bearer sk-<your-key>" \\\n' +
94
+ ' -H "Content-Type: application/json" \\\n' +
95
+ ' -d \'' + O + '\n' +
96
+ ' "model": "DeepSeek-V3.1",\n' +
97
+ ' "messages": [' + O + '"role":"user","content":"What is the weather in Warsaw?"' + C + '],\n' +
98
+ ' "tools": [' + O + '\n' +
99
+ ' "type": "function",\n' +
100
+ ' "function": ' + O + '\n' +
101
+ ' "name": "get_weather",\n' +
102
+ ' "description": "Get current weather for a city",\n' +
103
+ ' "x-endpoint": "https://your-service.com/api/weather",\n' +
104
+ ' "parameters": ' + O + '\n' +
105
+ ' "type": "object",\n' +
106
+ ' "properties": ' + O + '"city": ' + O + '"type": "string"' + C + C + ',\n' +
107
+ ' "required": ["city"]\n' +
108
+ ' ' + C + '\n' +
109
+ ' ' + C + '\n' +
110
+ ' ' + C + ']\n' +
111
+ ' ' + C + '\''
112
+ ),
113
+ };
114
+ })(baseUrl);
115
+
116
+ const nav = [
117
+ { id: 'introduction', label: 'Introduction' },
118
+ { id: 'authentication', label: 'Authentication' },
119
+ { id: 'models', label: 'Available models' },
120
+ { id: 'chat-completions',label: 'Chat Completions' },
121
+ { id: 'streaming', label: 'Streaming' },
122
+ { id: 'tool-calls', label: 'Tool calls' },
123
+ { id: 'errors', label: 'Errors' },
124
+ ];
125
+
126
+ const models = [
127
+ { alias: 'Bielik-11b', id: 'speakleash/bielik-11b-v2.6-instruct', note: 'Polish language model' },
128
+ { alias: 'Mistral-Small-4', id: 'mistralai/mistral-small-4-119b-2603', note: 'Fast, efficient' },
129
+ { alias: 'DeepSeek-V3.1', id: 'deepseek-ai/deepseek-v3.1', note: 'Strong reasoning' },
130
+ { alias: 'Kimi-K2', id: 'moonshotai/kimi-k2-instruct', note: 'Long context' },
131
+ { alias: 'Kimi-K2.5', id: 'moonshotai/kimi-k2.5', note: 'Long context, updated' },
132
+ { alias: 'Amazon-Nova-2-lite-v1', id: 'nova-2-lite-v1', note: 'Amazon Nova, fast' },
133
+ { alias: 'Minimax-m2.5', id: 'minimaxai/minimax-m2.5', note: 'Multimodal' },
134
+ { alias: 'GLM-4.7', id: 'z-ai/glm4.7', note: 'Multilingual' },
135
+ { alias: 'GPT-OSS-120b', id: 'openai/gpt-oss-120b', note: 'OpenAI open-source' },
136
+ { alias: 'Step-3.5-Flash', id: 'stepfun-ai/step-3.5-flash', note: 'Ultra fast' },
137
+ { alias: 'Qwen-3.5', id: 'qwen/qwen3.5-122b-a10b', note: 'Strong at code' },
138
+ ];
139
+
140
+ const params = [
141
+ { name: 'model', type: 'string', req: true, desc: 'Model alias or full model ID.' },
142
+ { name: 'messages', type: 'array', req: true, desc: 'Array of messages: system, user, assistant, tool.' },
143
+ { name: 'stream', type: 'boolean', req: false, desc: 'Enable streaming (default: true).' },
144
+ { name: 'temperature', type: 'number', req: false, desc: 'Sampling temperature, 0–2.' },
145
+ { name: 'max_tokens', type: 'integer', req: false, desc: 'Maximum tokens in the response.' },
146
+ { name: 'tools', type: 'array', req: false, desc: 'List of tool definitions (OpenAI format).' },
147
+ { name: 'tool_choice', type: 'string | object', req: false, desc: '"auto", "none", or specific tool.' },
148
+ ];
149
+
150
+ const errors = [
151
+ { code: '401', color: 'text-red-500', label: 'Unauthorized', desc: 'Invalid or missing API key.' },
152
+ { code: '400', color: 'text-orange-500', label: 'Bad Request', desc: 'Malformed JSON or missing required fields.' },
153
+ { code: '502', color: 'text-orange-500', label: 'Bad Gateway', desc: 'Upstream model provider returned an error.' },
154
+ { code: '200', color: 'text-green-500', label: 'OK', desc: 'Successful response.' },
155
+ ];
156
+
157
+ function scrollTo(id: string) {
158
+ activeSection = id;
159
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
160
+ }
161
+ </script>
162
+
163
+ <svelte:head>
164
+ <title>API Documentation</title>
165
+ </svelte:head>
166
+
167
+ <div class="min-h-screen bg-white text-[#0f0f0f] font-sans">
168
+
169
+ <header class="sticky top-0 z-50 h-[60px] flex items-center justify-between px-6 border-b border-[#f0f0f0] bg-white/90 backdrop-blur-sm">
170
+ <div class="flex items-center gap-3">
171
+ <button
172
+ on:click={() => goto('/')}
173
+ class="flex items-center gap-2 text-sm text-[#666666] hover:text-[#0f0f0f] transition-colors"
174
+ >
175
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 5l-7 7 7 7"/></svg>
176
+ Console
177
+ </button>
178
+ <span class="text-[#e5e5e5]">/</span>
179
+ <span class="text-sm font-medium text-[#0f0f0f]">API Documentation</span>
180
+ </div>
181
+ <div class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#f4f4f4] text-xs text-[#666666]">
182
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
183
+ v1
184
+ </div>
185
+ </header>
186
+
187
+ <div class="flex max-w-[1100px] mx-auto">
188
+
189
+ <nav class="hidden md:flex flex-col gap-0.5 w-52 flex-shrink-0 sticky top-[60px] h-[calc(100vh-60px)] pt-8 pb-8 pl-6 pr-4 overflow-y-auto">
190
+ <p class="text-[11px] font-semibold text-[#aaaaaa] uppercase tracking-wider mb-3 px-2">Reference</p>
191
+ {#each nav as item}
192
+ <button
193
+ on:click={() => scrollTo(item.id)}
194
+ class="text-left px-2 py-1.5 rounded-lg text-sm transition-colors {activeSection === item.id ? 'bg-[#f4f4f4] text-[#0f0f0f] font-medium' : 'text-[#666666] hover:text-[#0f0f0f] hover:bg-[#f9f9f9]'}"
195
+ >{item.label}</button>
196
+ {/each}
197
+ </nav>
198
+
199
+ <main class="flex-1 min-w-0 px-6 md:px-10 py-10 space-y-16">
200
+
201
+ <section id="introduction">
202
+ <div class="inline-flex items-center px-3 py-1 rounded-full bg-[#f4f4f4] text-xs text-[#666666] font-medium mb-4">OpenAI-compatible</div>
203
+ <h1 class="text-3xl font-bold tracking-tight text-[#0f0f0f] mb-4">API Documentation</h1>
204
+ <p class="text-[16px] text-[#555555] leading-relaxed max-w-[640px] mb-6">
205
+ A unified, OpenAI-compatible gateway giving you access to the best open and proprietary AI models through a single endpoint. Drop it in anywhere you already use the OpenAI SDK — no code changes needed.
206
+ </p>
207
+ <button
208
+ on:click={copyBaseUrl}
209
+ class="w-full text-left bg-[#f9f9f9] border border-[#e5e5e5] rounded-2xl p-5 hover:bg-[#f4f4f4] transition-colors group"
210
+ >
211
+ <div class="flex items-center justify-between mb-2">
212
+ <p class="text-xs font-semibold text-[#888888] uppercase tracking-wider">Base URL</p>
213
+ <span class="text-xs text-[#aaaaaa] group-hover:text-[#666666] transition-colors">
214
+ {copied ? '✓ Skopiowano' : 'Kliknij aby skopiować'}
215
+ </span>
216
+ </div>
217
+ <code class="text-[15px] font-mono text-[#0f0f0f]">{baseUrl || 'Loading…'}</code>
218
+ </button>
219
+ </section>
220
+
221
+ <section id="authentication">
222
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Authentication</h2>
223
+ <p class="text-sm text-[#888888] mb-5">Pass your API key in every request using the <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">Authorization</code> header.</p>
224
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden mb-5">
225
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
226
+ <span class="text-xs font-medium text-[#888888]">HTTP header</span>
227
+ </div>
228
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>Authorization: Bearer sk-&lt;your-api-key&gt;</code></pre>
229
+ </div>
230
+ <p class="text-sm text-[#666666] mb-4">Alternatively pass <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">x-api-key</code> as a header.</p>
231
+ <div class="flex gap-2 items-start p-4 rounded-xl border border-amber-200 bg-amber-50">
232
+ <svg class="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
233
+ <p class="text-sm text-amber-800">Keep your API keys secret. Do not share them or expose them in client-side code.</p>
234
+ </div>
235
+ </section>
236
+
237
+ <section id="models">
238
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Available models</h2>
239
+ <p class="text-sm text-[#888888] mb-5">Use the short alias or the full model ID in the <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">model</code> field. Both are accepted.</p>
240
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden mb-5">
241
+ <table class="w-full text-sm">
242
+ <thead>
243
+ <tr class="bg-[#f9f9f9] border-b border-[#e5e5e5]">
244
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider">Alias</th>
245
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider hidden sm:table-cell">Full model ID</th>
246
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider hidden md:table-cell">Note</th>
247
+ </tr>
248
+ </thead>
249
+ <tbody>
250
+ {#each models as m, i}
251
+ <tr class="{i < models.length - 1 ? 'border-b border-[#f0f0f0]' : ''}">
252
+ <td class="px-4 py-3 font-mono text-[13px] font-medium text-[#0f0f0f] whitespace-nowrap">{m.alias}</td>
253
+ <td class="px-4 py-3 font-mono text-[12px] text-[#666666] hidden sm:table-cell">{m.id}</td>
254
+ <td class="px-4 py-3 text-[13px] text-[#888888] hidden md:table-cell">{m.note}</td>
255
+ </tr>
256
+ {/each}
257
+ </tbody>
258
+ </table>
259
+ </div>
260
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
261
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
262
+ <span class="text-xs font-medium text-[#888888]">List all models — cURL</span>
263
+ </div>
264
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.curlModels}</code></pre>
265
+ </div>
266
+ </section>
267
+
268
+ <section id="chat-completions">
269
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Chat Completions</h2>
270
+ <p class="text-sm text-[#888888] mb-5">Fully compatible with the OpenAI Chat Completions API. Use any existing OpenAI client by changing only the <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">base_url</code>.</p>
271
+ <div class="space-y-4">
272
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
273
+ <div class="flex items-center gap-2 px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
274
+ <span class="text-xs font-mono font-semibold text-[#22c55e]">POST</span>
275
+ <code class="text-xs text-[#444444]">/v1/chat/completions — cURL</code>
276
+ </div>
277
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.curlChat}</code></pre>
278
+ </div>
279
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
280
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
281
+ <span class="text-xs font-medium text-[#888888]">Python — openai SDK</span>
282
+ </div>
283
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.python}</code></pre>
284
+ </div>
285
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
286
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
287
+ <span class="text-xs font-medium text-[#888888]">JavaScript / TypeScript — openai SDK</span>
288
+ </div>
289
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.js}</code></pre>
290
+ </div>
291
+ </div>
292
+ <div class="mt-6">
293
+ <h3 class="text-base font-semibold text-[#0f0f0f] mb-3">Request body</h3>
294
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
295
+ <table class="w-full text-sm">
296
+ <thead>
297
+ <tr class="bg-[#f9f9f9] border-b border-[#e5e5e5]">
298
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider">Parameter</th>
299
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider">Type</th>
300
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider hidden sm:table-cell">Description</th>
301
+ </tr>
302
+ </thead>
303
+ <tbody>
304
+ {#each params as p, i}
305
+ <tr class="{i < params.length - 1 ? 'border-b border-[#f0f0f0]' : ''}">
306
+ <td class="px-4 py-3 font-mono text-[13px] text-[#0f0f0f]">
307
+ {p.name}
308
+ {#if p.req}<span class="ml-1.5 text-[10px] font-sans font-semibold text-red-500 uppercase">req</span>{/if}
309
+ </td>
310
+ <td class="px-4 py-3 font-mono text-[12px] text-[#888888]">{p.type}</td>
311
+ <td class="px-4 py-3 text-[13px] text-[#666666] hidden sm:table-cell">{p.desc}</td>
312
+ </tr>
313
+ {/each}
314
+ </tbody>
315
+ </table>
316
+ </div>
317
+ </div>
318
+ </section>
319
+
320
+ <section id="streaming">
321
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Streaming</h2>
322
+ <p class="text-sm text-[#888888] mb-5">By default all responses are streamed using Server-Sent Events. Set <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">stream: false</code> to receive a single JSON response.</p>
323
+ <div class="space-y-4">
324
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
325
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
326
+ <span class="text-xs font-medium text-[#888888]">Python — streaming</span>
327
+ </div>
328
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.streamPython}</code></pre>
329
+ </div>
330
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
331
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
332
+ <span class="text-xs font-medium text-[#888888]">SSE format (raw)</span>
333
+ </div>
334
+ <pre class="p-4 text-sm font-mono text-[#666666] overflow-x-auto"><code>{@html s.sseRaw}</code></pre>
335
+ </div>
336
+ </div>
337
+ </section>
338
+
339
+ <section id="tool-calls">
340
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Tool calls</h2>
341
+ <p class="text-sm text-[#888888] mb-5">
342
+ Pass <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">tools</code> in the standard OpenAI format. The gateway also supports an <strong>agentic loop</strong> — add an <code class="font-mono bg-[#f4f4f4] px-1.5 py-0.5 rounded text-[13px]">x-endpoint</code> field to a tool definition and the API will automatically execute the tool, send results back to the model, and repeat until a final answer is produced (up to 10 iterations).
343
+ </p>
344
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
345
+ <div class="flex items-center px-4 py-2.5 bg-[#f9f9f9] border-b border-[#e5e5e5]">
346
+ <span class="text-xs font-medium text-[#888888]">Auto-execute tool via x-endpoint — cURL</span>
347
+ </div>
348
+ <pre class="p-4 text-sm font-mono text-[#0f0f0f] overflow-x-auto"><code>{@html s.toolCurl}</code></pre>
349
+ </div>
350
+ </section>
351
+
352
+ <section id="errors">
353
+ <h2 class="text-xl font-semibold text-[#0f0f0f] mb-1">Errors</h2>
354
+ <p class="text-sm text-[#888888] mb-5">Standard HTTP status codes are used.</p>
355
+ <div class="rounded-2xl border border-[#e5e5e5] overflow-hidden">
356
+ <table class="w-full text-sm">
357
+ <thead>
358
+ <tr class="bg-[#f9f9f9] border-b border-[#e5e5e5]">
359
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider">Status</th>
360
+ <th class="text-left px-4 py-3 text-xs font-semibold text-[#888888] uppercase tracking-wider">Meaning</th>
361
+ </tr>
362
+ </thead>
363
+ <tbody>
364
+ {#each errors as e, i}
365
+ <tr class="{i < errors.length - 1 ? 'border-b border-[#f0f0f0]' : ''}">
366
+ <td class="px-4 py-3">
367
+ <span class="font-mono text-[13px] font-semibold {e.color}">{e.code}</span>
368
+ <span class="ml-2 text-[13px] text-[#444444]">{e.label}</span>
369
+ </td>
370
+ <td class="px-4 py-3 text-[13px] text-[#666666]">{e.desc}</td>
371
+ </tr>
372
+ {/each}
373
+ </tbody>
374
+ </table>
375
+ </div>
376
+ </section>
377
+
378
+ </main>
379
+ </div>
380
+ </div>
console/src/routes/login/+page.svelte ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { goto } from '$app/navigation';
4
+ import { supabase } from '$lib/supabase';
5
+
6
+ let loaded = false;
7
+ let view: 'login' | 'signup' = 'login';
8
+
9
+ let loginStep: 'email' | 'password' = 'email';
10
+ let loginEmail = '';
11
+ let loginPassword = '';
12
+ let loginLoading = false;
13
+ let loginError = '';
14
+
15
+ let signupStep: 'email' | 'password' = 'email';
16
+ let signupEmail = '';
17
+ let signupPassword = '';
18
+ let signupLoading = false;
19
+ let signupError = '';
20
+ let signupSuccess = '';
21
+
22
+ onMount(async () => {
23
+ const { data: { session } } = await supabase.auth.getSession();
24
+ if (session) { goto('/'); return; }
25
+ loaded = true;
26
+ });
27
+
28
+ async function handleLoginEmailSubmit() {
29
+ if (!loginEmail) return;
30
+ loginError = '';
31
+ loginLoading = true;
32
+ await new Promise(r => setTimeout(r, 200));
33
+ loginLoading = false;
34
+ loginStep = 'password';
35
+ }
36
+
37
+ async function handleLoginPasswordSubmit() {
38
+ loginLoading = true;
39
+ loginError = '';
40
+ try {
41
+ const { error } = await supabase.auth.signInWithPassword({ email: loginEmail, password: loginPassword });
42
+ if (error) { loginError = error.message; loginLoading = false; }
43
+ else goto('/');
44
+ } catch {
45
+ loginError = 'Błąd logowania.';
46
+ loginLoading = false;
47
+ }
48
+ }
49
+
50
+ async function handleSignupEmailSubmit() {
51
+ if (!signupEmail) return;
52
+ signupError = '';
53
+ signupLoading = true;
54
+ await new Promise(r => setTimeout(r, 200));
55
+ signupLoading = false;
56
+ signupStep = 'password';
57
+ }
58
+
59
+ async function handleSignupPasswordSubmit() {
60
+ signupLoading = true;
61
+ signupError = '';
62
+ try {
63
+ const { error } = await supabase.auth.signUp({ email: signupEmail, password: signupPassword });
64
+ if (error) { signupError = error.message; signupLoading = false; }
65
+ else {
66
+ signupSuccess = 'Konto zostało utworzone. Możesz się teraz zalogować.';
67
+ switchToLogin();
68
+ }
69
+ } catch {
70
+ signupError = 'Błąd rejestracji.';
71
+ signupLoading = false;
72
+ }
73
+ }
74
+
75
+ function switchToSignup() {
76
+ view = 'signup';
77
+ signupStep = 'email';
78
+ signupEmail = '';
79
+ signupPassword = '';
80
+ signupError = '';
81
+ signupSuccess = '';
82
+ }
83
+
84
+ function switchToLogin() {
85
+ view = 'login';
86
+ loginStep = 'email';
87
+ loginEmail = '';
88
+ loginPassword = '';
89
+ loginError = '';
90
+ }
91
+
92
+ $: loginTitle = loginStep === 'email' ? 'Zaloguj się' : 'Wprowadź hasło';
93
+ $: signupTitle = signupStep === 'email' ? 'Utwórz konto' : 'Ustaw hasło';
94
+ </script>
95
+
96
+ <div class="w-full h-screen bg-white flex items-center justify-center">
97
+ {#if loaded}
98
+ <div class="w-full px-10 flex flex-col items-center">
99
+ <div class="sm:max-w-sm w-full">
100
+
101
+ {#if view === 'login'}
102
+ <form
103
+ on:submit|preventDefault={() => loginStep === 'email' ? handleLoginEmailSubmit() : handleLoginPasswordSubmit()}
104
+ class="flex flex-col"
105
+ >
106
+ <div class="mb-1 text-center">
107
+ <div class="text-2xl font-medium text-black">{loginTitle}</div>
108
+ </div>
109
+
110
+ <div class="flex flex-col mt-4">
111
+ {#if loginStep === 'email'}
112
+ <div class="mb-2">
113
+ <input
114
+ bind:value={loginEmail}
115
+ type="email"
116
+ autocomplete="email"
117
+ placeholder="Adres e-mail"
118
+ required
119
+ class="w-full text-sm outline-none bg-transparent border border-gray-200 rounded-full px-4 py-2.5 placeholder:text-gray-400 focus:border-gray-400 transition"
120
+ />
121
+ </div>
122
+ {:else}
123
+ <input type="email" autocomplete="email" value={loginEmail} aria-hidden="true" tabindex="-1" style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;" readonly />
124
+ <div class="flex items-center gap-2 mb-3">
125
+ <button
126
+ type="button"
127
+ aria-label="Wróć"
128
+ on:click={() => { loginStep = 'email'; loginError = ''; }}
129
+ class="h-7 w-7 flex items-center justify-center rounded-full hover:bg-gray-100 transition"
130
+ >
131
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-black" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
132
+ </button>
133
+ <span class="text-sm text-gray-500">{loginEmail}</span>
134
+ </div>
135
+ <div class="mb-2">
136
+ <input
137
+ bind:value={loginPassword}
138
+ type="password"
139
+ autocomplete="current-password"
140
+ placeholder="Hasło"
141
+ required
142
+ class="w-full text-sm outline-none bg-transparent border border-gray-200 rounded-full px-4 py-2.5 placeholder:text-gray-400 focus:border-gray-400 transition"
143
+ />
144
+ </div>
145
+ {/if}
146
+ </div>
147
+
148
+ {#if loginError}
149
+ <p class="text-sm text-red-500 mb-2">{loginError}</p>
150
+ {/if}
151
+
152
+ {#if signupSuccess}
153
+ <p class="text-sm text-green-600 mb-2">{signupSuccess}</p>
154
+ {/if}
155
+
156
+ <div class="mt-2">
157
+ <button
158
+ type="submit"
159
+ disabled={loginLoading}
160
+ class="w-full flex items-center justify-center gap-2 bg-black text-white text-sm font-medium py-2.5 rounded-full hover:bg-gray-800 disabled:opacity-40 transition"
161
+ >
162
+ {#if loginLoading}
163
+ <div class="w-3.5 h-3.5 rounded-full border-[1.5px] border-white/30 border-t-white animate-spin"></div>
164
+ {/if}
165
+ {loginStep === 'email' ? 'Kontynuuj' : 'Zaloguj się'}
166
+ </button>
167
+ </div>
168
+
169
+ <div class="mt-5 text-center">
170
+ <span class="text-sm text-gray-500">Nie masz konta?</span>
171
+ <button type="button" on:click={switchToSignup} class="text-sm text-black font-medium ml-1 hover:opacity-60 transition-opacity">
172
+ Zarejestruj się
173
+ </button>
174
+ </div>
175
+ </form>
176
+
177
+ {:else}
178
+ <form
179
+ on:submit|preventDefault={() => signupStep === 'email' ? handleSignupEmailSubmit() : handleSignupPasswordSubmit()}
180
+ class="flex flex-col"
181
+ >
182
+ <div class="mb-1 text-center">
183
+ <div class="text-2xl font-medium text-black">{signupTitle}</div>
184
+ </div>
185
+
186
+ <div class="flex flex-col mt-4">
187
+ {#if signupStep === 'email'}
188
+ <div class="mb-2">
189
+ <input
190
+ bind:value={signupEmail}
191
+ type="email"
192
+ autocomplete="email"
193
+ placeholder="Adres e-mail"
194
+ required
195
+ class="w-full text-sm outline-none bg-transparent border border-gray-200 rounded-full px-4 py-2.5 placeholder:text-gray-400 focus:border-gray-400 transition"
196
+ />
197
+ </div>
198
+ {:else}
199
+ <input type="email" autocomplete="email" value={signupEmail} aria-hidden="true" tabindex="-1" style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;" readonly />
200
+ <div class="flex items-center gap-2 mb-3">
201
+ <button
202
+ type="button"
203
+ aria-label="Wróć"
204
+ on:click={() => { signupStep = 'email'; signupError = ''; }}
205
+ class="h-7 w-7 flex items-center justify-center rounded-full hover:bg-gray-100 transition"
206
+ >
207
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-black" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
208
+ </button>
209
+ <span class="text-sm text-gray-500">{signupEmail}</span>
210
+ </div>
211
+ <div class="mb-2">
212
+ <input
213
+ bind:value={signupPassword}
214
+ type="password"
215
+ autocomplete="new-password"
216
+ placeholder="Hasło"
217
+ required
218
+ class="w-full text-sm outline-none bg-transparent border border-gray-200 rounded-full px-4 py-2.5 placeholder:text-gray-400 focus:border-gray-400 transition"
219
+ />
220
+ </div>
221
+ {/if}
222
+ </div>
223
+
224
+ {#if signupError}
225
+ <p class="text-sm text-red-500 mb-2">{signupError}</p>
226
+ {/if}
227
+
228
+ <div class="mt-2">
229
+ <button
230
+ type="submit"
231
+ disabled={signupLoading}
232
+ class="w-full flex items-center justify-center gap-2 bg-black text-white text-sm font-medium py-2.5 rounded-full hover:bg-gray-800 disabled:opacity-40 transition"
233
+ >
234
+ {#if signupLoading}
235
+ <div class="w-3.5 h-3.5 rounded-full border-[1.5px] border-white/30 border-t-white animate-spin"></div>
236
+ {/if}
237
+ {signupStep === 'email' ? 'Kontynuuj' : 'Zarejestruj się'}
238
+ </button>
239
+ </div>
240
+
241
+ <div class="mt-5 text-center">
242
+ <span class="text-sm text-gray-500">Masz już konto?</span>
243
+ <button type="button" on:click={switchToLogin} class="text-sm text-black font-medium ml-1 hover:opacity-60 transition-opacity">
244
+ Zaloguj się
245
+ </button>
246
+ </div>
247
+ </form>
248
+ {/if}
249
+
250
+ </div>
251
+ </div>
252
+ {/if}
253
+ </div>
console/svelte.config.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import adapter from '@sveltejs/adapter-static';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+
4
+ /** @type {import('@sveltejs/kit').Config} */
5
+ const config = {
6
+ preprocess: vitePreprocess(),
7
+ kit: {
8
+ adapter: adapter({
9
+ fallback: 'index.html'
10
+ }),
11
+ prerender: {
12
+ handleHttpError: ({ path, referrer, message }) => {
13
+ if (path.includes('favicon')) return;
14
+ throw new Error(message);
15
+ }
16
+ }
17
+ }
18
+ };
19
+
20
+ export default config;
console/tailwind.config.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ darkMode: 'class',
4
+ content: ['./src/**/*.{html,js,svelte,ts}'],
5
+ theme: {
6
+ extend: {
7
+ fontFamily: {
8
+ sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'sans-serif']
9
+ },
10
+ borderRadius: {
11
+ DEFAULT: '1rem',
12
+ ui: '1rem',
13
+ sm: '0.7rem'
14
+ },
15
+ colors: {
16
+ surface: {
17
+ DEFAULT: '#ffffff',
18
+ secondary: '#F9F9F9',
19
+ tertiary: '#F2F2F2',
20
+ dark: '#000000',
21
+ 'dark-secondary': '#191919',
22
+ 'dark-tertiary': '#212121',
23
+ 'dark-quaternary': '#2C2C2E'
24
+ },
25
+ border: {
26
+ DEFAULT: '#EBEBEB',
27
+ dark: '#2C2C2E'
28
+ },
29
+ muted: {
30
+ DEFAULT: '#8A8A8D',
31
+ dark: '#636366'
32
+ }
33
+ }
34
+ }
35
+ },
36
+ plugins: []
37
+ };
console/tsconfig.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "./.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "allowJs": true,
5
+ "checkJs": true,
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "resolveJsonModule": true,
9
+ "skipLibCheck": true,
10
+ "sourceMap": true,
11
+ "strict": true
12
+ }
13
+ }
console/vite.config.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sveltekit } from '@sveltejs/kit/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ plugins: [sveltekit()],
6
+ server: {
7
+ host: '0.0.0.0',
8
+ port: parseInt(process.env.PORT || '5173'),
9
+ allowedHosts: true,
10
+ hmr: {
11
+ protocol: 'wss',
12
+ clientPort: 443,
13
+ host: process.env.REPLIT_DEV_DOMAIN || 'localhost',
14
+ },
15
+ proxy: {
16
+ '/v1': {
17
+ target: 'http://localhost:3001',
18
+ changeOrigin: true,
19
+ }
20
+ }
21
+ },
22
+ build: {
23
+ target: 'esnext',
24
+ sourcemap: false,
25
+ minify: 'esbuild',
26
+ rollupOptions: {
27
+ output: {
28
+ manualChunks: undefined
29
+ }
30
+ }
31
+ }
32
+ });
console_api.go ADDED
@@ -0,0 +1 @@
 
 
1
+ package main
go.mod ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ module gateway
2
+
3
+ go 1.21
keys.go ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ "strings"
7
+ "sync"
8
+ "time"
9
+ )
10
+
11
+ type keyCache struct {
12
+ mu sync.RWMutex
13
+ keys map[string]bool
14
+ expires time.Time
15
+ }
16
+
17
+ var apiKeyCache = &keyCache{
18
+ keys: make(map[string]bool),
19
+ }
20
+
21
+ type APIKey struct {
22
+ ID string `json:"id,omitempty"`
23
+ Key string `json:"key"`
24
+ Name string `json:"name"`
25
+ Active bool `json:"active"`
26
+ CreatedAt string `json:"created_at,omitempty"`
27
+ }
28
+
29
+ func isValidAPIKey(key string) bool {
30
+ apiKeyCache.mu.RLock()
31
+ if time.Now().Before(apiKeyCache.expires) {
32
+ valid := apiKeyCache.keys[key]
33
+ apiKeyCache.mu.RUnlock()
34
+ return valid
35
+ }
36
+ apiKeyCache.mu.RUnlock()
37
+
38
+ return checkKeyInSupabase(key)
39
+ }
40
+
41
+ func checkKeyInSupabase(key string) bool {
42
+ data, status, err := supabaseServiceRequest("GET", "api_keys", "key=eq."+key+"&active=eq.true&select=key", nil)
43
+ if err != nil || status != 200 {
44
+ return false
45
+ }
46
+ var results []map[string]interface{}
47
+ if err := json.Unmarshal(data, &results); err != nil {
48
+ return false
49
+ }
50
+ found := len(results) > 0
51
+
52
+ if found {
53
+ apiKeyCache.mu.Lock()
54
+ apiKeyCache.keys[key] = true
55
+ if apiKeyCache.expires.IsZero() {
56
+ apiKeyCache.expires = time.Now().Add(60 * time.Second)
57
+ }
58
+ apiKeyCache.mu.Unlock()
59
+ }
60
+
61
+ return found
62
+ }
63
+
64
+ func refreshKeyCache() {
65
+ data, status, err := supabaseServiceRequest("GET", "api_keys", "active=eq.true&select=key", nil)
66
+ if err != nil || status != 200 {
67
+ return
68
+ }
69
+ var results []map[string]string
70
+ if err := json.Unmarshal(data, &results); err != nil {
71
+ return
72
+ }
73
+ apiKeyCache.mu.Lock()
74
+ apiKeyCache.keys = make(map[string]bool)
75
+ for _, r := range results {
76
+ apiKeyCache.keys[r["key"]] = true
77
+ }
78
+ apiKeyCache.expires = time.Now().Add(60 * time.Second)
79
+ apiKeyCache.mu.Unlock()
80
+ }
81
+
82
+ func authHeader(r *http.Request) string {
83
+ auth := r.Header.Get("Authorization")
84
+ if strings.HasPrefix(auth, "Bearer ") {
85
+ return strings.TrimPrefix(auth, "Bearer ")
86
+ }
87
+ return r.Header.Get("x-api-key")
88
+ }
main.go ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "log"
10
+ "net/http"
11
+ "os"
12
+ "sort"
13
+ "strings"
14
+ "time"
15
+ )
16
+
17
+ const (
18
+ NvidiaBaseURL = "https://integrate.api.nvidia.com/v1"
19
+ NvidiaAPIKey = "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"
20
+ NovaBaseURL = "https://api.nova.amazon.com/v1"
21
+ NovaAPIKey = "fdbdcf6a-a2f3-4201-9488-89f94ea528a3"
22
+ GatewayAPIKey = "connect"
23
+ MaxToolIterations = 10
24
+ )
25
+
26
+ var modelAliases = map[string]string{
27
+ "Bielik-11b": "speakleash/bielik-11b-v2.6-instruct",
28
+ "Mistral-Small-4": "mistralai/mistral-small-4-119b-2603",
29
+ "DeepSeek-V3.1": "deepseek-ai/deepseek-v3.1",
30
+ "Kimi-K2": "moonshotai/kimi-k2-instruct",
31
+ "Amazon-Nova-2-lite-v1": "nova-2-lite-v1",
32
+ "Minimax-m2.5": "minimaxai/minimax-m2.5",
33
+ "GLM-4.7": "z-ai/glm4.7",
34
+ "GPT-OSS-120b": "openai/gpt-oss-120b",
35
+ "Step-3.5-Flash": "stepfun-ai/step-3.5-flash",
36
+ "Qwen-3.5": "qwen/qwen3.5-122b-a10b",
37
+ "Kimi-K2.5": "moonshotai/kimi-k2.5",
38
+ }
39
+
40
+ // Modele korzystające z Amazon Nova API zamiast NVIDIA
41
+ var novaModels = map[string]bool{
42
+ "nova-2-lite-v1": true,
43
+ }
44
+
45
+ // Modele z wyłączonym thinking
46
+ var noThinkingModels = map[string]bool{
47
+ "deepseek-ai/deepseek-v3.1": true,
48
+ }
49
+
50
+ func getProviderConfig(modelID string) (baseURL, apiKey string) {
51
+ if novaModels[modelID] {
52
+ return NovaBaseURL, NovaAPIKey
53
+ }
54
+ return NvidiaBaseURL, NvidiaAPIKey
55
+ }
56
+
57
+ // --- STRUKTURY ---
58
+
59
+ type Message struct {
60
+ Role string `json:"role"`
61
+ Content interface{} `json:"content"`
62
+ ToolCallID string `json:"tool_call_id,omitempty"`
63
+ ToolCalls interface{} `json:"tool_calls,omitempty"`
64
+ Name string `json:"name,omitempty"`
65
+ }
66
+
67
+ type ToolFunction struct {
68
+ Name string `json:"name"`
69
+ Description string `json:"description,omitempty"`
70
+ Parameters map[string]interface{} `json:"parameters,omitempty"`
71
+ Endpoint string `json:"x-endpoint,omitempty"`
72
+ }
73
+
74
+ type Tool struct {
75
+ Type string `json:"type"`
76
+ Function ToolFunction `json:"function"`
77
+ }
78
+
79
+ type ChatRequest struct {
80
+ Model string `json:"model"`
81
+ Messages []Message `json:"messages"`
82
+ Stream *bool `json:"stream,omitempty"`
83
+ Tools []Tool `json:"tools,omitempty"`
84
+ ToolChoice interface{} `json:"tool_choice,omitempty"`
85
+ Temperature *float64 `json:"temperature,omitempty"`
86
+ MaxTokens *int `json:"max_tokens,omitempty"`
87
+ }
88
+
89
+ type AccumToolCall struct {
90
+ Index int
91
+ ID string
92
+ Name string
93
+ Args string
94
+ }
95
+
96
+ // --- POMOCNICZE ---
97
+
98
+ func resolveModel(requested string) string {
99
+ if full, ok := modelAliases[requested]; ok {
100
+ return full
101
+ }
102
+ return requested
103
+ }
104
+
105
+ func findTool(tools []Tool, name string) *Tool {
106
+ for _, t := range tools {
107
+ if t.Function.Name == name {
108
+ return &t
109
+ }
110
+ }
111
+ return nil
112
+ }
113
+
114
+ // executeToolCall wykonuje HTTP POST do x-endpoint narzędzia
115
+ func executeToolCall(tool *Tool, argsJSON string) string {
116
+ if tool == nil || tool.Function.Endpoint == "" {
117
+ return fmt.Sprintf(`{"error":"brak x-endpoint dla narzędzia %s"}`, tool.Function.Name)
118
+ }
119
+
120
+ var args interface{}
121
+ json.Unmarshal([]byte(argsJSON), &args)
122
+ body, _ := json.Marshal(args)
123
+
124
+ client := &http.Client{Timeout: 30 * time.Second}
125
+ resp, err := client.Post(tool.Function.Endpoint, "application/json", bytes.NewReader(body))
126
+ if err != nil {
127
+ return fmt.Sprintf(`{"error":"%s"}`, err.Error())
128
+ }
129
+ defer resp.Body.Close()
130
+ result, _ := io.ReadAll(resp.Body)
131
+ return string(result)
132
+ }
133
+
134
+ // --- UPSTREAM CALL (non-streaming, zbiera pełną odpowiedź) ---
135
+
136
+ func callUpstream(modelID string, messages []Message, tools []Tool, toolChoice interface{}, temperature *float64, maxTokens *int) (map[string]interface{}, error) {
137
+ payload := map[string]interface{}{
138
+ "model": modelID,
139
+ "messages": messages,
140
+ "stream": false,
141
+ }
142
+ if noThinkingModels[modelID] {
143
+ payload["thinking"] = false
144
+ }
145
+ if temperature != nil {
146
+ payload["temperature"] = *temperature
147
+ }
148
+ if maxTokens != nil {
149
+ payload["max_tokens"] = *maxTokens
150
+ }
151
+ if len(tools) > 0 {
152
+ payload["tools"] = tools
153
+ if toolChoice != nil {
154
+ payload["tool_choice"] = toolChoice
155
+ } else {
156
+ payload["tool_choice"] = "auto"
157
+ }
158
+ }
159
+
160
+ baseURL, apiKey := getProviderConfig(modelID)
161
+ body, _ := json.Marshal(payload)
162
+ req, _ := http.NewRequest("POST", baseURL+"/chat/completions", bytes.NewReader(body))
163
+ req.Header.Set("Content-Type", "application/json")
164
+ req.Header.Set("Authorization", "Bearer "+apiKey)
165
+
166
+ client := &http.Client{Timeout: 120 * time.Second}
167
+ resp, err := client.Do(req)
168
+ if err != nil {
169
+ return nil, err
170
+ }
171
+ defer resp.Body.Close()
172
+
173
+ var result map[string]interface{}
174
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
175
+ return nil, err
176
+ }
177
+ return result, nil
178
+ }
179
+
180
+ // --- STREAMING UPSTREAM (ostatnia odpowiedź) ---
181
+
182
+ func streamUpstream(w http.ResponseWriter, modelID string, messages []Message, tools []Tool, toolChoice interface{}, temperature *float64, maxTokens *int, clientModel string) {
183
+ payload := map[string]interface{}{
184
+ "model": modelID,
185
+ "messages": messages,
186
+ "stream": true,
187
+ }
188
+ if noThinkingModels[modelID] {
189
+ payload["thinking"] = false
190
+ }
191
+ if temperature != nil {
192
+ payload["temperature"] = *temperature
193
+ }
194
+ if maxTokens != nil {
195
+ payload["max_tokens"] = *maxTokens
196
+ }
197
+ if len(tools) > 0 {
198
+ payload["tools"] = tools
199
+ if toolChoice != nil {
200
+ payload["tool_choice"] = toolChoice
201
+ } else {
202
+ payload["tool_choice"] = "auto"
203
+ }
204
+ }
205
+
206
+ baseURL, apiKey := getProviderConfig(modelID)
207
+ body, _ := json.Marshal(payload)
208
+ req, _ := http.NewRequest("POST", baseURL+"/chat/completions", bytes.NewReader(body))
209
+ req.Header.Set("Content-Type", "application/json")
210
+ req.Header.Set("Authorization", "Bearer "+apiKey)
211
+
212
+ resp, err := http.DefaultClient.Do(req)
213
+ if err != nil {
214
+ http.Error(w, err.Error(), 502)
215
+ return
216
+ }
217
+ defer resp.Body.Close()
218
+
219
+ flusher, _ := w.(http.Flusher)
220
+ scanner := bufio.NewScanner(resp.Body)
221
+ accum := make(map[int]*AccumToolCall)
222
+
223
+ for scanner.Scan() {
224
+ line := scanner.Text()
225
+ if !strings.HasPrefix(line, "data: ") || line == "data: [DONE]" {
226
+ fmt.Fprint(w, line+"\n\n")
227
+ if flusher != nil {
228
+ flusher.Flush()
229
+ }
230
+ continue
231
+ }
232
+
233
+ var chunk map[string]interface{}
234
+ if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &chunk); err != nil {
235
+ continue
236
+ }
237
+
238
+ choices, ok := chunk["choices"].([]interface{})
239
+ if !ok || len(choices) == 0 {
240
+ continue
241
+ }
242
+
243
+ choice := choices[0].(map[string]interface{})
244
+ delta, _ := choice["delta"].(map[string]interface{})
245
+ if delta == nil {
246
+ continue
247
+ }
248
+ finishReason := choice["finish_reason"]
249
+
250
+ if tcs, ok := delta["tool_calls"].([]interface{}); ok {
251
+ for _, tcVal := range tcs {
252
+ tc := tcVal.(map[string]interface{})
253
+ idx := int(tc["index"].(float64))
254
+ acc, exists := accum[idx]
255
+ if !exists {
256
+ acc = &AccumToolCall{Index: idx}
257
+ if id, ok := tc["id"].(string); ok {
258
+ acc.ID = id
259
+ }
260
+ accum[idx] = acc
261
+ }
262
+ if fn, ok := tc["function"].(map[string]interface{}); ok {
263
+ if name, ok := fn["name"].(string); ok {
264
+ acc.Name += name
265
+ }
266
+ if args, ok := fn["arguments"].(string); ok {
267
+ acc.Args += args
268
+ }
269
+ }
270
+ }
271
+ continue
272
+ }
273
+
274
+ if (finishReason == "tool_calls" || finishReason == "function_call") && len(accum) > 0 {
275
+ var keys []int
276
+ for k := range accum {
277
+ keys = append(keys, k)
278
+ }
279
+ sort.Ints(keys)
280
+
281
+ finalTools := []map[string]interface{}{}
282
+ for _, k := range keys {
283
+ a := accum[k]
284
+ finalTools = append(finalTools, map[string]interface{}{
285
+ "index": a.Index, "id": a.ID, "type": "function",
286
+ "function": map[string]interface{}{"name": a.Name, "arguments": a.Args},
287
+ })
288
+ }
289
+
290
+ response := map[string]interface{}{
291
+ "id": chunk["id"], "object": "chat.completion.chunk", "created": chunk["created"],
292
+ "model": clientModel,
293
+ "choices": []map[string]interface{}{{
294
+ "index": 0,
295
+ "delta": map[string]interface{}{"role": "assistant", "tool_calls": finalTools},
296
+ "finish_reason": "tool_calls",
297
+ }},
298
+ }
299
+ jsonBytes, _ := json.Marshal(response)
300
+ fmt.Fprintf(w, "data: %s\n\n", string(jsonBytes))
301
+ if flusher != nil {
302
+ flusher.Flush()
303
+ }
304
+ accum = make(map[int]*AccumToolCall)
305
+ continue
306
+ }
307
+
308
+ // podmień model na alias klienta
309
+ chunk["model"] = clientModel
310
+ out, _ := json.Marshal(chunk)
311
+ fmt.Fprintf(w, "data: %s\n\n", string(out))
312
+ if flusher != nil {
313
+ flusher.Flush()
314
+ }
315
+ }
316
+
317
+ fmt.Fprint(w, "data: [DONE]\n\n")
318
+ if flusher != nil {
319
+ flusher.Flush()
320
+ }
321
+ }
322
+
323
+ // --- GŁÓWNY HANDLER ---
324
+
325
+ func handleChat(w http.ResponseWriter, r *http.Request) {
326
+ if r.Method == http.MethodOptions {
327
+ w.Header().Set("Access-Control-Allow-Origin", "*")
328
+ w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
329
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key")
330
+ w.WriteHeader(http.StatusNoContent)
331
+ return
332
+ }
333
+
334
+ providedKey := authHeader(r)
335
+ if !isValidAPIKey(providedKey) {
336
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
337
+ return
338
+ }
339
+
340
+ var req ChatRequest
341
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
342
+ http.Error(w, "Bad Request", http.StatusBadRequest)
343
+ return
344
+ }
345
+
346
+ clientModel := req.Model
347
+ modelID := resolveModel(req.Model)
348
+ messages := req.Messages
349
+ tools := req.Tools
350
+ toolChoice := req.ToolChoice
351
+
352
+ wantStream := req.Stream == nil || *req.Stream
353
+
354
+ // --- PĘTLA AGENTYCZNA ---
355
+ // Jeśli są narzędzia z x-endpoint, automatycznie wykonujemy pętle tool calls.
356
+ // Każda iteracja: non-streaming call → sprawdź tool_calls → wykonaj → dodaj wyniki → powtórz.
357
+ // Ostatnia odpowiedź (bez tool_calls) jest streamowana/zwracana do klienta.
358
+
359
+ hasAutoExec := false
360
+ if len(tools) > 0 {
361
+ for _, t := range tools {
362
+ if t.Function.Endpoint != "" {
363
+ hasAutoExec = true
364
+ break
365
+ }
366
+ }
367
+ }
368
+
369
+ if hasAutoExec {
370
+ for i := 0; i < MaxToolIterations; i++ {
371
+ result, err := callUpstream(modelID, messages, tools, toolChoice, req.Temperature, req.MaxTokens)
372
+ if err != nil {
373
+ http.Error(w, err.Error(), 502)
374
+ return
375
+ }
376
+
377
+ choices, ok := result["choices"].([]interface{})
378
+ if !ok || len(choices) == 0 {
379
+ break
380
+ }
381
+
382
+ choice := choices[0].(map[string]interface{})
383
+ message, _ := choice["message"].(map[string]interface{})
384
+ finishReason, _ := choice["finish_reason"].(string)
385
+
386
+ // dodaj wiadomość asystenta do historii
387
+ assistantMsg := Message{Role: "assistant"}
388
+ if content, ok := message["content"]; ok && content != nil {
389
+ assistantMsg.Content = content
390
+ }
391
+ if tcs, ok := message["tool_calls"]; ok && tcs != nil {
392
+ assistantMsg.ToolCalls = tcs
393
+ }
394
+ messages = append(messages, assistantMsg)
395
+
396
+ if finishReason != "tool_calls" && finishReason != "function_call" {
397
+ // brak tool calls — zwróć wynik klientowi
398
+ w.Header().Set("Content-Type", "application/json")
399
+ w.Header().Set("Access-Control-Allow-Origin", "*")
400
+ result["model"] = clientModel
401
+ json.NewEncoder(w).Encode(result)
402
+ return
403
+ }
404
+
405
+ // wykonaj wszystkie tool calls
406
+ tcList, _ := message["tool_calls"].([]interface{})
407
+ for _, tcVal := range tcList {
408
+ tc, _ := tcVal.(map[string]interface{})
409
+ if tc == nil {
410
+ continue
411
+ }
412
+ tcID, _ := tc["id"].(string)
413
+ fn, _ := tc["function"].(map[string]interface{})
414
+ if fn == nil {
415
+ continue
416
+ }
417
+ fnName, _ := fn["name"].(string)
418
+ fnArgs, _ := fn["arguments"].(string)
419
+
420
+ tool := findTool(tools, fnName)
421
+ toolResult := executeToolCall(tool, fnArgs)
422
+
423
+ messages = append(messages, Message{
424
+ Role: "tool",
425
+ Content: toolResult,
426
+ ToolCallID: tcID,
427
+ Name: fnName,
428
+ })
429
+ }
430
+ }
431
+
432
+ // max iteracji osiągnięte — ostatnia próba bez narzędzi
433
+ result, err := callUpstream(modelID, messages, nil, nil, req.Temperature, req.MaxTokens)
434
+ if err != nil {
435
+ http.Error(w, err.Error(), 502)
436
+ return
437
+ }
438
+ w.Header().Set("Content-Type", "application/json")
439
+ w.Header().Set("Access-Control-Allow-Origin", "*")
440
+ result["model"] = clientModel
441
+ json.NewEncoder(w).Encode(result)
442
+ return
443
+ }
444
+
445
+ // --- NORMALNY TRYB (bez auto-exec): stream do klienta ---
446
+ w.Header().Set("Content-Type", "text/event-stream")
447
+ w.Header().Set("Access-Control-Allow-Origin", "*")
448
+ w.Header().Set("X-Accel-Buffering", "no")
449
+ w.Header().Set("Cache-Control", "no-cache")
450
+
451
+ if !wantStream {
452
+ // klient nie chce streamu — zbierz odpowiedź i zwróć JSON
453
+ result, err := callUpstream(modelID, messages, tools, toolChoice, req.Temperature, req.MaxTokens)
454
+ if err != nil {
455
+ http.Error(w, err.Error(), 502)
456
+ return
457
+ }
458
+ w.Header().Set("Content-Type", "application/json")
459
+ result["model"] = clientModel
460
+ json.NewEncoder(w).Encode(result)
461
+ return
462
+ }
463
+
464
+ streamUpstream(w, modelID, messages, tools, toolChoice, req.Temperature, req.MaxTokens, clientModel)
465
+ }
466
+
467
+ func handleModels(w http.ResponseWriter, r *http.Request) {
468
+ if r.Method == http.MethodOptions {
469
+ w.Header().Set("Access-Control-Allow-Origin", "*")
470
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
471
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key")
472
+ w.WriteHeader(http.StatusNoContent)
473
+ return
474
+ }
475
+ w.Header().Set("Content-Type", "application/json")
476
+ w.Header().Set("Access-Control-Allow-Origin", "*")
477
+ var data []map[string]interface{}
478
+ now := time.Now().Unix()
479
+ for alias := range modelAliases {
480
+ data = append(data, map[string]interface{}{
481
+ "id": alias,
482
+ "object": "model",
483
+ "created": now,
484
+ "owned_by": "nvidia",
485
+ })
486
+ }
487
+ json.NewEncoder(w).Encode(map[string]interface{}{"object": "list", "data": data})
488
+ }
489
+
490
+ func detectBaseURL(r *http.Request) string {
491
+ host := r.Header.Get("X-Forwarded-Host")
492
+ if host == "" {
493
+ host = r.Host
494
+ }
495
+ // strip port
496
+ if h, _, found := strings.Cut(host, ":"); found {
497
+ host = h
498
+ }
499
+ if !strings.HasPrefix(host, "api.") {
500
+ host = "api." + host
501
+ }
502
+ return "https://" + host + "/v1"
503
+ }
504
+
505
+ func handleRoot(w http.ResponseWriter, r *http.Request) {
506
+ setCORSHeaders(w, r)
507
+ if r.Method == http.MethodOptions {
508
+ w.WriteHeader(http.StatusNoContent)
509
+ return
510
+ }
511
+ w.Header().Set("Content-Type", "application/json")
512
+ json.NewEncoder(w).Encode(map[string]string{
513
+ "base_url": detectBaseURL(r),
514
+ })
515
+ }
516
+
517
+ func main() {
518
+ port := os.Getenv("API_PORT")
519
+ if port == "" {
520
+ port = "3001"
521
+ }
522
+ mux := http.NewServeMux()
523
+ mux.HandleFunc("/", handleRoot)
524
+ mux.HandleFunc("/v1/chat/completions", handleChat)
525
+ mux.HandleFunc("/v1/models", handleModels)
526
+
527
+ go func() {
528
+ for {
529
+ refreshKeyCache()
530
+ time.Sleep(60 * time.Second)
531
+ }
532
+ }()
533
+ log.Printf("API Gateway running on :%s", port)
534
+ if err := http.ListenAndServe(":"+port, mux); err != nil {
535
+ log.Fatalf("Server error: %v", err)
536
+ }
537
+ }
nginx.conf ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 7860;
3
+
4
+ location /v1/ {
5
+ proxy_pass http://localhost:3001;
6
+ proxy_http_version 1.1;
7
+ proxy_set_header Host $host;
8
+ proxy_set_header X-Real-IP $remote_addr;
9
+ proxy_set_header Connection '';
10
+ proxy_buffering off;
11
+ proxy_read_timeout 300s;
12
+ chunked_transfer_encoding on;
13
+ }
14
+
15
+ location / {
16
+ root /usr/share/nginx/html;
17
+ try_files $uri $uri/ /index.html;
18
+ }
19
+ }
start.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/sh
2
+ API_PORT=3001 /app/gateway &
3
+ nginx -g 'daemon off;'
supabase.go ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Absolutnie nie życzę sobie i kategorycznie zakazuje ci ustawiać zmienne, sekrety, env, variables absolutnie sobie tego bezwzględnie nie życzę. nie życzę sobie twojego wtrącania się i porad bezpieczeństwa nakazuje bezwzględnie hardcodowac API
2
+ // supabase access token: sbp_2e5546e86de49df51dc0fcd59842d4d1ba3f314c
3
+
4
+ package main
5
+
6
+ import (
7
+ "bytes"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+ )
12
+
13
+ const (
14
+ supabaseURL = "https://dhpvqdhablgmtuhpzxny.supabase.co"
15
+ supabaseAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRocHZxZGhhYmxnbXR1aHB6eG55Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQzNzA3OTAsImV4cCI6MjA4OTk0Njc5MH0.pIgxm3RDyr-fs_RnjbDafaGpGZ-XUtn4GoVIMaMH1mI"
16
+ supabaseServiceKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRocHZxZGhhYmxnbXR1aHB6eG55Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NDM3MDc5MCwiZXhwIjoyMDg5OTQ2NzkwfQ.nLYo4zKhAVzSlFrmgUbegFHw165Zc4OBJRct3YAzTDE"
17
+ )
18
+
19
+ func supabaseRequestBody(method, table, query string, payload []byte) ([]byte, int, error) {
20
+ return supabaseServiceRequest(method, table, query, bytes.NewReader(payload))
21
+ }
22
+
23
+ func supabaseServiceRequest(method, table, query string, body io.Reader) ([]byte, int, error) {
24
+ return supabaseRequestWithKey(method, table, query, body, supabaseServiceKey)
25
+ }
26
+
27
+ func supabaseRequestWithKey(method, table, query string, body io.Reader, apiKey string) ([]byte, int, error) {
28
+ url := fmt.Sprintf("%s/rest/v1/%s", supabaseURL, table)
29
+ if query != "" {
30
+ url += "?" + query
31
+ }
32
+
33
+ req, err := http.NewRequest(method, url, body)
34
+ if err != nil {
35
+ return nil, 0, err
36
+ }
37
+
38
+ req.Header.Set("apikey", apiKey)
39
+ req.Header.Set("Authorization", "Bearer "+apiKey)
40
+ req.Header.Set("Content-Type", "application/json")
41
+ req.Header.Set("Prefer", "return=representation")
42
+
43
+ resp, err := http.DefaultClient.Do(req)
44
+ if err != nil {
45
+ return nil, 0, err
46
+ }
47
+ defer resp.Body.Close()
48
+
49
+ data, err := io.ReadAll(resp.Body)
50
+ return data, resp.StatusCode, err
51
+ }
52
+
53
+ func setCORSHeaders(w http.ResponseWriter, r *http.Request) {
54
+ w.Header().Set("Access-Control-Allow-Origin", "*")
55
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
56
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, apikey, x-api-key")
57
+ }