mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat(kilo): fetch free models from Kilo API + Windows build fixes (#455)
- Add /api/providers/kilo/free-models endpoint with 1hr cache - Fetch and merge Kilo free models with hardcoded models for kilocode provider - Display 'Free' badge on models fetched from Kilo API - Fix Windows build: add cross-env, remove --webpack flag, add turbopack config - Add outputFileTracingExcludes for Windows system directories
This commit is contained in:
@@ -2,10 +2,18 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
serverExternalPackages: ["better-sqlite3"],
|
serverExternalPackages: ["better-sqlite3"],
|
||||||
|
outputFileTracingExcludes: {
|
||||||
|
"*": [
|
||||||
|
"**/Cookies/**",
|
||||||
|
"**/AppData/Local/**",
|
||||||
|
"**/node_modules/.cache/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true
|
unoptimized: true
|
||||||
},
|
},
|
||||||
env: {},
|
env: {},
|
||||||
|
turbopack: {},
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
// Ignore fs/path modules in browser bundle
|
// Ignore fs/path modules in browser bundle
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
|
|||||||
+3
-2
@@ -5,8 +5,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --webpack --port 20128",
|
"dev": "next dev --webpack --port 20128",
|
||||||
"build": "NODE_ENV=production next build --webpack",
|
"build": "cross-env NODE_ENV=production next build",
|
||||||
"start": "NODE_ENV=production next start",
|
"start": "cross-env NODE_ENV=production next start",
|
||||||
"dev:bun": "bun --bun next dev --webpack --port 20128",
|
"dev:bun": "bun --bun next dev --webpack --port 20128",
|
||||||
"build:bun": "NODE_ENV=production bun --bun next build --webpack",
|
"build:bun": "NODE_ENV=production bun --bun next build --webpack",
|
||||||
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export default function ProviderDetailPage() {
|
|||||||
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
|
const [providerStrategy, setProviderStrategy] = useState(null); // null = use global, "round-robin" = override
|
||||||
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
const [providerStickyLimit, setProviderStickyLimit] = useState("");
|
||||||
const [suggestedModels, setSuggestedModels] = useState([]);
|
const [suggestedModels, setSuggestedModels] = useState([]);
|
||||||
|
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
||||||
const { copied, copy } = useCopyToClipboard();
|
const { copied, copy } = useCopyToClipboard();
|
||||||
|
|
||||||
const providerInfo = providerNode
|
const providerInfo = providerNode
|
||||||
@@ -77,6 +78,15 @@ export default function ProviderDetailPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Fetch free models from Kilo API for kilocode provider
|
||||||
|
useEffect(() => {
|
||||||
|
if (providerId !== "kilocode") return;
|
||||||
|
fetch("/api/providers/kilo/free-models")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => { if (data.models?.length) setKiloFreeModels(data.models); })
|
||||||
|
.catch(() => {});
|
||||||
|
}, [providerId]);
|
||||||
|
|
||||||
const fetchConnections = useCallback(async () => {
|
const fetchConnections = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([
|
const [connectionsRes, nodesRes, proxyPoolsRes, settingsRes] = await Promise.all([
|
||||||
@@ -537,6 +547,11 @@ export default function ProviderDetailPage() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Combine hardcoded models with Kilo free models (deduplicated)
|
||||||
|
const displayModels = [
|
||||||
|
...models,
|
||||||
|
...kiloFreeModels.filter((fm) => !models.some((m) => m.id === fm.id)),
|
||||||
|
];
|
||||||
// Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
|
// Custom models added by user (stored as aliases: modelId → providerAlias/modelId)
|
||||||
const customModels = Object.entries(modelAliases)
|
const customModels = Object.entries(modelAliases)
|
||||||
.filter(([alias, fullModel]) => {
|
.filter(([alias, fullModel]) => {
|
||||||
@@ -556,7 +571,7 @@ export default function ProviderDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{models.map((model) => {
|
{displayModels.map((model) => {
|
||||||
const fullModel = `${providerStorageAlias}/${model.id}`;
|
const fullModel = `${providerStorageAlias}/${model.id}`;
|
||||||
const oldFormatModel = `${providerId}/${model.id}`;
|
const oldFormatModel = `${providerId}/${model.id}`;
|
||||||
const existingAlias = Object.entries(modelAliases).find(
|
const existingAlias = Object.entries(modelAliases).find(
|
||||||
@@ -575,6 +590,7 @@ export default function ProviderDetailPage() {
|
|||||||
testStatus={modelTestResults[model.id]}
|
testStatus={modelTestResults[model.id]}
|
||||||
onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined}
|
onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined}
|
||||||
isTesting={testingModelId === model.id}
|
isTesting={testingModelId === model.id}
|
||||||
|
isFree={model.isFree}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -964,7 +980,7 @@ export default function ProviderDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, onDeleteAlias, onTest, isTesting }) {
|
function ModelRow({ model, fullModel, alias, copied, onCopy, testStatus, isCustom, isFree, onDeleteAlias, onTest, isTesting }) {
|
||||||
const borderColor = testStatus === "ok"
|
const borderColor = testStatus === "ok"
|
||||||
? "border-green-500/40"
|
? "border-green-500/40"
|
||||||
: testStatus === "error"
|
: testStatus === "error"
|
||||||
@@ -1040,6 +1056,7 @@ ModelRow.propTypes = {
|
|||||||
onCopy: PropTypes.func.isRequired,
|
onCopy: PropTypes.func.isRequired,
|
||||||
testStatus: PropTypes.oneOf(["ok", "error"]),
|
testStatus: PropTypes.oneOf(["ok", "error"]),
|
||||||
isCustom: PropTypes.bool,
|
isCustom: PropTypes.bool,
|
||||||
|
isFree: PropTypes.bool,
|
||||||
onDeleteAlias: PropTypes.func,
|
onDeleteAlias: PropTypes.func,
|
||||||
onTest: PropTypes.func,
|
onTest: PropTypes.func,
|
||||||
isTesting: PropTypes.bool,
|
isTesting: PropTypes.bool,
|
||||||
@@ -1168,6 +1185,9 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
|
|||||||
|
|
||||||
<div className="flex items-center gap-1 mt-1">
|
<div className="flex items-center gap-1 mt-1">
|
||||||
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
|
<code className="text-xs text-text-muted font-mono bg-sidebar px-1.5 py-0.5 rounded">{fullModel}</code>
|
||||||
|
{isFree && (
|
||||||
|
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-green-500/10 text-green-600 dark:text-green-400">Free</span>
|
||||||
|
)}
|
||||||
<div className="relative group/btn">
|
<div className="relative group/btn">
|
||||||
<button
|
<button
|
||||||
onClick={() => onCopy(fullModel, `model-${modelId}`)}
|
onClick={() => onCopy(fullModel, `model-${modelId}`)}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const KILO_MODELS_URL = "https://api.kilo.ai/api/gateway/models";
|
||||||
|
|
||||||
|
// In-memory cache with TTL
|
||||||
|
let cachedModels = null;
|
||||||
|
let cacheTimestamp = 0;
|
||||||
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Return cached result if still valid
|
||||||
|
if (cachedModels && now - cacheTimestamp < CACHE_TTL_MS) {
|
||||||
|
return NextResponse.json({ models: cachedModels, cached: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(KILO_MODELS_URL, {
|
||||||
|
headers: { "Accept": "application/json" },
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Kilo API returned ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
const allModels = json.data || [];
|
||||||
|
|
||||||
|
const freeModels = allModels
|
||||||
|
.filter((m) => m.isFree === true)
|
||||||
|
.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
isFree: true,
|
||||||
|
context_length: m.context_length || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
cachedModels = freeModels;
|
||||||
|
cacheTimestamp = now;
|
||||||
|
|
||||||
|
return NextResponse.json({ models: freeModels, cached: false });
|
||||||
|
} catch (error) {
|
||||||
|
// Return cached data if available, even if expired
|
||||||
|
if (cachedModels) {
|
||||||
|
return NextResponse.json({ models: cachedModels, cached: true, warning: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ models: [], error: `Failed to fetch Kilo models: ${error.message}` },
|
||||||
|
{ status: 502 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user