ruvnet/ruflo · error · Error
Routes config must be a flat array of routes
Error message
Routes config must be a flat array of routes
What it means
Thrown by loadPolicy in router/policy.ts when the JSON file at LLM_ROUTER_ROUTES_PATH parses successfully but the top-level value is not an Array. The router routes file must be a flat JSON array of Route objects (each with name, description, primary_model); an object, string, number, or null at the top level is rejected before any per-entry validation runs.
Source
Thrown at ruflo/src/ruvocal/src/lib/server/router/policy.ts:13
import { readFile } from "node:fs/promises";
import { config } from "$lib/server/config";
import type { Route } from "./types";
let ROUTES: Route[] = [];
let loaded = false;
export async function loadPolicy(): Promise<Route[]> {
const path = config.LLM_ROUTER_ROUTES_PATH;
const text = await readFile(path, "utf8");
const arr = JSON.parse(text) as Route[];
if (!Array.isArray(arr)) {
throw new Error("Routes config must be a flat array of routes");
}
const seen = new Set<string>();
for (const r of arr) {
if (!r?.name || !r?.description || !r?.primary_model) {
throw new Error(`Invalid route entry: ${JSON.stringify(r)}`);
}
if (seen.has(r.name)) {
throw new Error(`Duplicate route name: ${r.name}`);
}
seen.add(r.name);
}
ROUTES = arr;
loaded = true;
return ROUTES;
}
export async function getRoutes(): Promise<Route[]> {
if (!loaded) await loadPolicy();View on GitHub (pinned to 6b01dc5a68)
Solutions
- Rewrite the routes file as a flat JSON array of route objects: [{ "name": ..., "description": ..., "primary_model": ... }, ...].
- If your source of truth is keyed by name, flatten it at config-load time (Object.values(obj)) before writing the file.
- Validate the file shape in CI with a JSON schema or a quick node -e check that JSON.parse yields Array.isArray.
- Confirm LLM_ROUTER_ROUTES_PATH points at the file you actually edited.
Example fix
// before (routes.json)
{
"routes": [
{ "name": "code", "description": "coding", "primary_model": "Qwen2.5-Coder-32B" }
]
}
// after
[
{ "name": "code", "description": "coding", "primary_model": "Qwen2.5-Coder-32B" }
] Defensive patterns
Strategy: validation
Validate before calling
import { readFileSync } from "node:fs";
function routesFileIsValid(path: string): boolean {
try {
const arr = JSON.parse(readFileSync(path, "utf8"));
if (!Array.isArray(arr)) return false;
return arr.every((r) => r?.name && r?.description && r?.primary_model);
} catch {
return false;
}
} Type guard
function isRouteArray(value: unknown): value is { name: string; description: string; primary_model: string }[] {
return Array.isArray(value) && value.every((r) => r && typeof r === "object"
&& typeof r.name === "string" && typeof r.description === "string" && typeof r.primary_model === "string");
} Try / catch
try { await loadPolicy(); }
catch (e) {
if (e instanceof Error && /Routes config must be a flat array/.test(e.message)) {
// stop the router from loading; fall back to no routes or default route
loaded = true; // prevent reload loop
ROUTES = [];
} else throw e;
} Prevention
- Author the routes file as a bare JSON array of route objects, no envelope key.
- Add a CI lint that JSON.parses the file and asserts Array.isArray.
- If templating from a keyed source, flatten with Object.values before writing the file.
- Confirm LLM_ROUTER_ROUTES_PATH points at the file you actually edited.
When it happens
Trigger: LLM_ROUTER_ROUTES_PATH points at a JSON file whose contents are an object (e.g. {"routes": [...]} or {"default": {...}}) instead of a bare [...] array; or the file contains a single route object {"name": ...} rather than a one-element array; or the file was overwritten with non-JSON-yaml/map structure.
Common situations: Hand-editing the routes file and wrapping it in an envelope key; converting from YAML/TOML and keeping the top-level mapping; a deployment tool templating the file with an object schema; copy-pasting a single route object instead of an array; merging config from a CMS that returns objects keyed by route name.
Related errors
- Invalid route entry: ${JSON.stringify(r)}
- Failed to load any models from upstream
- OPENAI_BASE_URL not set
- Failed to fetch ${baseURL}/models: ${response.status} ${resp
- Router multimodal is enabled but LLM_ROUTER_MULTIMODAL_MODEL
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/b647b3e2c2241a1f.
Report an issue: GitHub.