Reimplement Openrouter filter
Browse files- src/app/api/openrouter-models/route.ts +43 -4
- src/app/page.tsx +69 -23
src/app/api/openrouter-models/route.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { NextResponse } from "next/server";
|
|
| 3 |
interface OpenRouterModel {
|
| 4 |
id: string;
|
| 5 |
name: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
pricing: {
|
| 7 |
prompt: string;
|
| 8 |
completion: string;
|
|
@@ -10,6 +16,29 @@ interface OpenRouterModel {
|
|
| 10 |
context_length: number;
|
| 11 |
}
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
let cachedModels: { id: string; name: string }[] | null = null;
|
| 14 |
let cacheTimestamp: number = 0;
|
| 15 |
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour
|
|
@@ -34,10 +63,20 @@ export async function GET() {
|
|
| 34 |
|
| 35 |
const data = await response.json();
|
| 36 |
|
| 37 |
-
cachedModels = data.data
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
cacheTimestamp = now;
|
| 43 |
|
|
|
|
| 3 |
interface OpenRouterModel {
|
| 4 |
id: string;
|
| 5 |
name: string;
|
| 6 |
+
created_at?: string | number;
|
| 7 |
+
createdAt?: string | number;
|
| 8 |
+
created?: string | number;
|
| 9 |
+
released_at?: string | number;
|
| 10 |
+
releasedAt?: string | number;
|
| 11 |
+
release_date?: string | number;
|
| 12 |
pricing: {
|
| 13 |
prompt: string;
|
| 14 |
completion: string;
|
|
|
|
| 16 |
context_length: number;
|
| 17 |
}
|
| 18 |
|
| 19 |
+
function toTimestampMs(value: unknown): number {
|
| 20 |
+
if (typeof value === "number") {
|
| 21 |
+
// Heuristic: treat small values as seconds
|
| 22 |
+
return value > 1_000_000_000_000 ? value : value * 1000;
|
| 23 |
+
}
|
| 24 |
+
if (typeof value === "string") {
|
| 25 |
+
const parsed = Date.parse(value);
|
| 26 |
+
return Number.isFinite(parsed) ? parsed : 0;
|
| 27 |
+
}
|
| 28 |
+
return 0;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function getModelTimestampMs(model: OpenRouterModel): number {
|
| 32 |
+
return Math.max(
|
| 33 |
+
toTimestampMs(model.released_at),
|
| 34 |
+
toTimestampMs(model.releasedAt),
|
| 35 |
+
toTimestampMs(model.release_date),
|
| 36 |
+
toTimestampMs(model.created_at),
|
| 37 |
+
toTimestampMs(model.createdAt),
|
| 38 |
+
toTimestampMs(model.created)
|
| 39 |
+
);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
let cachedModels: { id: string; name: string }[] | null = null;
|
| 43 |
let cacheTimestamp: number = 0;
|
| 44 |
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour
|
|
|
|
| 63 |
|
| 64 |
const data = await response.json();
|
| 65 |
|
| 66 |
+
cachedModels = data.data
|
| 67 |
+
.slice()
|
| 68 |
+
.sort((a: OpenRouterModel, b: OpenRouterModel) => {
|
| 69 |
+
const bt = getModelTimestampMs(b);
|
| 70 |
+
const at = getModelTimestampMs(a);
|
| 71 |
+
if (bt !== at) return bt - at;
|
| 72 |
+
const an = (a.name || a.id).toLowerCase();
|
| 73 |
+
const bn = (b.name || b.id).toLowerCase();
|
| 74 |
+
return an.localeCompare(bn);
|
| 75 |
+
})
|
| 76 |
+
.map((model: OpenRouterModel) => ({
|
| 77 |
+
id: model.id,
|
| 78 |
+
name: model.name || model.id,
|
| 79 |
+
}));
|
| 80 |
|
| 81 |
cacheTimestamp = now;
|
| 82 |
|
src/app/page.tsx
CHANGED
|
@@ -69,16 +69,7 @@ const STUDENT_MODELS = [
|
|
| 69 |
"Other",
|
| 70 |
];
|
| 71 |
|
| 72 |
-
const
|
| 73 |
-
"google/gemini-3-flash-preview",
|
| 74 |
-
"openai/gpt-4o-mini",
|
| 75 |
-
"openai/gpt-4o",
|
| 76 |
-
"anthropic/claude-3-5-sonnet",
|
| 77 |
-
"deepseek/deepseek-r1",
|
| 78 |
-
"Qwen/Qwen2.5-72B-Instruct",
|
| 79 |
-
"meta-llama/Llama-3.1-8B-Instruct",
|
| 80 |
-
"Other (Huggingface Models)",
|
| 81 |
-
];
|
| 82 |
|
| 83 |
const REASONING_DEPTHS = ["low", "medium", "high"];
|
| 84 |
const DATASET_SIZES = ["100x", "250x", "500x", "1000x", "3000x", "11000x"];
|
|
@@ -114,6 +105,10 @@ export default function Home() {
|
|
| 114 |
const [hfModelQuery, setHfModelQuery] = useState("");
|
| 115 |
const [loadingHfModels, setLoadingHfModels] = useState(false);
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
// Distillation form state
|
| 118 |
const [sourceDataset, setSourceDataset] = useState("");
|
| 119 |
const [sourceDatasetOther, setSourceDatasetOther] = useState("");
|
|
@@ -165,6 +160,36 @@ export default function Home() {
|
|
| 165 |
};
|
| 166 |
}, [hfModelQuery]);
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
async function fetchRequests() {
|
| 169 |
try {
|
| 170 |
const [distillRes, datasetRes] = await Promise.all([
|
|
@@ -233,6 +258,8 @@ export default function Home() {
|
|
| 233 |
setSourceDatasetOther("");
|
| 234 |
setStudentModel("");
|
| 235 |
setStudentModelOther("");
|
|
|
|
|
|
|
| 236 |
setDistillNotes("");
|
| 237 |
setShowDistillForm(false);
|
| 238 |
fetchRequests();
|
|
@@ -249,7 +276,7 @@ export default function Home() {
|
|
| 249 |
async function handleDatasetSubmit(e: React.FormEvent) {
|
| 250 |
e.preventDefault();
|
| 251 |
const resolvedSourceModel =
|
| 252 |
-
sourceModel ===
|
| 253 |
if (!resolvedSourceModel) {
|
| 254 |
toast({ title: "Error", description: "Please select a source model", variant: "destructive" });
|
| 255 |
return;
|
|
@@ -269,8 +296,10 @@ export default function Home() {
|
|
| 269 |
});
|
| 270 |
if (res.ok) {
|
| 271 |
toast({ title: "Success", description: "Dataset request submitted!" });
|
| 272 |
-
setSourceModel("
|
| 273 |
setSourceModelOther("");
|
|
|
|
|
|
|
| 274 |
setDatasetSize("250x");
|
| 275 |
setReasoningDepth("high");
|
| 276 |
setSelectedTopics([]);
|
|
@@ -433,7 +462,15 @@ export default function Home() {
|
|
| 433 |
value={studentModel}
|
| 434 |
onValueChange={(v) => {
|
| 435 |
setStudentModel(v);
|
| 436 |
-
if (v !== "Other")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
}}
|
| 438 |
>
|
| 439 |
<SelectTrigger>
|
|
@@ -463,13 +500,18 @@ export default function Home() {
|
|
| 463 |
)}
|
| 464 |
{studentModel === "Other" && (
|
| 465 |
<div className="space-y-2">
|
| 466 |
-
<Label htmlFor="studentOther">Student Model
|
| 467 |
-
<
|
| 468 |
-
|
| 469 |
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
| 470 |
value={studentModelOther}
|
| 471 |
-
|
| 472 |
-
placeholder="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
/>
|
| 474 |
</div>
|
| 475 |
)}
|
|
@@ -581,19 +623,23 @@ export default function Home() {
|
|
| 581 |
<div className="space-y-2">
|
| 582 |
<Label htmlFor="sourceModel">Source Model *</Label>
|
| 583 |
<Combobox
|
| 584 |
-
options={
|
|
|
|
|
|
|
|
|
|
| 585 |
value={sourceModel}
|
| 586 |
onValueChange={(v) => {
|
| 587 |
setSourceModel(v);
|
| 588 |
-
if (v !==
|
| 589 |
setSourceModelOther("");
|
| 590 |
setHfModelQuery("");
|
| 591 |
setHuggingfaceModels([]);
|
| 592 |
}
|
| 593 |
}}
|
| 594 |
placeholder="Select a model"
|
| 595 |
-
searchPlaceholder="Search
|
| 596 |
emptyMessage="No models found"
|
|
|
|
| 597 |
/>
|
| 598 |
</div>
|
| 599 |
<div className="space-y-2">
|
|
@@ -612,7 +658,7 @@ export default function Home() {
|
|
| 612 |
</Select>
|
| 613 |
</div>
|
| 614 |
</div>
|
| 615 |
-
{sourceModel ===
|
| 616 |
<div className="space-y-2">
|
| 617 |
<Label htmlFor="sourceModelOther">Hugging Face Model *</Label>
|
| 618 |
<Combobox
|
|
|
|
| 69 |
"Other",
|
| 70 |
];
|
| 71 |
|
| 72 |
+
const SOURCE_MODEL_OTHER_HF = "Other (HuggingFace)";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
const REASONING_DEPTHS = ["low", "medium", "high"];
|
| 75 |
const DATASET_SIZES = ["100x", "250x", "500x", "1000x", "3000x", "11000x"];
|
|
|
|
| 105 |
const [hfModelQuery, setHfModelQuery] = useState("");
|
| 106 |
const [loadingHfModels, setLoadingHfModels] = useState(false);
|
| 107 |
|
| 108 |
+
const [distillHuggingfaceModels, setDistillHuggingfaceModels] = useState<HuggingFaceModel[]>([]);
|
| 109 |
+
const [distillHfModelQuery, setDistillHfModelQuery] = useState("");
|
| 110 |
+
const [loadingDistillHfModels, setLoadingDistillHfModels] = useState(false);
|
| 111 |
+
|
| 112 |
// Distillation form state
|
| 113 |
const [sourceDataset, setSourceDataset] = useState("");
|
| 114 |
const [sourceDatasetOther, setSourceDatasetOther] = useState("");
|
|
|
|
| 160 |
};
|
| 161 |
}, [hfModelQuery]);
|
| 162 |
|
| 163 |
+
useEffect(() => {
|
| 164 |
+
let cancelled = false;
|
| 165 |
+
const q = distillHfModelQuery.trim();
|
| 166 |
+
|
| 167 |
+
const timer = setTimeout(async () => {
|
| 168 |
+
setLoadingDistillHfModels(true);
|
| 169 |
+
try {
|
| 170 |
+
const res = await fetch(`/api/huggingface-models?q=${encodeURIComponent(q)}&limit=20`);
|
| 171 |
+
const data = await res.json();
|
| 172 |
+
if (!cancelled) {
|
| 173 |
+
setDistillHuggingfaceModels(Array.isArray(data) ? data : []);
|
| 174 |
+
}
|
| 175 |
+
} catch (error) {
|
| 176 |
+
console.error("Error fetching Hugging Face models:", error);
|
| 177 |
+
if (!cancelled) {
|
| 178 |
+
setDistillHuggingfaceModels([]);
|
| 179 |
+
}
|
| 180 |
+
} finally {
|
| 181 |
+
if (!cancelled) {
|
| 182 |
+
setLoadingDistillHfModels(false);
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
}, 250);
|
| 186 |
+
|
| 187 |
+
return () => {
|
| 188 |
+
cancelled = true;
|
| 189 |
+
clearTimeout(timer);
|
| 190 |
+
};
|
| 191 |
+
}, [distillHfModelQuery]);
|
| 192 |
+
|
| 193 |
async function fetchRequests() {
|
| 194 |
try {
|
| 195 |
const [distillRes, datasetRes] = await Promise.all([
|
|
|
|
| 258 |
setSourceDatasetOther("");
|
| 259 |
setStudentModel("");
|
| 260 |
setStudentModelOther("");
|
| 261 |
+
setDistillHfModelQuery("");
|
| 262 |
+
setDistillHuggingfaceModels([]);
|
| 263 |
setDistillNotes("");
|
| 264 |
setShowDistillForm(false);
|
| 265 |
fetchRequests();
|
|
|
|
| 276 |
async function handleDatasetSubmit(e: React.FormEvent) {
|
| 277 |
e.preventDefault();
|
| 278 |
const resolvedSourceModel =
|
| 279 |
+
sourceModel === SOURCE_MODEL_OTHER_HF ? sourceModelOther.trim() : sourceModel;
|
| 280 |
if (!resolvedSourceModel) {
|
| 281 |
toast({ title: "Error", description: "Please select a source model", variant: "destructive" });
|
| 282 |
return;
|
|
|
|
| 296 |
});
|
| 297 |
if (res.ok) {
|
| 298 |
toast({ title: "Success", description: "Dataset request submitted!" });
|
| 299 |
+
setSourceModel(openrouterModels[0]?.id || "");
|
| 300 |
setSourceModelOther("");
|
| 301 |
+
setHfModelQuery("");
|
| 302 |
+
setHuggingfaceModels([]);
|
| 303 |
setDatasetSize("250x");
|
| 304 |
setReasoningDepth("high");
|
| 305 |
setSelectedTopics([]);
|
|
|
|
| 462 |
value={studentModel}
|
| 463 |
onValueChange={(v) => {
|
| 464 |
setStudentModel(v);
|
| 465 |
+
if (v !== "Other") {
|
| 466 |
+
setStudentModelOther("");
|
| 467 |
+
setDistillHfModelQuery("");
|
| 468 |
+
setDistillHuggingfaceModels([]);
|
| 469 |
+
} else {
|
| 470 |
+
setStudentModelOther("");
|
| 471 |
+
setDistillHfModelQuery("");
|
| 472 |
+
setDistillHuggingfaceModels([]);
|
| 473 |
+
}
|
| 474 |
}}
|
| 475 |
>
|
| 476 |
<SelectTrigger>
|
|
|
|
| 500 |
)}
|
| 501 |
{studentModel === "Other" && (
|
| 502 |
<div className="space-y-2">
|
| 503 |
+
<Label htmlFor="studentOther">Hugging Face Student Model *</Label>
|
| 504 |
+
<Combobox
|
| 505 |
+
options={distillHuggingfaceModels}
|
|
|
|
| 506 |
value={studentModelOther}
|
| 507 |
+
onValueChange={setStudentModelOther}
|
| 508 |
+
placeholder="Search safetensors models"
|
| 509 |
+
searchPlaceholder="Type to search models..."
|
| 510 |
+
emptyMessage={distillHfModelQuery.trim() ? "No models found" : "Start typing to search"}
|
| 511 |
+
loading={loadingDistillHfModels}
|
| 512 |
+
searchValue={distillHfModelQuery}
|
| 513 |
+
onSearchValueChange={setDistillHfModelQuery}
|
| 514 |
+
disableLocalFilter
|
| 515 |
/>
|
| 516 |
</div>
|
| 517 |
)}
|
|
|
|
| 623 |
<div className="space-y-2">
|
| 624 |
<Label htmlFor="sourceModel">Source Model *</Label>
|
| 625 |
<Combobox
|
| 626 |
+
options={[
|
| 627 |
+
...openrouterModels,
|
| 628 |
+
{ id: SOURCE_MODEL_OTHER_HF, name: SOURCE_MODEL_OTHER_HF },
|
| 629 |
+
]}
|
| 630 |
value={sourceModel}
|
| 631 |
onValueChange={(v) => {
|
| 632 |
setSourceModel(v);
|
| 633 |
+
if (v !== SOURCE_MODEL_OTHER_HF) {
|
| 634 |
setSourceModelOther("");
|
| 635 |
setHfModelQuery("");
|
| 636 |
setHuggingfaceModels([]);
|
| 637 |
}
|
| 638 |
}}
|
| 639 |
placeholder="Select a model"
|
| 640 |
+
searchPlaceholder="Search OpenRouter models..."
|
| 641 |
emptyMessage="No models found"
|
| 642 |
+
loading={loadingModels}
|
| 643 |
/>
|
| 644 |
</div>
|
| 645 |
<div className="space-y-2">
|
|
|
|
| 658 |
</Select>
|
| 659 |
</div>
|
| 660 |
</div>
|
| 661 |
+
{sourceModel === SOURCE_MODEL_OTHER_HF && (
|
| 662 |
<div className="space-y-2">
|
| 663 |
<Label htmlFor="sourceModelOther">Hugging Face Model *</Label>
|
| 664 |
<Combobox
|