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:
Vishal Raj V
2026-03-31 07:52:21 +05:30
committed by GitHub
parent ffa172c92d
commit 8640503b36
4 changed files with 89 additions and 5 deletions

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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}`)}

View 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 }
);
}
}