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

  1. Rewrite the routes file as a flat JSON array of route objects: [{ "name": ..., "description": ..., "primary_model": ... }, ...].
  2. If your source of truth is keyed by name, flatten it at config-load time (Object.values(obj)) before writing the file.
  3. Validate the file shape in CI with a JSON schema or a quick node -e check that JSON.parse yields Array.isArray.
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/b647b3e2c2241a1f. Report an issue: GitHub.