mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +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 = {
|
||||
output: "standalone",
|
||||
serverExternalPackages: ["better-sqlite3"],
|
||||
outputFileTracingExcludes: {
|
||||
"*": [
|
||||
"**/Cookies/**",
|
||||
"**/AppData/Local/**",
|
||||
"**/node_modules/.cache/**",
|
||||
],
|
||||
},
|
||||
images: {
|
||||
unoptimized: true
|
||||
},
|
||||
env: {},
|
||||
turbopack: {},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Ignore fs/path modules in browser bundle
|
||||
if (!isServer) {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --webpack --port 20128",
|
||||
"build": "NODE_ENV=production next build --webpack",
|
||||
"start": "NODE_ENV=production next start",
|
||||
"build": "cross-env NODE_ENV=production next build",
|
||||
"start": "cross-env NODE_ENV=production next start",
|
||||
"dev:bun": "bun --bun next dev --webpack --port 20128",
|
||||
"build:bun": "NODE_ENV=production bun --bun next build --webpack",
|
||||
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js"
|
||||
@@ -44,6 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.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 [providerStickyLimit, setProviderStickyLimit] = useState("");
|
||||
const [suggestedModels, setSuggestedModels] = useState([]);
|
||||
const [kiloFreeModels, setKiloFreeModels] = useState([]);
|
||||
const { copied, copy } = useCopyToClipboard();
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
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)
|
||||
const customModels = Object.entries(modelAliases)
|
||||
.filter(([alias, fullModel]) => {
|
||||
@@ -556,7 +571,7 @@ export default function ProviderDetailPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{models.map((model) => {
|
||||
{displayModels.map((model) => {
|
||||
const fullModel = `${providerStorageAlias}/${model.id}`;
|
||||
const oldFormatModel = `${providerId}/${model.id}`;
|
||||
const existingAlias = Object.entries(modelAliases).find(
|
||||
@@ -575,6 +590,7 @@ export default function ProviderDetailPage() {
|
||||
testStatus={modelTestResults[model.id]}
|
||||
onTest={connections.length > 0 ? () => handleTestModel(model.id) : undefined}
|
||||
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"
|
||||
? "border-green-500/40"
|
||||
: testStatus === "error"
|
||||
@@ -1040,6 +1056,7 @@ ModelRow.propTypes = {
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
testStatus: PropTypes.oneOf(["ok", "error"]),
|
||||
isCustom: PropTypes.bool,
|
||||
isFree: PropTypes.bool,
|
||||
onDeleteAlias: PropTypes.func,
|
||||
onTest: PropTypes.func,
|
||||
isTesting: PropTypes.bool,
|
||||
@@ -1167,7 +1184,10 @@ function PassthroughModelRow({ modelId, fullModel, copied, onCopy, onDeleteAlias
|
||||
<p className="text-sm font-medium truncate">{modelId}</p>
|
||||
|
||||
<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">
|
||||
<button
|
||||
onClick={() => onCopy(fullModel, `model-${modelId}`)}
|
||||
|
||||
55
src/app/api/providers/kilo/free-models/route.js
Normal file
55
src/app/api/providers/kilo/free-models/route.js
Normal file
@@ -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