Skip to content

Commit befc707

Browse files
authored
Slim the models list payload and harden per-model endpoints (#2410)
Slim models list payload, serve providers/parameters from per-model endpoints
1 parent 9a83f4f commit befc707

9 files changed

Lines changed: 109 additions & 41 deletions

File tree

src/lib/APIClient.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ export function useAPIClient({
117117
reports: endpoint(fetcher, `${baseUrl}/user/reports`),
118118
"billing-orgs": endpoint(fetcher, `${baseUrl}/user/billing-orgs`),
119119
},
120-
models: {
121-
...endpoint(fetcher, `${baseUrl}/models`),
122-
old: endpoint(fetcher, `${baseUrl}/models/old`),
123-
},
120+
models: Object.assign(
121+
// client.models({ id: "org/name" }) — per-model detail (providers, parameters).
122+
// Model ids may contain a slash; the path resolves to /models/[namespace]([/model]).
123+
(params: { id: string }) => endpoint(fetcher, `${baseUrl}/models/${params.id}`),
124+
{
125+
...endpoint(fetcher, `${baseUrl}/models`),
126+
old: endpoint(fetcher, `${baseUrl}/models/old`),
127+
}
128+
),
124129
spaces: {
125130
deploy: endpoint(fetcher, `${baseUrl}/spaces/deploy`),
126131
},

src/lib/server/api/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { BackendModel } from "$lib/server/models";
22

3+
// List shape: intentionally excludes `providers` (~60KB across all models) and
4+
// `parameters` — no list consumer reads them, and the whole list is SSR-inlined
5+
// into every page. They are served by the per-model detail endpoint instead.
36
export type GETModelsResponse = Array<{
47
id: string;
58
name: string;
@@ -10,9 +13,7 @@ export type GETModelsResponse = Array<{
1013
displayName: string;
1114
description?: string;
1215
logoUrl?: string;
13-
providers?: Array<{ provider: string } & Record<string, unknown>>;
1416
promptExamples?: { title: string; prompt: string }[];
15-
parameters: BackendModel["parameters"];
1617
preprompt?: string;
1718
multimodal: boolean;
1819
multimodalAcceptedMimetypes?: string[];
@@ -24,6 +25,11 @@ export type GETModelsResponse = Array<{
2425
isRouter: boolean;
2526
}>;
2627

28+
export type GETModelResponse = GETModelsResponse[number] & {
29+
providers?: Array<{ provider: string } & Record<string, unknown>>;
30+
parameters: BackendModel["parameters"];
31+
};
32+
2733
export type GETOldModelsResponse = Array<{
2834
id: string;
2935
name: string;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { ProcessedModel } from "$lib/server/models";
2+
import type { GETModelResponse, GETModelsResponse } from "$lib/server/api/types";
3+
4+
// API responses must never serialize a model object directly: the raw
5+
// ProcessedModel carries server-only configuration (an `endpoints` array that
6+
// can hold per-model API keys for self-hosted deployments, prompt render
7+
// functions, etc.). Every field that leaves the server is listed explicitly.
8+
9+
export function serializeModelSummary(model: ProcessedModel): GETModelsResponse[number] {
10+
return {
11+
id: model.id,
12+
name: model.name,
13+
websiteUrl: model.websiteUrl,
14+
modelUrl: model.modelUrl,
15+
datasetName: model.datasetName,
16+
datasetUrl: model.datasetUrl,
17+
displayName: model.displayName,
18+
description: model.description,
19+
logoUrl: model.logoUrl,
20+
promptExamples: model.promptExamples,
21+
preprompt: model.preprompt,
22+
multimodal: model.multimodal,
23+
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
24+
supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
25+
supportsReasoning:
26+
(model as unknown as { supportsReasoning?: boolean }).supportsReasoning ?? false,
27+
supportsArtifacts:
28+
(model as unknown as { supportsArtifacts?: boolean }).supportsArtifacts ?? false,
29+
unlisted: model.unlisted,
30+
hasInferenceAPI: model.hasInferenceAPI,
31+
isRouter: model.isRouter,
32+
};
33+
}
34+
35+
// Detail shape for GET /models/[namespace]([/model]): the summary plus the
36+
// heavyweight fields (providers is ~60KB across all models) that only the
37+
// per-model settings page needs, fetched on demand.
38+
export function serializeModelDetail(model: ProcessedModel): GETModelResponse {
39+
return {
40+
...serializeModelSummary(model),
41+
providers: model.providers as unknown as Array<{ provider: string } & Record<string, unknown>>,
42+
parameters: model.parameters,
43+
};
44+
}

src/lib/server/endpoints/endpoints.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from "@huggingface/inference";
88
import { z } from "zod";
99
import { endpointOAIParametersSchema, endpointOai } from "./openai/endpointOai";
10-
import type { Model } from "$lib/types/Model";
10+
import type { BackendModel } from "$lib/server/models";
1111
import type { ObjectId } from "mongodb";
1212

1313
export type EndpointMessage = Omit<Message, "id">;
@@ -16,7 +16,7 @@ export type EndpointMessage = Omit<Message, "id">;
1616
export interface EndpointParameters {
1717
messages: EndpointMessage[];
1818
preprompt?: Conversation["preprompt"];
19-
generateSettings?: Partial<Model["parameters"]>;
19+
generateSettings?: Partial<BackendModel["parameters"]>;
2020
isMultimodal?: boolean;
2121
conversationId?: ObjectId;
2222
locals: App.Locals | undefined;

src/lib/types/Model.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { BackendModel } from "$lib/server/models";
22

3+
// Client-side model shape, mirroring the models LIST payload
4+
// (GETModelsResponse): `providers` and `parameters` are intentionally absent —
5+
// they are heavyweight and only served by the per-model detail endpoint.
36
export type Model = Pick<
47
BackendModel,
58
| "id"
@@ -9,7 +12,6 @@ export type Model = Pick<
912
| "websiteUrl"
1013
| "datasetName"
1114
| "promptExamples"
12-
| "parameters"
1315
| "description"
1416
| "logoUrl"
1517
| "modelUrl"
@@ -19,7 +21,6 @@ export type Model = Pick<
1921
| "multimodalAcceptedMimetypes"
2022
| "unlisted"
2123
| "hasInferenceAPI"
22-
| "providers"
2324
| "supportsTools"
2425
| "supportsReasoning"
2526
| "supportsArtifacts"

src/routes/api/v2/models/+server.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { RequestHandler } from "@sveltejs/kit";
22
import { superjsonResponse } from "$lib/server/api/utils/superjsonResponse";
3+
import { serializeModelSummary } from "$lib/server/api/utils/serializeModel";
34
import type { GETModelsResponse } from "$lib/server/api/types";
45

56
// Models are loaded once at server startup; new models appear on redeploy.
@@ -13,33 +14,7 @@ export const GET: RequestHandler = async () => {
1314
return superjsonResponse(
1415
models
1516
.filter((m) => m.unlisted == false)
16-
.map((model) => ({
17-
id: model.id,
18-
name: model.name,
19-
websiteUrl: model.websiteUrl,
20-
modelUrl: model.modelUrl,
21-
datasetName: model.datasetName,
22-
datasetUrl: model.datasetUrl,
23-
displayName: model.displayName,
24-
description: model.description,
25-
logoUrl: model.logoUrl,
26-
providers: model.providers as unknown as Array<
27-
{ provider: string } & Record<string, unknown>
28-
>,
29-
promptExamples: model.promptExamples,
30-
parameters: model.parameters,
31-
preprompt: model.preprompt,
32-
multimodal: model.multimodal,
33-
multimodalAcceptedMimetypes: model.multimodalAcceptedMimetypes,
34-
supportsTools: (model as unknown as { supportsTools?: boolean }).supportsTools ?? false,
35-
supportsReasoning:
36-
(model as unknown as { supportsReasoning?: boolean }).supportsReasoning ?? false,
37-
supportsArtifacts:
38-
(model as unknown as { supportsArtifacts?: boolean }).supportsArtifacts ?? false,
39-
unlisted: model.unlisted,
40-
hasInferenceAPI: model.hasInferenceAPI,
41-
isRouter: model.isRouter,
42-
})) satisfies GETModelsResponse,
17+
.map(serializeModelSummary) satisfies GETModelsResponse,
4318
{ headers: MODELS_CACHE_HEADERS }
4419
);
4520
} catch {
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { RequestHandler } from "@sveltejs/kit";
22
import { superjsonResponse } from "$lib/server/api/utils/superjsonResponse";
3+
import { serializeModelDetail } from "$lib/server/api/utils/serializeModel";
34
import { resolveModel } from "$lib/server/api/utils/resolveModel";
45

56
export const GET: RequestHandler = async ({ params }) => {
67
const model = await resolveModel(params.namespace ?? "");
7-
return superjsonResponse(model);
8+
// Serialize an explicit field list — the raw model object carries
9+
// server-only endpoint configuration that must never reach the client.
10+
return superjsonResponse(serializeModelDetail(model));
811
};
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type { RequestHandler } from "@sveltejs/kit";
22
import { superjsonResponse } from "$lib/server/api/utils/superjsonResponse";
3+
import { serializeModelDetail } from "$lib/server/api/utils/serializeModel";
34
import { resolveModel } from "$lib/server/api/utils/resolveModel";
45

56
export const GET: RequestHandler = async ({ params }) => {
67
const model = await resolveModel(params.namespace ?? "", params.model ?? "");
7-
return superjsonResponse(model);
8+
// Serialize an explicit field list — the raw model object carries
9+
// server-only endpoint configuration that must never reach the client.
10+
return superjsonResponse(serializeModelDetail(model));
811
};

src/routes/settings/(nav)/[...model]/+page.svelte

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
2222
import { goto } from "$app/navigation";
2323
import { usePublicConfig } from "$lib/utils/PublicConfig.svelte";
24+
import { useAPIClient, handleResponse } from "$lib/APIClient";
2425
import Switch from "$lib/components/Switch.svelte";
2526
2627
const publicConfig = usePublicConfig();
2728
const settings = useSettingsStore();
29+
const client = useAPIClient();
2830
const modelId = $derived(page.params.model ?? "");
2931
3032
// Functional bindings for nested settings (Svelte 5):
@@ -123,7 +125,36 @@
123125
);
124126
125127
let model = $derived(page.data.models.find((el: BackendModel) => el.id === modelId));
126-
let providerList: RouterProvider[] = $derived((model?.providers ?? []) as RouterProvider[]);
128+
129+
// Providers are excluded from the models list payload (they are ~60KB
130+
// across all models and this page is their only reader); fetch them for
131+
// the current model on demand from the per-model detail endpoint.
132+
let providerList: RouterProvider[] = $state([]);
133+
let providersLoading = $state(false);
134+
$effect(() => {
135+
providerList = [];
136+
if (!publicConfig.isHuggingChat || !model || model.isRouter) {
137+
providersLoading = false;
138+
return;
139+
}
140+
const requestedId = modelId;
141+
providersLoading = true;
142+
client
143+
.models({ id: requestedId })
144+
.get()
145+
.then(handleResponse)
146+
.then((detail: { providers?: RouterProvider[] } | null) => {
147+
if (requestedId !== modelId) return; // stale response for a previous model
148+
providerList = detail?.providers ?? [];
149+
})
150+
.catch(() => {
151+
// No providers section is rendered on failure; the selection-mode
152+
// options (auto/fastest/cheapest) still work via settings.
153+
})
154+
.finally(() => {
155+
if (requestedId === modelId) providersLoading = false;
156+
});
157+
});
127158
128159
// multimodalOverrides/toolsOverrides intentionally have no initValue: getters fall back
129160
// to the model's advertised capability, so upstream capability changes flow through.
@@ -407,7 +438,7 @@
407438
</div>
408439
</div>
409440

410-
{#if publicConfig.isHuggingChat && model.providers?.length && !model?.isRouter}
441+
{#if publicConfig.isHuggingChat && !model?.isRouter && (providersLoading || providerList.length > 0)}
411442
<div
412443
class="mt-3 flex flex-col items-start gap-2.5 rounded-xl border border-gray-200 bg-white px-3 py-3 shadow-xs dark:border-gray-700 dark:bg-gray-800"
413444
>

0 commit comments

Comments
 (0)