Short version: Kimi K3 just topped the Frontend Code Arena — the first open-weight model to win a frontier coding board on human preference. But it's 2.8 trillion parameters (a rack to self-host) and priced like a flagship (~$3/$15 per million, and it always reasons). So don't switch your agent to it. Send it only your UI calls and keep everything else where it is. Here's the whole router.

The router is a lookup, not a migration#

The insight that makes this cheap: your app already knows when it's asking for UI. The "generate component" button, the "redesign this section" endpoint — those call sites are unambiguously front-end. Tag them, and route on the tag.

import OpenAI from "openai";

// One OpenAI-compatible client, pointed at OpenRouter.
const client = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});

const FRONTEND_MODEL = "moonshotai/kimi-k3";            // NOT "moonshot/..." — that 404s
const DEFAULT_MODEL  = process.env.DEFAULT_MODEL ?? "anthropic/claude-sonnet-5";

type Task = "frontend" | "general";

const modelFor = (task: Task) =>
  task === "frontend" ? FRONTEND_MODEL : DEFAULT_MODEL;

That's the entire routing decision. Everything else is calling it well.

export async function generate(task: Task, prompt: string, system: string) {
  const res = await client.chat.completions.create({
    model: modelFor(task),
    max_tokens: 4096,                       // guardrail 1: cap the run
    // guardrail 2: don't pay for max reasoning on a styled component
    // (OpenRouter unified param; Moonshot-native uses `reasoning_effort`)
    reasoning: task === "frontend" ? { effort: "low" } : undefined,
    messages: [
      { role: "system", content: system }, // guardrail 3: keep this STABLE → cacheable
      { role: "user", content: prompt },
    ],
  });
  return res.choices[0].message.content;
}

Now a UI request routes to K3, and a "summarize this ticket" request doesn't. One map, done. This is the same shape as a general cost-aware model router — you're just routing on task class instead of on cost tier.

Tag deterministically; classify only as an escape hatch#

Deterministic tagging is free, instant, and never wrong, so use it wherever you own the call site. You only need inference when you're routing free-form text you don't control — an open chat box where the user might ask for anything. Then add a cheap classifier as a fallback, exactly the deterministic-router-with-an-LLM-escape-hatch pattern:

async function classify(text: string): Promise<Task> {
  // cheap + fast; only runs on requests you couldn't tag yourself
  const r = await client.chat.completions.create({
    model: "openai/gpt-5.6-terra",
    max_tokens: 1,
    messages: [{
      role: "user",
      content: `Reply exactly "frontend" if this asks to build/redesign UI ` +
               `(component, page, dashboard, CSS), else "general":\n\n${text}`,
    }],
  });
  return r.choices[0].message.content?.trim() === "frontend" ? "frontend" : "general";
}

A keyword match on component / landing page / dashboard / Tailwind / CSS is a fine first cut, but it's brittle — ship it as a stopgap, not the answer.

The two K3 gotchas that set the guardrails#

  1. The slug is moonshotai/kimi-k3. Dropping the ai (moonshot/kimi-k3) is the number-one first-call 404. Get this right once and forget it.
  2. K3 always reasons. You cannot disable thinking, and reasoning tokens bill at the $15/M output rate. That's why the router above does three things: caps max_tokens, drops reasoning effort to low for straightforward UI (K3 defaults to max), and keeps the system prompt stable so it's cacheable — cached input is ~$0.30/M, a 10x discount. If you resend a design-system or component-library preamble on every call (you should), caching keeps it from being re-billed at full input price. Put stable context first, let the variable request append at the end.

The math that decides whether it's worth it#

K3 is flagship-priced, so the routing only pays off if it wins on your screens. Sketch the bill before and after:

If front-end generation is, say, 20% of your calls, routing sends only that fifth to a flagship-priced model and leaves the other 80% on your cheaper default — versus paying K3's ~$3/$15 (plus mandatory reasoning tokens) on everything if you switched wholesale. The routed bill is a small premium on a slice; the wholesale bill is a flagship rate on your whole business.

Then gate it on preference, not on the leaderboard. Freeze 15–30 real front-end tasks from your backlog, run K3 and your current model through the same prompt, and look at the rendered UI. Keep the route only where K3 clearly wins — and remember it loses Gaming to Claude Fable 5, so exclude that lane. If you just want to kick the tires first, call Kimi K3's API in ten minutes and eyeball one component before you wire any of this.

The whole point of routing by task is that it's reversible: a leaderboard win becomes one map entry you can add, measure, and pull back out — never a migration you have to live with.