armand0e commited on
Commit
3bad34d
·
1 Parent(s): 6593c28

Add Supabase REST helper + schema; HF models route

Browse files
src/app/api/huggingface-models/route.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ interface HuggingFaceModel {
4
+ id?: string;
5
+ modelId?: string;
6
+ tags?: string[];
7
+ }
8
+
9
+ type ModelOption = { id: string; name: string };
10
+
11
+ type CacheEntry = {
12
+ timestamp: number;
13
+ data: ModelOption[];
14
+ };
15
+
16
+ const CACHE_DURATION = 1000 * 60 * 5; // 5 minutes
17
+ const cache = new Map<string, CacheEntry>();
18
+
19
+ function getCacheKey(query: string, limit: number) {
20
+ return `${query}::${limit}`;
21
+ }
22
+
23
+ function normalizeModelId(model: HuggingFaceModel) {
24
+ return model.id || model.modelId || "";
25
+ }
26
+
27
+ function hasTag(model: HuggingFaceModel, tag: string) {
28
+ return Array.isArray(model.tags) && model.tags.includes(tag);
29
+ }
30
+
31
+ function isExcludedFormat(model: HuggingFaceModel) {
32
+ if (!Array.isArray(model.tags)) return false;
33
+ const tags = new Set(model.tags);
34
+ if (tags.has("gguf")) return true;
35
+ if (tags.has("mlx")) return true;
36
+ if (tags.has("ggml")) return true;
37
+ return false;
38
+ }
39
+
40
+ export async function GET(request: NextRequest) {
41
+ const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
42
+ const limitParam = Number(request.nextUrl.searchParams.get("limit") ?? "20");
43
+ const limit = Number.isFinite(limitParam) ? Math.max(1, Math.min(50, limitParam)) : 20;
44
+
45
+ const cacheKey = getCacheKey(query.toLowerCase(), limit);
46
+ const now = Date.now();
47
+
48
+ const cached = cache.get(cacheKey);
49
+ if (cached && now - cached.timestamp < CACHE_DURATION) {
50
+ return NextResponse.json(cached.data);
51
+ }
52
+
53
+ try {
54
+ const url = new URL("https://huggingface.co/api/models");
55
+
56
+ if (query) {
57
+ url.searchParams.set("search", query);
58
+ } else {
59
+ url.searchParams.set("sort", "downloads");
60
+ url.searchParams.set("direction", "-1");
61
+ }
62
+
63
+ // Restrict to models that have the `safetensors` tag.
64
+ url.searchParams.append("filter", "safetensors");
65
+
66
+ url.searchParams.set("limit", String(limit));
67
+
68
+ const response = await fetch(url.toString(), {
69
+ headers: {
70
+ Accept: "application/json",
71
+ },
72
+ next: { revalidate: 300 },
73
+ });
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`Hugging Face API error: ${response.status}`);
77
+ }
78
+
79
+ const data = (await response.json()) as HuggingFaceModel[];
80
+
81
+ const seen = new Set<string>();
82
+ const models: ModelOption[] = Array.isArray(data)
83
+ ? data
84
+ .filter((m) => hasTag(m, "safetensors") && !isExcludedFormat(m))
85
+ .map((m) => normalizeModelId(m))
86
+ .filter(Boolean)
87
+ .filter((id) => {
88
+ if (seen.has(id)) return false;
89
+ seen.add(id);
90
+ return true;
91
+ })
92
+ .map((id) => ({
93
+ id,
94
+ name: id,
95
+ }))
96
+ : [];
97
+
98
+ cache.set(cacheKey, { timestamp: now, data: models });
99
+
100
+ return NextResponse.json(models);
101
+ } catch (error) {
102
+ console.error("Error fetching Hugging Face models:", error);
103
+ if (cached) {
104
+ return NextResponse.json(cached.data);
105
+ }
106
+ return NextResponse.json([], { status: 500 });
107
+ }
108
+ }
src/lib/supabaseRest.ts ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ type SupabaseEnv = {
2
+ url: string;
3
+ anonKey: string;
4
+ serviceRoleKey?: string;
5
+ };
6
+
7
+ function getSupabaseEnv(): SupabaseEnv {
8
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
9
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY || "";
10
+ const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || "";
11
+
12
+ if (!url || !anonKey) {
13
+ throw new Error("Supabase env vars missing: NEXT_PUBLIC_SUPABASE_URL and/or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY");
14
+ }
15
+
16
+ return { url, anonKey, serviceRoleKey: serviceRoleKey || undefined };
17
+ }
18
+
19
+ export type SupabaseRestError = {
20
+ message: string;
21
+ details?: string;
22
+ hint?: string;
23
+ code?: string;
24
+ };
25
+
26
+ export async function supabaseRest<T>(
27
+ path: string,
28
+ init: RequestInit & {
29
+ query?: Record<string, string | number | boolean | undefined | null>;
30
+ preferReturn?: "representation" | "minimal";
31
+ acceptObject?: boolean;
32
+ count?: "exact" | "planned" | "estimated";
33
+ } = {}
34
+ ): Promise<{ data: T | null; error: SupabaseRestError | null; status: number; count: number | null }> {
35
+ const { url, anonKey, serviceRoleKey } = getSupabaseEnv();
36
+ const apiKey = serviceRoleKey || anonKey;
37
+
38
+ const endpoint = new URL(path.startsWith("/") ? path : `/${path}`, url);
39
+ if (init.query) {
40
+ for (const [k, v] of Object.entries(init.query)) {
41
+ if (v === undefined || v === null) continue;
42
+ endpoint.searchParams.set(k, String(v));
43
+ }
44
+ }
45
+
46
+ const headers = new Headers(init.headers);
47
+ headers.set("apikey", apiKey);
48
+ headers.set("Authorization", `Bearer ${apiKey}`);
49
+
50
+ if (!headers.has("Content-Type") && init.body) {
51
+ headers.set("Content-Type", "application/json");
52
+ }
53
+
54
+ if (init.preferReturn) {
55
+ const existing = headers.get("Prefer");
56
+ const next = `return=${init.preferReturn}`;
57
+ headers.set("Prefer", existing ? `${existing}, ${next}` : next);
58
+ }
59
+
60
+ if (init.count) {
61
+ const existing = headers.get("Prefer");
62
+ const next = `count=${init.count}`;
63
+ headers.set("Prefer", existing ? `${existing}, ${next}` : next);
64
+ }
65
+
66
+ if (init.acceptObject) {
67
+ headers.set("Accept", "application/vnd.pgrst.object+json");
68
+ }
69
+
70
+ const res = await fetch(endpoint.toString(), {
71
+ ...init,
72
+ headers,
73
+ });
74
+
75
+ const status = res.status;
76
+
77
+ const contentRange = res.headers.get("content-range") || res.headers.get("Content-Range");
78
+ let count: number | null = null;
79
+ if (contentRange) {
80
+ const slash = contentRange.lastIndexOf("/");
81
+ if (slash !== -1) {
82
+ const total = Number(contentRange.slice(slash + 1));
83
+ if (Number.isFinite(total)) count = total;
84
+ }
85
+ }
86
+
87
+ const text = await res.text();
88
+
89
+ if (!res.ok) {
90
+ let err: SupabaseRestError = { message: `Supabase REST error: ${status}` };
91
+ try {
92
+ const parsed = JSON.parse(text);
93
+ if (parsed && typeof parsed === "object") {
94
+ err = {
95
+ message: typeof (parsed as any).message === "string" ? (parsed as any).message : err.message,
96
+ details: typeof (parsed as any).details === "string" ? (parsed as any).details : undefined,
97
+ hint: typeof (parsed as any).hint === "string" ? (parsed as any).hint : undefined,
98
+ code: typeof (parsed as any).code === "string" ? (parsed as any).code : undefined,
99
+ };
100
+ }
101
+ } catch {
102
+ if (text) err = { message: text };
103
+ }
104
+ return { data: null, error: err, status, count };
105
+ }
106
+
107
+ if (!text) {
108
+ return { data: null, error: null, status, count };
109
+ }
110
+
111
+ try {
112
+ return { data: JSON.parse(text) as T, error: null, status, count };
113
+ } catch {
114
+ return { data: null, error: null, status, count };
115
+ }
116
+ }
supabase/schema.sql ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ create extension if not exists pgcrypto;
2
+
3
+ create table if not exists public.distillation_requests (
4
+ id uuid primary key default gen_random_uuid (),
5
+ source_dataset text not null,
6
+ student_model text not null,
7
+ additional_notes text not null default '',
8
+ upvotes integer not null default 0,
9
+ voted_ips text [] not null default '{}',
10
+ owner_id text not null,
11
+ created_at timestamptz not null default now(),
12
+ status text not null default 'pending'
13
+ );
14
+
15
+ create table if not exists public.dataset_requests (
16
+ id uuid primary key default gen_random_uuid (),
17
+ source_model text not null,
18
+ dataset_size text not null default '250x',
19
+ reasoning_depth text not null default 'high',
20
+ topics text [] not null default '{}',
21
+ additional_notes text not null default '',
22
+ upvotes integer not null default 0,
23
+ voted_ips text [] not null default '{}',
24
+ owner_id text not null,
25
+ created_at timestamptz not null default now(),
26
+ status text not null default 'pending'
27
+ );
28
+
29
+ create table if not exists public.request_comments (
30
+ id uuid primary key default gen_random_uuid (),
31
+ request_type text not null check (
32
+ request_type in ('distillation', 'dataset')
33
+ ),
34
+ request_id uuid not null,
35
+ body text not null,
36
+ author text not null,
37
+ role text not null check (role in ('admin', 'user')),
38
+ owner_id text not null,
39
+ created_at timestamptz not null default now(),
40
+ edited_at timestamptz null
41
+ );
42
+
43
+ create index if not exists idx_request_comments_request on public.request_comments (request_type, request_id);
44
+
45
+ create index if not exists idx_request_comments_created_at on public.request_comments (created_at);
46
+
47
+ create or replace function public.toggle_upvote_distillation(request_id uuid, voter_ip text)
48
+ returns table(success boolean, upvotes integer, action text)
49
+ language plpgsql
50
+ security definer
51
+ as $$
52
+ declare
53
+ current_ips text[];
54
+ current_upvotes integer;
55
+ begin
56
+ select r.voted_ips, r.upvotes
57
+ into current_ips, current_upvotes
58
+ from public.distillation_requests r
59
+ where r.id = request_id
60
+ for update;
61
+
62
+ if not found then
63
+ success := false;
64
+ upvotes := 0;
65
+ action := null;
66
+ return next;
67
+ return;
68
+ end if;
69
+
70
+ if voter_ip = any(current_ips) then
71
+ update public.distillation_requests
72
+ set
73
+ voted_ips = array_remove(voted_ips, voter_ip),
74
+ upvotes = greatest(upvotes - 1, 0)
75
+ where id = request_id
76
+ returning public.distillation_requests.upvotes into upvotes;
77
+
78
+ success := true;
79
+ action := 'unvoted';
80
+ return next;
81
+ return;
82
+ end if;
83
+
84
+ update public.distillation_requests
85
+ set
86
+ voted_ips = array_append(voted_ips, voter_ip),
87
+ upvotes = upvotes + 1
88
+ where id = request_id
89
+ returning public.distillation_requests.upvotes into upvotes;
90
+
91
+ success := true;
92
+ action := 'upvoted';
93
+ return next;
94
+ end;
95
+ $$;
96
+
97
+ create or replace function public.toggle_upvote_dataset(request_id uuid, voter_ip text)
98
+ returns table(success boolean, upvotes integer, action text)
99
+ language plpgsql
100
+ security definer
101
+ as $$
102
+ declare
103
+ current_ips text[];
104
+ current_upvotes integer;
105
+ begin
106
+ select r.voted_ips, r.upvotes
107
+ into current_ips, current_upvotes
108
+ from public.dataset_requests r
109
+ where r.id = request_id
110
+ for update;
111
+
112
+ if not found then
113
+ success := false;
114
+ upvotes := 0;
115
+ action := null;
116
+ return next;
117
+ return;
118
+ end if;
119
+
120
+ if voter_ip = any(current_ips) then
121
+ update public.dataset_requests
122
+ set
123
+ voted_ips = array_remove(voted_ips, voter_ip),
124
+ upvotes = greatest(upvotes - 1, 0)
125
+ where id = request_id
126
+ returning public.dataset_requests.upvotes into upvotes;
127
+
128
+ success := true;
129
+ action := 'unvoted';
130
+ return next;
131
+ return;
132
+ end if;
133
+
134
+ update public.dataset_requests
135
+ set
136
+ voted_ips = array_append(voted_ips, voter_ip),
137
+ upvotes = upvotes + 1
138
+ where id = request_id
139
+ returning public.dataset_requests.upvotes into upvotes;
140
+
141
+ success := true;
142
+ action := 'upvoted';
143
+ return next;
144
+ end;
145
+ $$;
146
+
147
+ alter table public.distillation_requests enable row level security;
148
+
149
+ alter table public.dataset_requests enable row level security;
150
+
151
+ alter table public.request_comments enable row level security;
152
+
153
+ drop policy if exists public_select on public.distillation_requests;
154
+
155
+ drop policy if exists public_insert on public.distillation_requests;
156
+
157
+ drop policy if exists public_update on public.distillation_requests;
158
+
159
+ drop policy if exists public_delete on public.distillation_requests;
160
+
161
+ drop policy if exists public_select on public.dataset_requests;
162
+
163
+ drop policy if exists public_insert on public.dataset_requests;
164
+
165
+ drop policy if exists public_update on public.dataset_requests;
166
+
167
+ drop policy if exists public_delete on public.dataset_requests;
168
+
169
+ drop policy if exists public_select on public.request_comments;
170
+
171
+ drop policy if exists public_insert on public.request_comments;
172
+
173
+ drop policy if exists public_update on public.request_comments;
174
+
175
+ drop policy if exists public_delete on public.request_comments;
176
+
177
+ create policy public_select on public.distillation_requests for
178
+ select using (true);
179
+
180
+ create policy public_insert on public.distillation_requests for insert
181
+ with
182
+ check (true);
183
+
184
+ create policy public_select on public.dataset_requests for
185
+ select using (true);
186
+
187
+ create policy public_insert on public.dataset_requests for insert
188
+ with
189
+ check (true);
190
+
191
+ create policy public_select on public.request_comments for
192
+ select using (true);
193
+
194
+ create policy public_insert on public.request_comments for insert
195
+ with
196
+ check (role = 'user');
197
+
198
+ grant
199
+ select, insert on table public.distillation_requests to anon, authenticated;
200
+
201
+ grant
202
+ select, insert on table public.dataset_requests to anon, authenticated;
203
+
204
+ grant
205
+ select, insert on table public.request_comments to anon, authenticated;
206
+
207
+ grant
208
+ execute on function public.toggle_upvote_distillation (uuid, text) to anon,
209
+ authenticated;
210
+
211
+ grant
212
+ execute on function public.toggle_upvote_dataset (uuid, text) to anon,
213
+ authenticated;