Thomas G. Lopes commited on
Commit
af1f386
·
1 Parent(s): 52c6f5c

switch models api route to use remote functions

Browse files
src/{routes/api/models/+server.ts → lib/remote/models.remote.ts} RENAMED
@@ -1,6 +1,40 @@
1
- import type { Model } from "$lib/types.js";
2
- import { json } from "@sveltejs/kit";
3
- import type { RequestHandler } from "./$types.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  enum CacheStatus {
6
  SUCCESS = "success",
@@ -12,8 +46,7 @@ type Cache = {
12
  data: Model[] | undefined;
13
  timestamp: number;
14
  status: CacheStatus;
15
- // Track failed models to selectively refetch them
16
- failedTokenizers: string[]; // Using array instead of Set for serialization compatibility
17
  failedApiCalls: {
18
  textGeneration: boolean;
19
  imageTextToText: boolean;
@@ -31,9 +64,8 @@ const cache: Cache = {
31
  },
32
  };
33
 
34
- // The time between cache refreshes
35
  const FULL_CACHE_REFRESH = 1000 * 60 * 60; // 1 hour
36
- const PARTIAL_CACHE_REFRESH = 1000 * 60 * 15; // 15 minutes (shorter for partial results)
37
 
38
  const headers: HeadersInit = {
39
  "Upgrade-Insecure-Requests": "1",
@@ -73,14 +105,12 @@ const baseUrl = "https://huggingface.co/api/models";
73
  function buildApiUrl(params: ApiQueryParams): string {
74
  const url = new URL(baseUrl);
75
 
76
- // Add simple params
77
  Object.entries(params).forEach(([key, value]) => {
78
  if (!Array.isArray(value) && value !== undefined) {
79
  url.searchParams.append(key, String(value));
80
  }
81
  });
82
 
83
- // Handle array params specially
84
  params.expand.forEach(item => {
85
  url.searchParams.append("expand[]", item);
86
  });
@@ -88,10 +118,7 @@ function buildApiUrl(params: ApiQueryParams): string {
88
  return url.toString();
89
  }
90
 
91
- async function fetchAllModelsWithPagination(
92
- pipeline_tag: "text-generation" | "image-text-to-text",
93
- fetch: typeof globalThis.fetch,
94
- ): Promise<Model[]> {
95
  const allModels: Model[] = [];
96
  let skip = 0;
97
  const batchSize = 1000;
@@ -113,46 +140,33 @@ async function fetchAllModelsWithPagination(
113
  const models: Model[] = await response.json();
114
 
115
  if (models.length === 0) {
116
- break; // No more models to fetch
117
  }
118
 
119
  allModels.push(...models);
120
  skip += batchSize;
121
 
122
- // Optional: Add a small delay to be respectful to the API
123
  await new Promise(resolve => setTimeout(resolve, 100));
124
  }
125
 
126
  return allModels;
127
  }
128
 
129
- export type ApiModelsResponse = {
130
- models: Model[];
131
- };
132
-
133
- function createResponse(data: ApiModelsResponse): Response {
134
- return json(data);
135
- }
136
-
137
- export const GET: RequestHandler = async ({ fetch }) => {
138
  const timestamp = Date.now();
139
 
140
- // Determine if cache is valid
141
  const elapsed = timestamp - cache.timestamp;
142
  const cacheRefreshTime = cache.status === CacheStatus.SUCCESS ? FULL_CACHE_REFRESH : PARTIAL_CACHE_REFRESH;
143
 
144
- // Use cache if it's still valid and has data
145
  if (elapsed < cacheRefreshTime && cache.data?.length) {
146
- console.log(`Using ${cache.status} cache (${Math.floor(elapsed / 1000 / 60)} min old)`);
147
- return createResponse({ models: cache.data });
148
  }
149
 
150
  try {
151
- // Determine which API calls we need to make based on cache status
152
  const needTextGenFetch = elapsed >= FULL_CACHE_REFRESH || cache.failedApiCalls.textGeneration;
153
  const needImgTextFetch = elapsed >= FULL_CACHE_REFRESH || cache.failedApiCalls.imageTextToText;
154
 
155
- // Track the existing models we'll keep
156
  const existingModels = new Map<string, Model>();
157
  if (cache.data) {
158
  cache.data.forEach(model => {
@@ -160,27 +174,24 @@ export const GET: RequestHandler = async ({ fetch }) => {
160
  });
161
  }
162
 
163
- // Initialize new tracking for failed requests
164
  const newFailedTokenizers: string[] = [];
165
  const newFailedApiCalls = {
166
  textGeneration: false,
167
  imageTextToText: false,
168
  };
169
 
170
- // Fetch models as needed
171
  let textGenModels: Model[] = [];
172
  let imgText2TextModels: Model[] = [];
173
 
174
- // Make the needed API calls in parallel
175
  const apiPromises: Promise<void>[] = [];
176
  if (needTextGenFetch) {
177
  apiPromises.push(
178
- fetchAllModelsWithPagination("text-generation", fetch)
179
  .then(models => {
180
  textGenModels = models;
181
  })
182
  .catch(error => {
183
- console.error(`Error fetching text-generation models:`, error);
184
  newFailedApiCalls.textGeneration = true;
185
  }),
186
  );
@@ -188,12 +199,12 @@ export const GET: RequestHandler = async ({ fetch }) => {
188
 
189
  if (needImgTextFetch) {
190
  apiPromises.push(
191
- fetchAllModelsWithPagination("image-text-to-text", fetch)
192
  .then(models => {
193
  imgText2TextModels = models;
194
  })
195
  .catch(error => {
196
- console.error(`Error fetching image-text-to-text models:`, error);
197
  newFailedApiCalls.imageTextToText = true;
198
  }),
199
  );
@@ -201,7 +212,6 @@ export const GET: RequestHandler = async ({ fetch }) => {
201
 
202
  await Promise.all(apiPromises);
203
 
204
- // If both needed API calls failed and we have cached data, use it
205
  if (
206
  needTextGenFetch &&
207
  newFailedApiCalls.textGeneration &&
@@ -209,14 +219,13 @@ export const GET: RequestHandler = async ({ fetch }) => {
209
  newFailedApiCalls.imageTextToText &&
210
  cache.data?.length
211
  ) {
212
- console.log("All API requests failed. Using existing cache as fallback.");
213
  cache.status = CacheStatus.ERROR;
214
- cache.timestamp = timestamp; // Update timestamp to avoid rapid retry loops
215
  cache.failedApiCalls = newFailedApiCalls;
216
- return createResponse({ models: cache.data });
217
  }
218
 
219
- // For API calls we didn't need to make, use cached models
220
  if (!needTextGenFetch && cache.data) {
221
  textGenModels = cache.data.filter(model => model.pipeline_tag === "text-generation").map(model => model as Model);
222
  }
@@ -232,9 +241,7 @@ export const GET: RequestHandler = async ({ fetch }) => {
232
  );
233
  models.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
234
 
235
- // Determine cache status based on failures
236
  const hasApiFailures = newFailedApiCalls.textGeneration || newFailedApiCalls.imageTextToText;
237
-
238
  const cacheStatus = hasApiFailures ? CacheStatus.PARTIAL : CacheStatus.SUCCESS;
239
 
240
  cache.data = models;
@@ -243,34 +250,31 @@ export const GET: RequestHandler = async ({ fetch }) => {
243
  cache.failedTokenizers = newFailedTokenizers;
244
  cache.failedApiCalls = newFailedApiCalls;
245
 
246
- console.log(
247
  `Cache updated: ${models.length} models, status: ${cacheStatus}, ` +
248
  `failed tokenizers: ${newFailedTokenizers.length}, ` +
249
  `API failures: text=${newFailedApiCalls.textGeneration}, img=${newFailedApiCalls.imageTextToText}`,
250
  );
251
 
252
- return createResponse({ models });
253
  } catch (error) {
254
- console.error("Error fetching models:", error);
255
 
256
- // If we have cached data, use it as fallback
257
  if (cache.data?.length) {
258
  cache.status = CacheStatus.ERROR;
259
- // Mark all API calls as failed so we retry them next time
260
  cache.failedApiCalls = {
261
  textGeneration: true,
262
  imageTextToText: true,
263
  };
264
- return createResponse({ models: cache.data });
265
  }
266
 
267
- // No cache available, return empty array
268
  cache.status = CacheStatus.ERROR;
269
  cache.timestamp = timestamp;
270
  cache.failedApiCalls = {
271
  textGeneration: true,
272
  imageTextToText: true,
273
  };
274
- return createResponse({ models: [] });
275
  }
276
- };
 
1
+ import { query } from "$app/server";
2
+ import type { Provider, Model } from "$lib/types.js";
3
+ import { debugError, debugLog } from "$lib/utils/debug.js";
4
+
5
+ export type RouterData = {
6
+ object: string;
7
+ data: Datum[];
8
+ };
9
+
10
+ type Datum = {
11
+ id: string;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ object: any;
14
+ created: number;
15
+ owned_by: string;
16
+ providers: ProviderElement[];
17
+ };
18
+
19
+ type ProviderElement = {
20
+ provider: Provider;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ status: any;
23
+ context_length?: number;
24
+ pricing?: Pricing;
25
+ supports_tools?: boolean;
26
+ supports_structured_output?: boolean;
27
+ };
28
+
29
+ type Pricing = {
30
+ input: number;
31
+ output: number;
32
+ };
33
+
34
+ export const getRouterData = query(async (): Promise<RouterData> => {
35
+ const res = await fetch("https://router.huggingface.co/v1/models");
36
+ return res.json();
37
+ });
38
 
39
  enum CacheStatus {
40
  SUCCESS = "success",
 
46
  data: Model[] | undefined;
47
  timestamp: number;
48
  status: CacheStatus;
49
+ failedTokenizers: string[];
 
50
  failedApiCalls: {
51
  textGeneration: boolean;
52
  imageTextToText: boolean;
 
64
  },
65
  };
66
 
 
67
  const FULL_CACHE_REFRESH = 1000 * 60 * 60; // 1 hour
68
+ const PARTIAL_CACHE_REFRESH = 1000 * 60 * 15; // 15 minutes
69
 
70
  const headers: HeadersInit = {
71
  "Upgrade-Insecure-Requests": "1",
 
105
  function buildApiUrl(params: ApiQueryParams): string {
106
  const url = new URL(baseUrl);
107
 
 
108
  Object.entries(params).forEach(([key, value]) => {
109
  if (!Array.isArray(value) && value !== undefined) {
110
  url.searchParams.append(key, String(value));
111
  }
112
  });
113
 
 
114
  params.expand.forEach(item => {
115
  url.searchParams.append("expand[]", item);
116
  });
 
118
  return url.toString();
119
  }
120
 
121
+ async function fetchAllModelsWithPagination(pipeline_tag: "text-generation" | "image-text-to-text"): Promise<Model[]> {
 
 
 
122
  const allModels: Model[] = [];
123
  let skip = 0;
124
  const batchSize = 1000;
 
140
  const models: Model[] = await response.json();
141
 
142
  if (models.length === 0) {
143
+ break;
144
  }
145
 
146
  allModels.push(...models);
147
  skip += batchSize;
148
 
 
149
  await new Promise(resolve => setTimeout(resolve, 100));
150
  }
151
 
152
  return allModels;
153
  }
154
 
155
+ export const getModels = query(async (): Promise<Model[]> => {
 
 
 
 
 
 
 
 
156
  const timestamp = Date.now();
157
 
 
158
  const elapsed = timestamp - cache.timestamp;
159
  const cacheRefreshTime = cache.status === CacheStatus.SUCCESS ? FULL_CACHE_REFRESH : PARTIAL_CACHE_REFRESH;
160
 
 
161
  if (elapsed < cacheRefreshTime && cache.data?.length) {
162
+ debugLog(`Using ${cache.status} cache (${Math.floor(elapsed / 1000 / 60)} min old)`);
163
+ return cache.data;
164
  }
165
 
166
  try {
 
167
  const needTextGenFetch = elapsed >= FULL_CACHE_REFRESH || cache.failedApiCalls.textGeneration;
168
  const needImgTextFetch = elapsed >= FULL_CACHE_REFRESH || cache.failedApiCalls.imageTextToText;
169
 
 
170
  const existingModels = new Map<string, Model>();
171
  if (cache.data) {
172
  cache.data.forEach(model => {
 
174
  });
175
  }
176
 
 
177
  const newFailedTokenizers: string[] = [];
178
  const newFailedApiCalls = {
179
  textGeneration: false,
180
  imageTextToText: false,
181
  };
182
 
 
183
  let textGenModels: Model[] = [];
184
  let imgText2TextModels: Model[] = [];
185
 
 
186
  const apiPromises: Promise<void>[] = [];
187
  if (needTextGenFetch) {
188
  apiPromises.push(
189
+ fetchAllModelsWithPagination("text-generation")
190
  .then(models => {
191
  textGenModels = models;
192
  })
193
  .catch(error => {
194
+ debugError(`Error fetching text-generation models:`, error);
195
  newFailedApiCalls.textGeneration = true;
196
  }),
197
  );
 
199
 
200
  if (needImgTextFetch) {
201
  apiPromises.push(
202
+ fetchAllModelsWithPagination("image-text-to-text")
203
  .then(models => {
204
  imgText2TextModels = models;
205
  })
206
  .catch(error => {
207
+ debugError(`Error fetching image-text-to-text models:`, error);
208
  newFailedApiCalls.imageTextToText = true;
209
  }),
210
  );
 
212
 
213
  await Promise.all(apiPromises);
214
 
 
215
  if (
216
  needTextGenFetch &&
217
  newFailedApiCalls.textGeneration &&
 
219
  newFailedApiCalls.imageTextToText &&
220
  cache.data?.length
221
  ) {
222
+ debugLog("All API requests failed. Using existing cache as fallback.");
223
  cache.status = CacheStatus.ERROR;
224
+ cache.timestamp = timestamp;
225
  cache.failedApiCalls = newFailedApiCalls;
226
+ return cache.data;
227
  }
228
 
 
229
  if (!needTextGenFetch && cache.data) {
230
  textGenModels = cache.data.filter(model => model.pipeline_tag === "text-generation").map(model => model as Model);
231
  }
 
241
  );
242
  models.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
243
 
 
244
  const hasApiFailures = newFailedApiCalls.textGeneration || newFailedApiCalls.imageTextToText;
 
245
  const cacheStatus = hasApiFailures ? CacheStatus.PARTIAL : CacheStatus.SUCCESS;
246
 
247
  cache.data = models;
 
250
  cache.failedTokenizers = newFailedTokenizers;
251
  cache.failedApiCalls = newFailedApiCalls;
252
 
253
+ debugLog(
254
  `Cache updated: ${models.length} models, status: ${cacheStatus}, ` +
255
  `failed tokenizers: ${newFailedTokenizers.length}, ` +
256
  `API failures: text=${newFailedApiCalls.textGeneration}, img=${newFailedApiCalls.imageTextToText}`,
257
  );
258
 
259
+ return models;
260
  } catch (error) {
261
+ debugError("Error fetching models:", error);
262
 
 
263
  if (cache.data?.length) {
264
  cache.status = CacheStatus.ERROR;
 
265
  cache.failedApiCalls = {
266
  textGeneration: true,
267
  imageTextToText: true,
268
  };
269
+ return cache.data;
270
  }
271
 
 
272
  cache.status = CacheStatus.ERROR;
273
  cache.timestamp = timestamp;
274
  cache.failedApiCalls = {
275
  textGeneration: true,
276
  imageTextToText: true,
277
  };
278
+ return [];
279
  }
280
+ });
src/lib/state/models.svelte.ts CHANGED
@@ -1,22 +1,27 @@
1
- import { page } from "$app/state";
2
  import { type CustomModel, type Model } from "$lib/types.js";
3
  import { edit, randomPick } from "$lib/utils/array.js";
4
  import { safeParse } from "$lib/utils/json.js";
5
  import typia from "typia";
6
- import type { PageData } from "../../routes/$types.js";
7
  import { conversations } from "./conversations.svelte";
 
8
 
9
  const LOCAL_STORAGE_KEY = "hf_inference_playground_custom_models";
10
 
11
- const pageData = $derived(page.data as PageData);
12
-
13
  class Models {
14
- remote = $derived(pageData.models);
 
15
  trending = $derived(this.remote.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
16
  nonTrending = $derived(this.remote.filter(m => !this.trending.includes(m)));
17
  all = $derived([...this.remote, ...this.custom]);
18
 
19
  constructor() {
 
 
 
 
 
 
 
20
  const savedData = localStorage.getItem(LOCAL_STORAGE_KEY);
21
  if (!savedData) return;
22
 
@@ -69,8 +74,9 @@ class Models {
69
  }
70
 
71
  supportsStructuredOutput(model: Model | CustomModel, provider?: string) {
 
72
  if (typia.is<CustomModel>(model)) return true;
73
- const routerDataEntry = pageData.routerData.data.find(d => d.id === model.id);
74
  if (!routerDataEntry) return false;
75
  return routerDataEntry.providers.find(p => p.provider === provider)?.supports_structured_output ?? false;
76
  }
 
 
1
  import { type CustomModel, type Model } from "$lib/types.js";
2
  import { edit, randomPick } from "$lib/utils/array.js";
3
  import { safeParse } from "$lib/utils/json.js";
4
  import typia from "typia";
 
5
  import { conversations } from "./conversations.svelte";
6
+ import { getModels, getRouterData, type RouterData } from "$lib/remote/models.remote";
7
 
8
  const LOCAL_STORAGE_KEY = "hf_inference_playground_custom_models";
9
 
 
 
10
  class Models {
11
+ routerData = $state<RouterData>();
12
+ remote: Model[] = $state([]);
13
  trending = $derived(this.remote.toSorted((a, b) => b.trendingScore - a.trendingScore).slice(0, 5));
14
  nonTrending = $derived(this.remote.filter(m => !this.trending.includes(m)));
15
  all = $derived([...this.remote, ...this.custom]);
16
 
17
  constructor() {
18
+ getModels().then(models => {
19
+ this.remote = models;
20
+ });
21
+ getRouterData().then(data => {
22
+ this.routerData = data;
23
+ });
24
+
25
  const savedData = localStorage.getItem(LOCAL_STORAGE_KEY);
26
  if (!savedData) return;
27
 
 
74
  }
75
 
76
  supportsStructuredOutput(model: Model | CustomModel, provider?: string) {
77
+ if (!this.routerData) return false;
78
  if (typia.is<CustomModel>(model)) return true;
79
+ const routerDataEntry = this.routerData?.data.find(d => d.id === model.id);
80
  if (!routerDataEntry) return false;
81
  return routerDataEntry.providers.find(p => p.provider === provider)?.supports_structured_output ?? false;
82
  }
src/lib/utils/debug.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const DEBUG_LOG = true;
2
+
3
+ export const debugLog = (...args: unknown[]) => {
4
+ if (!DEBUG_LOG) return;
5
+ console.log("[LOG DEBUG]", ...args);
6
+ };
7
+
8
+ export const debugError = (...args: unknown[]) => {
9
+ if (!DEBUG_LOG) return;
10
+ console.error("[LOG DEBUG]", ...args);
11
+ };
src/routes/+page.ts DELETED
@@ -1,47 +0,0 @@
1
- import type { Provider } from "$lib/types.js";
2
- import type { PageLoad } from "./$types.js";
3
- import type { ApiModelsResponse } from "./api/models/+server.js";
4
-
5
- export type RouterData = {
6
- object: string;
7
- data: Datum[];
8
- };
9
-
10
- type Datum = {
11
- id: string;
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
- object: any;
14
- created: number;
15
- owned_by: string;
16
- providers: ProviderElement[];
17
- };
18
-
19
- type ProviderElement = {
20
- provider: Provider;
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- status: any;
23
- context_length?: number;
24
- pricing?: Pricing;
25
- supports_tools?: boolean;
26
- supports_structured_output?: boolean;
27
- };
28
-
29
- type Pricing = {
30
- input: number;
31
- output: number;
32
- };
33
-
34
- export const load: PageLoad = async ({ fetch }) => {
35
- const [modelsRes, routerRes] = await Promise.all([
36
- fetch("/api/models"),
37
- fetch("https://router.huggingface.co/v1/models"),
38
- ]);
39
-
40
- const models: ApiModelsResponse = await modelsRes.json();
41
- const routerData = (await routerRes.json()) as RouterData;
42
-
43
- return {
44
- ...models,
45
- routerData,
46
- };
47
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
svelte.config.js CHANGED
@@ -12,6 +12,9 @@ const config = {
12
  // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13
  // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14
  adapter: adapter(),
 
 
 
15
  },
16
 
17
  compilerOptions: {
 
12
  // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13
  // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14
  adapter: adapter(),
15
+ experimental: {
16
+ remoteFunctions: true,
17
+ },
18
  },
19
 
20
  compilerOptions: {