> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fireworks.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> For Fireworks Nexus, start at https://docs.fireworks.ai/nexus.
> Use https://docs.fireworks.ai/nexus/quickstart for coding harnesses, custom agents, APIs, SDKs, and LLM gateways.
> Use https://docs.fireworks.ai/nexus/firerouter for how model routers work, the supported model list, composition, closed-model credentials, and pricing.
> Prefer canonical short model IDs such as firerouter/opus. In LiteLLM litellm_params.model, use the full path fireworks_ai/accounts/fireworks/routers/firerouter/opus.
> Family names such as opus track the latest evaluated family version; do not describe them as fixed model versions.

# FireRouter Setup

> Set up FireRouter in a coding harness with FireConnect, in an LLM gateway such as LiteLLM or Portkey, or from the OpenAI, Anthropic, or Fireworks SDK.

export const FireRouterHarnessSetup = () => {
  const HARNESSES = {
    claude: {
      label: "Claude Code",
      cli: "claude",
      local: "login",
      headers: true,
      verified: true
    },
    codex: {
      label: "Codex CLI, Codex app, and ChatGPT desktop",
      cli: "codex",
      local: "env",
      headers: true,
      verified: true
    },
    opencode: {
      label: "OpenCode",
      cli: "opencode",
      local: "flag",
      headers: true,
      verified: true
    },
    pi: {
      label: "Pi",
      cli: "pi",
      local: "flag",
      headers: true,
      verified: true
    },
    vscode: {
      label: "VS Code",
      cli: "vscode",
      local: "flag",
      headers: true,
      verified: false
    },
    "copilot-cli": {
      label: "Copilot CLI",
      cli: "copilot-cli",
      local: "none",
      headers: true,
      verified: true
    },
    deepseek: {
      label: "DeepSeek Harness",
      cli: "deepseek",
      local: "none",
      headers: true,
      verified: true
    },
    cursor: {
      label: "Cursor IDE",
      cli: "cursor",
      local: "none",
      headers: false,
      verified: false
    },
    "copilot-app": {
      label: "Copilot App",
      cli: "copilot-app",
      local: "none",
      headers: false,
      verified: false
    }
  };
  const PRESETS = ["auto", "auto-instant", "firerouter"];
  const MODELS = [{
    id: "opus",
    provider: "anthropic"
  }, {
    id: "claude-opus-5-5",
    provider: "anthropic"
  }, {
    id: "claude-opus-5",
    provider: "anthropic"
  }, {
    id: "fable",
    provider: "anthropic"
  }, {
    id: "claude-sonnet-5",
    provider: "anthropic"
  }, {
    id: "sol",
    provider: "openai"
  }, {
    id: "gpt-5.6-sol",
    provider: "openai"
  }, {
    id: "astra",
    provider: "openai"
  }, {
    id: "luna",
    provider: "openai"
  }, {
    id: "kimi-k3",
    provider: "fireworks"
  }, {
    id: "kimi-k3-fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3",
    provider: "fireworks"
  }, {
    id: "glm-5p3-fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3-flash",
    provider: "fireworks"
  }, {
    id: "deepseek-v4p1-flash",
    provider: "fireworks"
  }];
  const MAX_MEMBERS = 8;
  const [harness, setHarness] = useState("claude");
  const [mode, setMode] = useState("build");
  const [routerId, setRouterId] = useState("auto");
  const [primary, setPrimary] = useState("opus");
  const [others, setOthers] = useState(["glm-5p3"]);
  const [method, setMethod] = useState("fireconnect");
  const [providerKeys, setProviderKeys] = useState(false);
  const [copied, setCopied] = useState("");
  const members = [primary, ...others.filter(m => m !== primary)];
  const toggleOther = m => {
    if (m === primary) return;
    if (others.includes(m)) setOthers(others.filter(o => o !== m)); else if (members.length < MAX_MEMBERS) setOthers([...others, m]);
  };
  const h = HARNESSES[harness];
  const id = mode === "build" ? `firerouter/${members.join("/")}` : routerId.trim();
  const isAuto = id === "auto" || id === "auto-instant";
  const valid = isAuto || (/^firerouter(\/[A-Za-z0-9.\-]+){0,8}$/).test(id);
  const segs = id.split("/").slice(1).map(s => s.toLowerCase());
  const needsAnthropic = id === "firerouter" || segs.some(s => s === "opus" || s === "fable" || s.startsWith("claude"));
  const needsOpenAI = segs.some(s => ["sol", "astra", "luna"].includes(s) || s.startsWith("gpt"));
  const sendAnthropic = needsAnthropic && !providerKeys;
  const sendOpenAI = needsOpenAI && !providerKeys;
  const notes = [];
  const blocks = [];
  const fcBlocked = method === "fireconnect" && needsAnthropic && h.local === "none";
  if (valid && isAuto) {
    notes.push(["ok", id === "auto" ? "auto routes each turn across Fireworks open models. It needs only your Fireworks API key." : "auto-instant routes each turn to the lowest-latency Fireworks open model. It needs only your Fireworks API key."]);
  }
  if (valid && id === "firerouter") {
    notes.push(["ok", "Bare firerouter uses Claude Opus with an Anthropic key, or GPT Sol with an OpenAI key. With both, it picks GPT Sol for Codex and Responses API requests, and Claude Opus otherwise." + (method === "manual" && !providerKeys ? " To use GPT Sol with only an OpenAI key, send x-openai-api-key instead of x-anthropic-api-key." : "")]);
  }
  if (!valid) {
    notes.push(["stop", "Enter auto, auto-instant, or a router ID such as firerouter/opus, with up to eight models."]);
  } else if (method === "fireconnect") {
    if (fcBlocked) {
      notes.push(["stop", h.headers ? `FireConnect cannot add routes with Claude models to ${h.label} yet. Switch to Manual setup, which sends your Anthropic key as a header.` : `FireConnect cannot add routes with Claude models to ${h.label}, and ${h.label} cannot send an Anthropic key. Switch to Manual setup with an Anthropic Provider Key, or use a route with only GPT or open models.`]);
    } else {
      let cmd = `fireconnect ${h.cli} --model ${id}`;
      if (needsAnthropic && !providerKeys && (h.local === "flag" || h.local === "env")) {
        cmd += ` --anthropic-api-key "$ANTHROPIC_API_KEY"`;
      }
      blocks.push({
        title: "Terminal",
        lang: "bash",
        code: `fireconnect login\n${cmd}\nfireconnect ${h.cli} status`
      });
      if (needsAnthropic && h.local === "login") {
        notes.push(["ok", "Claude models in the route use your Claude login. No Anthropic key is needed."]);
      }
      if (needsAnthropic && h.local === "env" && !providerKeys) {
        notes.push(["ok", "FireConnect adds ANTHROPIC_API_KEY to your shell config. Open a new terminal, or run the source command it prints, before starting Codex."]);
      }
      if (needsOpenAI && !providerKeys) {
        notes.push(["warn", h.headers ? "FireConnect has no OpenAI key option yet. Without an OpenAI Provider Key, FireRouter leaves the GPT models out and serves every turn with open models. To send your OpenAI key yourself, use Manual setup." : "FireConnect has no OpenAI key option. Without an OpenAI Provider Key, FireRouter leaves the GPT models out and serves every turn with open models."]);
      }
      if (harness === "deepseek") {
        notes.push(["warn", "dsh 0.1.7 reads each profile's cordis.patch.yml and ignores the ~/.dsh/settings.yaml file FireConnect writes. If dsh still uses its DeepSeek provider, use Manual setup."]);
      }
      notes.push(["ok", harness === "claude" ? "Start a new Claude Code session, then pick the router in /model." : `Restart ${h.label} and pick the router in its model picker.`]);
    }
  } else {
    const aKey = "sk-ant-YOUR_ANTHROPIC_KEY";
    const oKey = "sk-YOUR_OPENAI_KEY";
    const fw = "fw_YOUR_FIREWORKS_API_KEY";
    const missing = [needsAnthropic && "Anthropic", needsOpenAI && "OpenAI"].filter(Boolean).join(" and ");
    if ((sendAnthropic || sendOpenAI) && !h.headers) {
      notes.push(["warn", `${h.label} cannot send provider keys. Connect the ${missing} key in Provider Keys, then turn on the Provider Keys option above. Without it, FireRouter leaves those models out and serves every turn with open models.`]);
    }
    if (harness === "claude") {
      const env = {
        ANTHROPIC_BASE_URL: "https://api.fireworks.ai/inference",
        ENABLE_TOOL_SEARCH: "true"
      };
      if (sendAnthropic) {
        env.ANTHROPIC_API_KEY = aKey;
        env.ANTHROPIC_CUSTOM_HEADERS = `X-Fireworks-Api-Key: ${fw}${sendOpenAI ? `\nx-openai-api-key: ${oKey}` : ""}`;
      } else {
        env.ANTHROPIC_AUTH_TOKEN = fw;
        if (sendOpenAI) env.ANTHROPIC_CUSTOM_HEADERS = `x-openai-api-key: ${oKey}`;
      }
      blocks.push({
        title: "~/.claude/settings.json",
        lang: "json",
        code: JSON.stringify({
          env,
          model: id
        }, null, 2)
      });
      if (sendAnthropic) notes.push(["ok", "Claude Code sends the Anthropic key as its API key, and the Fireworks key in a custom header."]);
    } else if (harness === "codex") {
      const envHeaders = [sendAnthropic && `"x-anthropic-api-key" = "ANTHROPIC_API_KEY"`, sendOpenAI && `"x-openai-api-key" = "OPENAI_API_KEY"`].filter(Boolean);
      blocks.push({
        title: "~/.codex/config.toml",
        lang: "toml",
        code: `model_provider = "fireworks-ai"\nmodel = "${id}"\n\n[model_providers.fireworks-ai]\nname = "Fireworks"\nbase_url = "https://api.fireworks.ai/inference/v1"\nwire_api = "responses"\nenv_key = "FIREWORKS_API_KEY"${envHeaders.length ? `\nenv_http_headers = { ${envHeaders.join(", ")} }` : ""}`
      });
      const exports = ["FIREWORKS_API_KEY", sendAnthropic && "ANTHROPIC_API_KEY", sendOpenAI && "OPENAI_API_KEY"].filter(Boolean);
      blocks.push({
        title: "Terminal",
        lang: "bash",
        code: `${exports.map(e => `export ${e}="..."`).join("\n")}\ncodex`
      });
      notes.push(["ok", "The Codex CLI, Codex app, and ChatGPT desktop app all read this file. Quit the desktop apps before editing it."]);
    } else if (harness === "opencode") {
      const headers = {};
      if (sendAnthropic) headers["x-anthropic-api-key"] = "{env:ANTHROPIC_API_KEY}";
      if (sendOpenAI) headers["x-openai-api-key"] = "{env:OPENAI_API_KEY}";
      const options = {
        apiKey: "{env:FIREWORKS_API_KEY}",
        ...Object.keys(headers).length ? {
          headers
        } : {}
      };
      blocks.push({
        title: "~/.config/opencode/opencode.json",
        lang: "json",
        code: JSON.stringify({
          $schema: "https://opencode.ai/config.json",
          provider: {
            "fireworks-ai": {
              options,
              models: {
                [id.slice(0)]: {
                  name: "FireRouter"
                }
              }
            }
          },
          model: `fireworks-ai/${id}`
        }, null, 2)
      });
    } else if (harness === "pi") {
      const headers = {};
      if (sendAnthropic) headers["x-anthropic-api-key"] = aKey;
      if (sendOpenAI) headers["x-openai-api-key"] = oKey;
      blocks.push({
        title: "~/.pi/agent/models.json",
        lang: "json",
        code: JSON.stringify({
          providers: {
            fireworks: {
              ...Object.keys(headers).length ? {
                headers
              } : {},
              models: [{
                id,
                name: "FireRouter",
                reasoning: true,
                input: ["text", "image"],
                contextWindow: 1000000,
                maxTokens: 131072
              }]
            }
          }
        }, null, 2)
      });
      blocks.push({
        title: "~/.pi/agent/settings.json",
        lang: "json",
        code: JSON.stringify({
          defaultProvider: "fireworks",
          defaultModel: id
        }, null, 2)
      });
      notes.push(["ok", "Pi's built-in fireworks provider reads FIREWORKS_API_KEY from your environment."]);
    } else if (harness === "vscode") {
      const requestHeaders = {};
      if (sendAnthropic) requestHeaders["x-anthropic-api-key"] = aKey;
      if (sendOpenAI) requestHeaders["x-openai-api-key"] = oKey;
      notes.push(["ok", "Add a Custom Endpoint in Copilot Chat (see GitHub Copilot in these docs), then add the model entry below to chatLanguageModels.json. Quit VS Code before editing the file."]);
      blocks.push({
        title: "chatLanguageModels.json (model entry)",
        lang: "json",
        code: JSON.stringify({
          id,
          name: "FireRouter",
          url: "https://api.fireworks.ai/inference",
          maxInputTokens: 1000000,
          maxOutputTokens: 131072,
          toolCalling: true,
          vision: true,
          ...Object.keys(requestHeaders).length ? {
            requestHeaders
          } : {}
        }, null, 2)
      });
    } else if (harness === "cursor") {
      blocks.push({
        title: "Cursor IDE settings",
        lang: "text",
        code: `Settings > Models > API Keys\n  OpenAI API Key:          ${fw}\n  Override OpenAI Base URL: https://api.fireworks.ai/inference/v1\n\nSettings > Models > Add model\n  ${id}`
      });
    } else if (harness === "copilot-app") {
      blocks.push({
        title: "Copilot App: Settings > Model providers",
        lang: "text",
        code: `Type:     OpenAI (Chat Completions)\nBase URL: https://api.fireworks.ai/inference/v1\nAPI key:  ${fw}\nModel:    ${id}`
      });
    } else if (harness === "copilot-cli") {
      blocks.push({
        title: "~/.copilot/providers.json",
        lang: "json",
        code: JSON.stringify({
          providers: [{
            name: "fireworks",
            type: "openai",
            wireApi: "completions",
            baseUrl: "https://api.fireworks.ai/inference/v1",
            apiKey: fw,
            ...sendAnthropic || sendOpenAI ? {
              headers: {
                ...sendAnthropic ? {
                  "x-anthropic-api-key": aKey
                } : {},
                ...sendOpenAI ? {
                  "x-openai-api-key": oKey
                } : {}
              }
            } : {}
          }],
          models: [{
            id,
            provider: "fireworks",
            wireModel: id,
            name: "FireRouter",
            maxPromptTokens: 1000000,
            maxContextWindowTokens: 1000000,
            maxOutputTokens: 131072
          }]
        }, null, 2)
      });
      blocks.push({
        title: "~/.copilot/settings.json",
        lang: "json",
        code: JSON.stringify({
          model: `fireworks/${id}`
        }, null, 2)
      });
    } else if (harness === "deepseek") {
      const hdrLines = [sendAnthropic && `          x-anthropic-api-key: ${aKey}`, sendOpenAI && `          x-openai-api-key: ${oKey}`].filter(Boolean);
      blocks.push({
        title: "~/.dsh/profiles/<profile>/cordis.patch.yml",
        lang: "yaml",
        code: `- id: llm-pi-ai\n  config:\n    providers:\n      fireworks:\n        displayName: Fireworks\n        apiKeyEnv: FIREWORKS_API_KEY\n        api: openai-completions\n        baseURL: https://api.fireworks.ai/inference/v1${hdrLines.length ? `\n        headers:\n${hdrLines.join("\n")}` : ""}\n        models:\n          - id: ${id}\n            name: FireRouter\n            reasoning: true\n            contextWindow: 1000000\n            maxTokens: 131072\n- id: agent-default-model\n  config:\n    provider: fireworks\n    model: ${id}`
      });
      blocks.push({
        title: "Terminal",
        lang: "bash",
        code: `export FIREWORKS_API_KEY="..."\ndsh tui`
      });
      notes.push(["ok", "Add the patch to each dsh profile you use, such as tui, web, or headless. Run dsh --profile <name> --dump-config once to create the profile folder."]);
    }
    notes.push(["ok", h.verified ? "This configuration was tested with a live request." : "This configuration mirrors the settings FireConnect writes for this harness."]);
  }
  const copy = (text, key) => {
    try {
      navigator.clipboard.writeText(text);
      setCopied(key);
      setTimeout(() => setCopied(""), 1500);
    } catch (e) {}
  };
  const [dark, setDark] = useState(false);
  useEffect(() => {
    const el = document.documentElement;
    const update = () => setDark(el.classList.contains("dark"));
    update();
    const observer = new MutationObserver(update);
    observer.observe(el, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const C = {
    primary: "rgb(var(--primary))",
    tint: "rgb(var(--primary) / 0.1)",
    ring: "rgb(var(--primary) / 0.35)",
    bg: dark ? "rgb(var(--background-dark))" : "#ffffff",
    surface: dark ? "rgb(var(--gray-900))" : "rgb(var(--gray-50))",
    raised: dark ? "rgb(var(--gray-800))" : "#ffffff",
    border: dark ? "rgb(var(--gray-800))" : "rgb(var(--gray-200))",
    text: dark ? "rgb(var(--gray-50))" : "rgb(var(--gray-900))",
    muted: dark ? "rgb(var(--gray-400))" : "rgb(var(--gray-500))",
    anthropic: "#d97757",
    openai: "#10a37f"
  };
  const NOTE = {
    ok: {
      fg: C.primary,
      bg: C.tint,
      icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
    },
    warn: {
      fg: "#d97706",
      bg: "rgb(217 119 6 / 0.1)",
      icon: "M12 9v4m0 4h.01M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z"
    },
    stop: {
      fg: "#dc2626",
      bg: "rgb(220 38 38 / 0.1)",
      icon: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
    }
  };
  const TILE_META = {
    claude: "Messages",
    codex: "Responses",
    opencode: "Chat Completions",
    pi: "Chat Completions",
    vscode: "Chat Completions",
    "copilot-cli": "Chat Completions",
    deepseek: "Chat Completions",
    cursor: "Chat Completions",
    "copilot-app": "Chat Completions"
  };
  const TILE_NAME = {
    codex: "Codex",
    "copilot-cli": "Copilot CLI",
    "copilot-app": "Copilot App"
  };
  const PROVIDER = {
    fireworks: {
      label: "Fireworks open models",
      color: C.primary
    },
    anthropic: {
      label: "Anthropic",
      color: C.anthropic
    },
    openai: {
      label: "OpenAI",
      color: C.openai
    }
  };
  const mono = "var(--font-mono, ui-monospace, monospace)";
  const chip = (active, color) => ({
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "4px 11px",
    borderRadius: 999,
    fontSize: 12,
    cursor: "pointer",
    fontFamily: mono,
    transition: "all 150ms",
    border: `1px solid ${active ? color : C.border}`,
    background: active ? color : C.raised,
    color: active ? "#fff" : C.text
  });
  const StepLabel = ({n, children, htmlFor}) => <label htmlFor={htmlFor} style={{
    display: "flex",
    alignItems: "center",
    gap: 10,
    fontSize: 14,
    fontWeight: 600,
    color: C.text,
    marginBottom: 10
  }}>
      <span style={{
    width: 22,
    height: 22,
    borderRadius: 999,
    background: C.primary,
    color: "#fff",
    fontSize: 12,
    fontWeight: 700,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>{n}</span>
      {children}
    </label>;
  const Pill = ({color, children}) => <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "3px 10px",
    borderRadius: 999,
    fontSize: 12,
    fontWeight: 500,
    color: C.text,
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
      <span style={{
    width: 7,
    height: 7,
    borderRadius: 999,
    background: color
  }} />
      {children}
    </span>;
  const Segmented = ({options, value, onChange}) => <div style={{
    display: "inline-flex",
    padding: 3,
    borderRadius: 10,
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
      {options.map(([k, l]) => <button key={k} type="button" onClick={() => onChange(k)} style={{
    padding: "6px 14px",
    borderRadius: 8,
    fontSize: 13,
    fontWeight: 600,
    border: "none",
    cursor: "pointer",
    background: value === k ? C.primary : "transparent",
    color: value === k ? "#fff" : C.muted,
    boxShadow: value === k ? "0 1px 2px rgb(0 0 0 / 0.15)" : "none",
    transition: "all 150ms"
  }}>
          {l}
        </button>)}
    </div>;
  const Toggle = ({checked, onChange, children}) => <label style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 10,
    fontSize: 13,
    color: C.text,
    cursor: "pointer"
  }}>
      <input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} style={{
    position: "absolute",
    opacity: 0,
    width: 1,
    height: 1
  }} />
      <span style={{
    width: 34,
    height: 20,
    borderRadius: 999,
    background: checked ? C.primary : dark ? "rgb(var(--gray-700))" : "rgb(var(--gray-300))",
    position: "relative",
    transition: "background 150ms",
    flexShrink: 0
  }}>
        <span style={{
    position: "absolute",
    top: 2,
    left: checked ? 16 : 2,
    width: 16,
    height: 16,
    borderRadius: 999,
    background: "#fff",
    boxShadow: "0 1px 2px rgb(0 0 0 / 0.3)",
    transition: "left 150ms"
  }} />
      </span>
      {children}
    </label>;
  const CopyIcon = ({done}) => done ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M5 13l4 4L19 7" stroke="#34d399" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" /></svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="2" /><path d="M5 15V5a2 2 0 012-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg>;
  return <div data-testid="fr-harness" className="not-prose" style={{
    margin: "16px 0 24px",
    borderRadius: 16,
    border: `1px solid ${C.border}`,
    background: C.bg,
    overflow: "hidden",
    color: C.text,
    boxShadow: dark ? "none" : "0 1px 3px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(var(--primary) / 0.18)"
  }}>
      <div style={{
    padding: "18px 22px",
    borderBottom: `1px solid ${C.border}`,
    background: `linear-gradient(135deg, ${C.tint}, transparent 70%)`,
    display: "flex",
    alignItems: "center",
    gap: 12
  }}>
        <span style={{
    width: 34,
    height: 34,
    borderRadius: 10,
    background: C.primary,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M4 17l6-6-6-6M12 19h8" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </span>
        <div>
          <div style={{
    fontSize: 15,
    fontWeight: 700,
    color: C.text
  }}>Set up FireRouter in your harness</div>
          <div style={{
    fontSize: 13,
    color: C.muted
  }}>Pick a harness, then a preset or your own route. Copy the command or config.</div>
        </div>
      </div>

      <div style={{
    padding: 22,
    display: "flex",
    flexDirection: "column",
    gap: 24
  }}>
        <div>
          <StepLabel n="1">Harness</StepLabel>
          <div role="radiogroup" aria-label="Harness" style={{
    display: "grid",
    gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
    gap: 8
  }}>
            {Object.entries(HARNESSES).map(([k, v]) => {
    const active = k === harness;
    return <button key={k} type="button" role="radio" aria-checked={active} data-harness={k} onClick={() => setHarness(k)} style={{
      textAlign: "left",
      padding: "9px 10px",
      borderRadius: 10,
      cursor: "pointer",
      transition: "all 150ms",
      border: `1.5px solid ${active ? C.primary : C.border}`,
      background: active ? C.tint : C.raised,
      boxShadow: active ? `0 0 0 3px ${C.ring}` : "none"
    }}>
                  <div style={{
      fontSize: 13,
      fontWeight: 600,
      color: C.text
    }}>{TILE_NAME[k] || v.label}</div>
                  <div style={{
      fontSize: 11,
      color: C.muted,
      marginTop: 2
    }}>{TILE_META[k]}</div>
                </button>;
  })}
          </div>
          <select id="fr-harness-select" aria-hidden="true" tabIndex={-1} value={harness} onChange={e => setHarness(e.target.value)} style={{
    display: "none"
  }}>
            {Object.keys(HARNESSES).map(k => <option key={k} value={k}>{k}</option>)}
          </select>
        </div>

        <div>
          <StepLabel n="2" htmlFor="fr-harness-id">Model or router ID</StepLabel>
          <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6,
    marginBottom: 10
  }}>
            {PRESETS.map(p => {
    const active = mode === "text" && p === id;
    return <button key={p} type="button" data-preset={p} onClick={() => {
      setRouterId(p);
      setMode("text");
    }} style={chip(active, C.primary)}>
                  <span style={{
      width: 6,
      height: 6,
      borderRadius: 999,
      background: active ? "#fff" : p === "firerouter" ? C.anthropic : C.primary
    }} />
                  {p}
                </button>;
  })}
            <button type="button" data-preset="build" onClick={() => setMode("build")} style={{
    ...chip(mode === "build", C.primary),
    fontFamily: "inherit",
    fontWeight: 600
  }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" /></svg>
              Build your own
            </button>
          </div>
          <input id="fr-harness-id" value={id} onChange={e => {
    setRouterId(e.target.value);
    setMode("text");
  }} spellCheck={false} style={{
    width: "100%",
    boxSizing: "border-box",
    padding: "10px 14px",
    borderRadius: 10,
    fontSize: 14,
    fontFamily: mono,
    color: C.text,
    background: C.raised,
    border: `1.5px solid ${valid ? C.border : "#dc2626"}`,
    outline: "none"
  }} />
          {mode === "build" && <div data-testid="fr-harness-composer" style={{
    marginTop: 12,
    padding: 14,
    borderRadius: 12,
    background: C.surface,
    border: `1px solid ${C.border}`,
    display: "flex",
    flexDirection: "column",
    gap: 14
  }}>
              <div style={{
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    gap: 10
  }}>
                <label htmlFor="fr-harness-primary" style={{
    fontSize: 12,
    fontWeight: 600,
    color: C.muted
  }}>Primary</label>
                <select id="fr-harness-primary" value={primary} onChange={e => {
    setPrimary(e.target.value);
    setOthers(others.filter(m => m !== e.target.value));
  }} style={{
    padding: "6px 10px",
    borderRadius: 8,
    fontSize: 13,
    fontFamily: mono,
    color: C.text,
    background: C.raised,
    border: `1px solid ${C.border}`,
    outline: "none",
    colorScheme: dark ? "dark" : "light"
  }}>
                  {MODELS.map(m => <option key={m.id} value={m.id}>{m.id}</option>)}
                </select>
                <span style={{
    fontSize: 12,
    color: C.muted,
    marginLeft: "auto"
  }}>{members.length} of {MAX_MEMBERS} models</span>
              </div>
              {["fireworks", "anthropic", "openai"].map(p => <div key={p} style={{
    display: "flex",
    flexDirection: "column",
    gap: 6
  }}>
                  <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    fontSize: 12,
    fontWeight: 600,
    color: C.muted
  }}>
                    <span style={{
    width: 8,
    height: 8,
    borderRadius: 999,
    background: PROVIDER[p].color
  }} />
                    {PROVIDER[p].label}
                  </span>
                  <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6
  }}>
                    {MODELS.filter(m => m.provider === p).map(m => {
    const isPrimary = m.id === primary;
    const active = isPrimary || others.includes(m.id);
    const disabled = !active && members.length >= MAX_MEMBERS;
    return <button key={m.id} type="button" data-model={m.id} disabled={isPrimary || disabled} aria-pressed={active} onClick={() => toggleOther(m.id)} style={{
      ...chip(active, PROVIDER[p].color),
      fontWeight: active ? 600 : 400,
      opacity: disabled ? 0.4 : 1,
      cursor: disabled ? "not-allowed" : isPrimary ? "default" : "pointer"
    }}>
                          {m.id}{isPrimary ? " (primary)" : ""}
                        </button>;
  })}
                  </div>
                </div>)}
              <div style={{
    fontSize: 12,
    color: C.muted,
    lineHeight: 1.5
  }}>
                {members.length === 1 && MODELS.find(m => m.id === primary).provider !== "fireworks" ? "One closed model: FireRouter also routes to the Fireworks-managed open-model mix." : members.length === 1 ? "One open model: every turn uses this model, with no routing." : "FireRouter picks one of these models for each turn. The primary serves turns when routing is unclear."}
              </div>
            </div>}
        </div>

        <div>
          <StepLabel n="3">Setup method</StepLabel>
          <div style={{
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    gap: 16
  }}>
            <Segmented options={[["fireconnect", "FireConnect"], ["manual", "Manual setup"]]} value={method} onChange={setMethod} />
            <Toggle checked={providerKeys} onChange={setProviderKeys}>My account has Provider Keys connected</Toggle>
          </div>
        </div>

        <div style={{
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    gap: 8
  }}>
          <span style={{
    fontSize: 12,
    fontWeight: 600,
    color: C.muted,
    textTransform: "uppercase",
    letterSpacing: "0.04em",
    marginRight: 4
  }}>Credentials</span>
          <Pill color={C.primary}>Fireworks API key</Pill>
          {id === "firerouter" ? <Pill color={C.anthropic}>Anthropic or OpenAI{providerKeys ? " (Provider Key)" : ""}</Pill> : needsAnthropic && <Pill color={C.anthropic}>Anthropic{providerKeys ? " (Provider Key)" : ""}</Pill>}
          {needsOpenAI && <Pill color={C.openai}>OpenAI{providerKeys ? " (Provider Key)" : ""}</Pill>}
        </div>

        {notes.length > 0 && <ul data-testid="fr-harness-notes" style={{
    listStyle: "none",
    margin: 0,
    padding: 0,
    display: "flex",
    flexDirection: "column",
    gap: 8
  }}>
            {notes.map(([kind, text], i) => <li key={i} style={{
    display: "flex",
    gap: 10,
    alignItems: "flex-start",
    padding: "10px 12px",
    borderRadius: 10,
    background: NOTE[kind].bg,
    fontSize: 13,
    lineHeight: 1.5,
    color: C.text
  }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{
    flexShrink: 0,
    marginTop: 2
  }}>
                  <path d={NOTE[kind].icon} stroke={NOTE[kind].fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
                <span>{text}</span>
              </li>)}
          </ul>}

        {blocks.map((b, i) => <div key={`${b.title}-${i}`} style={{
    borderRadius: 12,
    overflow: "hidden",
    background: "#0f0d14",
    border: "1px solid #27232e"
  }}>
            <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    padding: "8px 12px",
    background: "#17141d",
    borderBottom: "1px solid #27232e"
  }}>
              <div style={{
    display: "flex",
    alignItems: "center",
    gap: 10,
    minWidth: 0
  }}>
                <span style={{
    display: "inline-flex",
    gap: 5
  }}>
                  {["#ff5f57", "#febc2e", "#28c840"].map(c => <span key={c} style={{
    width: 9,
    height: 9,
    borderRadius: 999,
    background: c,
    opacity: 0.85
  }} />)}
                </span>
                <span style={{
    fontSize: 12,
    color: "#a1a1aa",
    fontFamily: "var(--font-mono, ui-monospace, monospace)",
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap"
  }}>{b.title}</span>
              </div>
              <button type="button" onClick={() => copy(b.code, `b${i}`)} aria-label="Copy" style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "4px 10px",
    borderRadius: 6,
    fontSize: 12,
    fontWeight: 500,
    cursor: "pointer",
    color: "#e4e4e7",
    background: "rgb(255 255 255 / 0.06)",
    border: "1px solid rgb(255 255 255 / 0.1)"
  }}>
                <CopyIcon done={copied === `b${i}`} />
                {copied === `b${i}` ? "Copied" : "Copy"}
              </button>
            </div>
            <pre style={{
    margin: 0,
    padding: "14px 16px",
    overflowX: "auto",
    fontSize: 12.5,
    lineHeight: 1.65,
    color: "#e4e4e7",
    background: "transparent",
    fontFamily: "var(--font-mono, ui-monospace, monospace)"
  }}>
              <code data-testid={`fr-harness-code-${i}`} data-title={b.title}>{b.code}</code>
            </pre>
          </div>)}
      </div>
    </div>;
};

export const FireRouterGatewaySetup = () => {
  const PRESETS = ["auto", "auto-instant", "firerouter"];
  const MODELS = [{
    id: "opus",
    provider: "anthropic"
  }, {
    id: "claude-opus-5-5",
    provider: "anthropic"
  }, {
    id: "claude-opus-5",
    provider: "anthropic"
  }, {
    id: "fable",
    provider: "anthropic"
  }, {
    id: "claude-sonnet-5",
    provider: "anthropic"
  }, {
    id: "sol",
    provider: "openai"
  }, {
    id: "gpt-5.6-sol",
    provider: "openai"
  }, {
    id: "astra",
    provider: "openai"
  }, {
    id: "luna",
    provider: "openai"
  }, {
    id: "kimi-k3",
    provider: "fireworks"
  }, {
    id: "kimi-k3-fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3",
    provider: "fireworks"
  }, {
    id: "glm-5p3-fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3-flash",
    provider: "fireworks"
  }, {
    id: "deepseek-v4p1-flash",
    provider: "fireworks"
  }];
  const MAX_MEMBERS = 8;
  const [gateway, setGateway] = useState("litellm");
  const [mode, setMode] = useState("build");
  const [routerId, setRouterId] = useState("auto");
  const [primary, setPrimary] = useState("opus");
  const [others, setOthers] = useState(["glm-5p3"]);
  const [creds, setCreds] = useState("config");
  const [bareKeys, setBareKeys] = useState("anthropic");
  const [copied, setCopied] = useState("");
  const members = [primary, ...others.filter(m => m !== primary)];
  const toggleOther = m => {
    if (m === primary) return;
    if (others.includes(m)) setOthers(others.filter(o => o !== m)); else if (members.length < MAX_MEMBERS) setOthers([...others, m]);
  };
  const id = mode === "build" ? `firerouter/${members.join("/")}` : routerId.trim();
  const isAuto = id === "auto" || id === "auto-instant";
  const valid = isAuto || (/^firerouter(\/[A-Za-z0-9.\-]+){0,8}$/).test(id);
  const bare = id === "firerouter";
  const segs = id.split("/").slice(1).map(s => s.toLowerCase());
  const needsAnthropic = bare ? bareKeys !== "openai" : segs.some(s => s === "opus" || s === "fable" || s.startsWith("claude"));
  const needsOpenAI = bare ? bareKeys !== "anthropic" : segs.some(s => ["sol", "astra", "luna"].includes(s) || s.startsWith("gpt"));
  const needsClosed = needsAnthropic || needsOpenAI;
  const CRED_OPTIONS = gateway === "litellm" ? [["provider", "Provider Keys"], ["config", "Keys in the gateway config"], ["client", "Keys from each client"]] : [["provider", "Provider Keys"], ["client", "Keys from each client"]];
  const effCreds = !needsClosed ? "none" : gateway === "portkey" && creds === "config" ? "client" : creds;
  const providerHeaders = [needsAnthropic && ["x-anthropic-api-key", "ANTHROPIC_API_KEY"], needsOpenAI && ["x-openai-api-key", "OPENAI_API_KEY"]].filter(Boolean);
  const prompt = "Say pong in one word.";
  const body = `  -d '{\n    "model": "${gateway === "portkey" ? `@YOUR_FIREWORKS_INTEGRATION/${id}` : id}",\n    "messages": [{"role": "user", "content": "${prompt}"}]\n  }'`;
  const notes = [];
  const blocks = [];
  if (!valid) {
    notes.push(["stop", "Enter auto, auto-instant, or a router ID such as firerouter/opus, with up to eight models."]);
  } else if (gateway === "litellm") {
    const lines = ["model_list:", `  - model_name: ${id}`, "    litellm_params:", `      model: fireworks_ai/accounts/fireworks/routers/${id}`, "      api_key: os.environ/FIREWORKS_API_KEY", "      api_base: https://api.fireworks.ai/inference/v1"];
    if (effCreds === "config") {
      lines.push("      extra_headers:", ...providerHeaders.map(([hdr, env]) => `        ${hdr}: os.environ/${env}`));
    }
    if (effCreds === "client") {
      lines.push("", "general_settings:", "  forward_client_headers_to_llm_api: true");
    }
    blocks.push({
      title: "config.yaml",
      code: lines.join("\n")
    });
    const exports = ["FIREWORKS_API_KEY", ...effCreds === "config" ? providerHeaders.map(([, env]) => env) : []];
    blocks.push({
      title: "Terminal: start the proxy",
      code: `${exports.map(e => `export ${e}="..."`).join("\n")}\nlitellm --config config.yaml --port 4000`
    });
    blocks.push({
      title: "Terminal: call the router",
      code: ["curl http://localhost:4000/chat/completions \\", `  -H "Authorization: Bearer $LITELLM_VIRTUAL_KEY" \\`, ...effCreds === "client" ? providerHeaders.map(([hdr, env]) => `  -H "${hdr}: $${env}" \\`) : [], `  -H "Content-Type: application/json" \\`, body].join("\n")
    });
    notes.push(["ok", "model_name is the ID clients send. litellm_params.model uses the full router path, because recent LiteLLM versions turn a short ID into a model path that returns 404."]);
    if (effCreds === "config") {
      notes.push(["ok", "LiteLLM adds the provider keys from its own environment to every request for this router. Clients send only their LiteLLM key. Set every variable before you start the proxy: a missing one makes each request fail with a 500."]);
    }
    if (effCreds === "client") {
      notes.push(["ok", "LiteLLM forwards client headers only when forward_client_headers_to_llm_api is on. Each client sends its own provider key."]);
    }
    notes.push(["ok", "LiteLLM returns model_name in the response model field, not the model that served the turn."]);
  } else {
    blocks.push({
      title: "Portkey app: Integrations",
      code: `Provider:  Fireworks\nAPI key:   fw_YOUR_FIREWORKS_API_KEY\nSlug:      YOUR_FIREWORKS_INTEGRATION`
    });
    const fwd = effCreds === "client" ? providerHeaders : [];
    blocks.push({
      title: "Terminal: call the router",
      code: ["curl https://api.portkey.ai/v1/chat/completions \\", `  -H "x-portkey-api-key: $PORTKEY_API_KEY" \\`, ...fwd.map(([hdr, env]) => `  -H "${hdr}: $${env}" \\`), ...fwd.length ? [`  -H "x-portkey-forward-headers: ${fwd.map(([hdr]) => hdr).join(",")}" \\`] : [], `  -H "Content-Type: application/json" \\`, body].join("\n")
    });
    notes.push(["ok", "Prefix the router ID with your integration's slug. Portkey sends the rest of the ID to Fireworks unchanged."]);
    if (effCreds === "client") {
      notes.push(["ok", "Portkey forwards only the headers named in x-portkey-forward-headers. Add them to x-portkey-sensitive-headers too, so Portkey masks the keys in its logs."]);
    }
  }
  if (valid && isAuto) {
    notes.unshift(["ok", `${id} routes each turn across Fireworks open models. It needs only your Fireworks API key.`]);
  }
  if (valid && bare) {
    notes.push(["ok", ({
      anthropic: "Bare firerouter uses Claude Opus with an Anthropic key. With an OpenAI Provider Key also connected, it picks GPT Sol for Responses API requests.",
      openai: "Bare firerouter uses GPT Sol with an OpenAI key. With an Anthropic Provider Key also connected, it picks Claude Opus for Chat Completions and Messages requests.",
      both: "With both keys, bare firerouter picks GPT Sol for Responses API requests and Claude Opus otherwise."
    })[bareKeys]]);
  }
  if (valid && effCreds === "provider") {
    notes.push(["ok", "An account admin connects the provider keys once in Settings > Provider Keys, so no provider credential passes through the gateway. Provider Keys is not yet available on every account."]);
  }
  if (valid && effCreds !== "none" && effCreds !== "provider") {
    notes.push(["warn", "Gateways often log request headers. Check that yours redacts the provider-key headers, or use Provider Keys to keep them out of the gateway."]);
  }
  const copy = (text, key) => {
    try {
      navigator.clipboard.writeText(text);
      setCopied(key);
      setTimeout(() => setCopied(""), 1500);
    } catch (e) {}
  };
  const [dark, setDark] = useState(false);
  useEffect(() => {
    const el = document.documentElement;
    const update = () => setDark(el.classList.contains("dark"));
    update();
    const observer = new MutationObserver(update);
    observer.observe(el, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const C = {
    primary: "rgb(var(--primary))",
    tint: "rgb(var(--primary) / 0.1)",
    ring: "rgb(var(--primary) / 0.35)",
    bg: dark ? "rgb(var(--background-dark))" : "#ffffff",
    surface: dark ? "rgb(var(--gray-900))" : "rgb(var(--gray-50))",
    raised: dark ? "rgb(var(--gray-800))" : "#ffffff",
    border: dark ? "rgb(var(--gray-800))" : "rgb(var(--gray-200))",
    text: dark ? "rgb(var(--gray-50))" : "rgb(var(--gray-900))",
    muted: dark ? "rgb(var(--gray-400))" : "rgb(var(--gray-500))",
    anthropic: "#d97757",
    openai: "#10a37f"
  };
  const PROVIDER = {
    fireworks: {
      label: "Fireworks open models",
      color: C.primary
    },
    anthropic: {
      label: "Anthropic",
      color: C.anthropic
    },
    openai: {
      label: "OpenAI",
      color: C.openai
    }
  };
  const NOTE = {
    ok: {
      fg: C.primary,
      bg: C.tint,
      icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
    },
    warn: {
      fg: "#d97706",
      bg: "rgb(217 119 6 / 0.1)",
      icon: "M12 9v4m0 4h.01M10.3 3.9L1.8 18a2 2 0 001.7 3h17a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z"
    },
    stop: {
      fg: "#dc2626",
      bg: "rgb(220 38 38 / 0.1)",
      icon: "M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
    }
  };
  const mono = "var(--font-mono, ui-monospace, monospace)";
  const chip = (active, color) => ({
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "4px 11px",
    borderRadius: 999,
    fontSize: 12,
    cursor: "pointer",
    fontFamily: mono,
    transition: "all 150ms",
    border: `1px solid ${active ? color : C.border}`,
    background: active ? color : C.raised,
    color: active ? "#fff" : C.text
  });
  const StepLabel = ({n, children, htmlFor}) => <label htmlFor={htmlFor} style={{
    display: "flex",
    alignItems: "center",
    gap: 10,
    fontSize: 14,
    fontWeight: 600,
    color: C.text,
    marginBottom: 10
  }}>
      <span style={{
    width: 22,
    height: 22,
    borderRadius: 999,
    background: C.primary,
    color: "#fff",
    fontSize: 12,
    fontWeight: 700,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>{n}</span>
      {children}
    </label>;
  const Pill = ({color, children}) => <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "3px 10px",
    borderRadius: 999,
    fontSize: 12,
    fontWeight: 500,
    color: C.text,
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
      <span style={{
    width: 7,
    height: 7,
    borderRadius: 999,
    background: color
  }} />
      {children}
    </span>;
  const Segmented = ({options, value, onChange, testid}) => <div data-testid={testid} style={{
    display: "inline-flex",
    flexWrap: "wrap",
    padding: 3,
    borderRadius: 10,
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
      {options.map(([k, l]) => <button key={k} type="button" data-value={k} onClick={() => onChange(k)} style={{
    padding: "6px 14px",
    borderRadius: 8,
    fontSize: 13,
    fontWeight: 600,
    border: "none",
    cursor: "pointer",
    background: value === k ? C.primary : "transparent",
    color: value === k ? "#fff" : C.muted,
    boxShadow: value === k ? "0 1px 2px rgb(0 0 0 / 0.15)" : "none",
    transition: "all 150ms"
  }}>
          {l}
        </button>)}
    </div>;
  const CopyIcon = ({done}) => done ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M5 13l4 4L19 7" stroke="#34d399" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" /></svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="2" /><path d="M5 15V5a2 2 0 012-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg>;
  const credLabel = name => effCreds === "provider" ? `${name} (Provider Key)` : effCreds === "config" ? `${name} (in the gateway)` : `${name} (from each client)`;
  return <div data-testid="fr-gateway" className="not-prose" style={{
    margin: "16px 0 24px",
    borderRadius: 16,
    border: `1px solid ${C.border}`,
    background: C.bg,
    overflow: "hidden",
    color: C.text,
    boxShadow: dark ? "none" : "0 1px 3px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(var(--primary) / 0.18)"
  }}>
      <div style={{
    padding: "18px 22px",
    borderBottom: `1px solid ${C.border}`,
    background: `linear-gradient(135deg, ${C.tint}, transparent 70%)`,
    display: "flex",
    alignItems: "center",
    gap: 12
  }}>
        <span style={{
    width: 34,
    height: 34,
    borderRadius: 10,
    background: C.primary,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16M8 4v6M16 9v6M11 14v6" stroke="#fff" strokeWidth="2" strokeLinecap="round" /></svg>
        </span>
        <div>
          <div style={{
    fontSize: 15,
    fontWeight: 700,
    color: C.text
  }}>Set up FireRouter in your LLM gateway</div>
          <div style={{
    fontSize: 13,
    color: C.muted
  }}>Pick a gateway, a route, and where provider keys live. Copy the config and a test request.</div>
        </div>
      </div>

      <div style={{
    padding: 22,
    display: "flex",
    flexDirection: "column",
    gap: 24
  }}>
        <div>
          <StepLabel n="1">Gateway</StepLabel>
          <Segmented testid="fr-gateway-gateway" options={[["litellm", "LiteLLM"], ["portkey", "Portkey"]]} value={gateway} onChange={setGateway} />
        </div>

        <div>
          <StepLabel n="2" htmlFor="fr-gateway-id">Model or router ID</StepLabel>
          <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6,
    marginBottom: 10
  }}>
            {PRESETS.map(p => {
    const active = mode === "text" && p === id;
    return <button key={p} type="button" data-preset={p} onClick={() => {
      setRouterId(p);
      setMode("text");
    }} style={chip(active, C.primary)}>
                  <span style={{
      width: 6,
      height: 6,
      borderRadius: 999,
      background: active ? "#fff" : p === "firerouter" ? C.anthropic : C.primary
    }} />
                  {p}
                </button>;
  })}
            <button type="button" data-preset="build" onClick={() => setMode("build")} style={{
    ...chip(mode === "build", C.primary),
    fontFamily: "inherit",
    fontWeight: 600
  }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" /></svg>
              Build your own
            </button>
          </div>
          <input id="fr-gateway-id" value={id} onChange={e => {
    setRouterId(e.target.value);
    setMode("text");
  }} spellCheck={false} style={{
    width: "100%",
    boxSizing: "border-box",
    padding: "10px 14px",
    borderRadius: 10,
    fontSize: 14,
    fontFamily: mono,
    color: C.text,
    background: C.raised,
    border: `1.5px solid ${valid ? C.border : "#dc2626"}`,
    outline: "none"
  }} />
          {mode === "build" && <div data-testid="fr-gateway-composer" style={{
    marginTop: 12,
    padding: 14,
    borderRadius: 12,
    background: C.surface,
    border: `1px solid ${C.border}`,
    display: "flex",
    flexDirection: "column",
    gap: 14
  }}>
              <div style={{
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    gap: 10
  }}>
                <label htmlFor="fr-gateway-primary" style={{
    fontSize: 12,
    fontWeight: 600,
    color: C.muted
  }}>Primary</label>
                <select id="fr-gateway-primary" value={primary} onChange={e => {
    setPrimary(e.target.value);
    setOthers(others.filter(m => m !== e.target.value));
  }} style={{
    padding: "6px 10px",
    borderRadius: 8,
    fontSize: 13,
    fontFamily: mono,
    color: C.text,
    background: C.raised,
    border: `1px solid ${C.border}`,
    outline: "none",
    colorScheme: dark ? "dark" : "light"
  }}>
                  {MODELS.map(m => <option key={m.id} value={m.id}>{m.id}</option>)}
                </select>
                <span style={{
    fontSize: 12,
    color: C.muted,
    marginLeft: "auto"
  }}>{members.length} of {MAX_MEMBERS} models</span>
              </div>
              {["fireworks", "anthropic", "openai"].map(p => <div key={p} style={{
    display: "flex",
    flexDirection: "column",
    gap: 6
  }}>
                  <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    fontSize: 12,
    fontWeight: 600,
    color: C.muted
  }}>
                    <span style={{
    width: 8,
    height: 8,
    borderRadius: 999,
    background: PROVIDER[p].color
  }} />
                    {PROVIDER[p].label}
                  </span>
                  <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6
  }}>
                    {MODELS.filter(m => m.provider === p).map(m => {
    const isPrimary = m.id === primary;
    const active = isPrimary || others.includes(m.id);
    const disabled = !active && members.length >= MAX_MEMBERS;
    return <button key={m.id} type="button" data-model={m.id} disabled={isPrimary || disabled} aria-pressed={active} onClick={() => toggleOther(m.id)} style={{
      ...chip(active, PROVIDER[p].color),
      fontWeight: active ? 600 : 400,
      opacity: disabled ? 0.4 : 1,
      cursor: disabled ? "not-allowed" : isPrimary ? "default" : "pointer"
    }}>
                          {m.id}{isPrimary ? " (primary)" : ""}
                        </button>;
  })}
                  </div>
                </div>)}
              <div style={{
    fontSize: 12,
    color: C.muted,
    lineHeight: 1.5
  }}>
                {members.length === 1 && MODELS.find(m => m.id === primary).provider !== "fireworks" ? "One closed model: FireRouter also routes to the Fireworks-managed open-model mix." : members.length === 1 ? "One open model: every turn uses this model, with no routing." : "FireRouter picks one of these models for each turn. The primary serves turns when routing is unclear."}
              </div>
            </div>}
        </div>

        <div>
          <StepLabel n="3">Closed-model credentials</StepLabel>
          {needsClosed ? <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 10,
    alignItems: "flex-start"
  }}>
              {bare && <Segmented testid="fr-gateway-bare-keys" options={[["anthropic", "Anthropic key"], ["openai", "OpenAI key"], ["both", "Both"]]} value={bareKeys} onChange={setBareKeys} />}
              <Segmented testid="fr-gateway-creds" options={CRED_OPTIONS} value={effCreds} onChange={setCreds} />
            </div> : <div style={{
    fontSize: 13,
    color: C.muted
  }}>This route has only Fireworks open models. The gateway needs only your Fireworks API key.</div>}
        </div>

        <div style={{
    display: "flex",
    flexWrap: "wrap",
    alignItems: "center",
    gap: 8
  }}>
          <span style={{
    fontSize: 12,
    fontWeight: 600,
    color: C.muted,
    textTransform: "uppercase",
    letterSpacing: "0.04em",
    marginRight: 4
  }}>Credentials</span>
          <Pill color={C.primary}>Fireworks API key (in the gateway)</Pill>
          {needsAnthropic && <Pill color={C.anthropic}>{credLabel("Anthropic")}</Pill>}
          {needsOpenAI && <Pill color={C.openai}>{credLabel("OpenAI")}</Pill>}
        </div>

        {notes.length > 0 && <ul data-testid="fr-gateway-notes" style={{
    listStyle: "none",
    margin: 0,
    padding: 0,
    display: "flex",
    flexDirection: "column",
    gap: 8
  }}>
            {notes.map(([kind, text], i) => <li key={i} style={{
    display: "flex",
    gap: 10,
    alignItems: "flex-start",
    padding: "10px 12px",
    borderRadius: 10,
    background: NOTE[kind].bg,
    fontSize: 13,
    lineHeight: 1.5,
    color: C.text
  }}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style={{
    flexShrink: 0,
    marginTop: 2
  }}>
                  <path d={NOTE[kind].icon} stroke={NOTE[kind].fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                </svg>
                <span>{text}</span>
              </li>)}
          </ul>}

        {blocks.map((b, i) => <div key={`${b.title}-${i}`} style={{
    borderRadius: 12,
    overflow: "hidden",
    background: "#0f0d14",
    border: "1px solid #27232e"
  }}>
            <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    padding: "8px 12px",
    background: "#17141d",
    borderBottom: "1px solid #27232e"
  }}>
              <div style={{
    display: "flex",
    alignItems: "center",
    gap: 10,
    minWidth: 0
  }}>
                <span style={{
    display: "inline-flex",
    gap: 5
  }}>
                  {["#ff5f57", "#febc2e", "#28c840"].map(c => <span key={c} style={{
    width: 9,
    height: 9,
    borderRadius: 999,
    background: c,
    opacity: 0.85
  }} />)}
                </span>
                <span style={{
    fontSize: 12,
    color: "#a1a1aa",
    fontFamily: mono,
    overflow: "hidden",
    textOverflow: "ellipsis",
    whiteSpace: "nowrap"
  }}>{b.title}</span>
              </div>
              <button type="button" onClick={() => copy(b.code, `b${i}`)} aria-label="Copy" style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "4px 10px",
    borderRadius: 6,
    fontSize: 12,
    fontWeight: 500,
    cursor: "pointer",
    color: "#e4e4e7",
    background: "rgb(255 255 255 / 0.06)",
    border: "1px solid rgb(255 255 255 / 0.1)"
  }}>
                <CopyIcon done={copied === `b${i}`} />
                {copied === `b${i}` ? "Copied" : "Copy"}
              </button>
            </div>
            <pre style={{
    margin: 0,
    padding: "14px 16px",
    overflowX: "auto",
    fontSize: 12.5,
    lineHeight: 1.65,
    color: "#e4e4e7",
    background: "transparent",
    fontFamily: mono
  }}>
              <code data-testid={`fr-gateway-code-${i}`} data-title={b.title}>{b.code}</code>
            </pre>
          </div>)}
      </div>
    </div>;
};

export const FireRouterBuilder = () => {
  const MODELS = [{
    id: "opus",
    label: "Claude Opus (latest)",
    provider: "anthropic"
  }, {
    id: "claude-opus-5-5",
    label: "Claude Opus 5.5",
    provider: "anthropic"
  }, {
    id: "claude-opus-5",
    label: "Claude Opus 5",
    provider: "anthropic"
  }, {
    id: "fable",
    label: "Claude Fable (latest)",
    provider: "anthropic"
  }, {
    id: "claude-sonnet-5",
    label: "Claude Sonnet 5",
    provider: "anthropic"
  }, {
    id: "sol",
    label: "GPT Sol (latest)",
    provider: "openai"
  }, {
    id: "gpt-5.6-sol",
    label: "GPT 5.6 Sol",
    provider: "openai"
  }, {
    id: "astra",
    label: "GPT Astra (latest)",
    provider: "openai"
  }, {
    id: "luna",
    label: "GPT Luna (latest)",
    provider: "openai"
  }, {
    id: "kimi-k3",
    label: "Kimi K3",
    provider: "fireworks"
  }, {
    id: "kimi-k3-fast",
    label: "Kimi K3 Fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3",
    label: "GLM 5.3",
    provider: "fireworks"
  }, {
    id: "glm-5p3-fast",
    label: "GLM 5.3 Fast",
    provider: "fireworks"
  }, {
    id: "glm-5p3-flash",
    label: "GLM 5.3 Flash",
    provider: "fireworks"
  }, {
    id: "deepseek-v4p1-flash",
    label: "DeepSeek V4.1 Flash",
    provider: "fireworks"
  }];
  const MAX_MEMBERS = 8;
  const BY_ID = Object.fromEntries(MODELS.map(m => [m.id, m]));
  const [primary, setPrimary] = useState("opus");
  const [others, setOthers] = useState(["glm-5p3"]);
  const [api, setApi] = useState("chat");
  const [lang, setLang] = useState("curl");
  const [providerKeys, setProviderKeys] = useState(false);
  const [forcePrimary, setForcePrimary] = useState(false);
  const [copied, setCopied] = useState("");
  const members = [primary, ...others.filter(m => m !== primary)];
  const routerId = `firerouter/${members.join("/")}`;
  const providers = new Set(members.map(m => BY_ID[m].provider));
  const needsAnthropic = providers.has("anthropic");
  const needsOpenAI = providers.has("openai");
  const onlyOneClosed = members.length === 1 && BY_ID[primary].provider !== "fireworks";
  const onlyOneOpen = members.length === 1 && BY_ID[primary].provider === "fireworks";
  const toggleOther = id => {
    if (id === primary) return;
    if (others.includes(id)) {
      setOthers(others.filter(m => m !== id));
    } else if (members.length < MAX_MEMBERS) {
      setOthers([...others, id]);
    }
  };
  const headers = [];
  if (!providerKeys && needsAnthropic) headers.push(["x-anthropic-api-key", "ANTHROPIC_API_KEY"]);
  if (!providerKeys && needsOpenAI) headers.push(["x-openai-api-key", "OPENAI_API_KEY"]);
  const extraHeaders = forcePrimary ? [["x-routing-preference", "1"]] : [];
  const prompt = "Say pong in one word.";
  const path = ({
    chat: "chat/completions",
    responses: "responses",
    messages: "messages"
  })[api];
  const curl = () => {
    const lines = [`curl https://api.fireworks.ai/inference/v1/${path} \\`, `  -H "Authorization: Bearer $FIREWORKS_API_KEY" \\`, ...headers.map(([h, env]) => `  -H "${h}: $${env}" \\`), ...extraHeaders.map(([h, v]) => `  -H "${h}: ${v}" \\`), ...api === "messages" ? [`  -H "anthropic-version: 2023-06-01" \\`] : [], `  -H "Content-Type: application/json" \\`];
    let body;
    if (api === "responses") {
      body = `  -d '{\n    "model": "${routerId}",\n    "input": "${prompt}"\n  }'`;
    } else if (api === "messages") {
      body = `  -d '{\n    "model": "${routerId}",\n    "max_tokens": 1024,\n    "messages": [{"role": "user", "content": "${prompt}"}]\n  }'`;
    } else {
      body = `  -d '{\n    "model": "${routerId}",\n    "messages": [{"role": "user", "content": "${prompt}"}]\n  }'`;
    }
    return [...lines, body].join("\n");
  };
  const python = () => {
    const all = [...headers.map(([h, env]) => [h, `os.environ["${env}"]`]), ...extraHeaders.map(([h, v]) => [h, `"${v}"`])];
    const hdr = all.length ? `    default_headers={\n${all.map(([h, v]) => `        "${h}": ${v},`).join("\n")}\n    },\n` : "";
    if (api === "messages") {
      return `import os

import anthropic

client = anthropic.Anthropic(
    api_key=os.environ["FIREWORKS_API_KEY"],
    base_url="https://api.fireworks.ai/inference",
${hdr})

message = client.messages.create(
    model="${routerId}",
    max_tokens=1024,
    messages=[{"role": "user", "content": "${prompt}"}],
)

text = next(block.text for block in message.content if block.type == "text")
print(message.model, text)`;
    }
    const call = api === "responses" ? `response = client.responses.create(
    model="${routerId}",
    input="${prompt}",
)

print(response.model, response.output_text)` : `response = client.chat.completions.create(
    model="${routerId}",
    messages=[{"role": "user", "content": "${prompt}"}],
)

print(response.model, response.choices[0].message.content)`;
    return `import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["FIREWORKS_API_KEY"],
    base_url="https://api.fireworks.ai/inference/v1",
${hdr})

${call}`;
  };
  const typescript = () => {
    const all = [...headers.map(([h, env]) => [h, `process.env.${env}`]), ...extraHeaders.map(([h, v]) => [h, `"${v}"`])];
    const hdr = all.length ? `  defaultHeaders: {\n${all.map(([h, v]) => `    "${h}": ${v},`).join("\n")}\n  },\n` : "";
    if (api === "messages") {
      return `import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  apiKey: process.env.FIREWORKS_API_KEY,
  baseURL: "https://api.fireworks.ai/inference",
${hdr}});

const message = await client.messages.create({
  model: "${routerId}",
  max_tokens: 1024,
  messages: [{ role: "user", content: "${prompt}" }],
});

const text = message.content.find((block) => block.type === "text");
console.log(message.model, text?.type === "text" ? text.text : "");`;
    }
    const call = api === "responses" ? `const response = await client.responses.create({
  model: "${routerId}",
  input: "${prompt}",
});

console.log(response.model, response.output_text);` : `const response = await client.chat.completions.create({
  model: "${routerId}",
  messages: [{ role: "user", content: "${prompt}" }],
});

console.log(response.model, response.choices[0].message.content);`;
    return `import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.FIREWORKS_API_KEY,
  baseURL: "https://api.fireworks.ai/inference/v1",
${hdr}});

${call}`;
  };
  const code = ({
    curl,
    python,
    typescript
  })[lang]();
  const copy = (text, key) => {
    try {
      navigator.clipboard.writeText(text);
      setCopied(key);
      setTimeout(() => setCopied(""), 1500);
    } catch (e) {}
  };
  const [dark, setDark] = useState(false);
  useEffect(() => {
    const el = document.documentElement;
    const update = () => setDark(el.classList.contains("dark"));
    update();
    const observer = new MutationObserver(update);
    observer.observe(el, {
      attributes: true,
      attributeFilter: ["class"]
    });
    return () => observer.disconnect();
  }, []);
  const C = {
    primary: "rgb(var(--primary))",
    tint: "rgb(var(--primary) / 0.1)",
    bg: dark ? "rgb(var(--background-dark))" : "#ffffff",
    surface: dark ? "rgb(var(--gray-900))" : "rgb(var(--gray-50))",
    raised: dark ? "rgb(var(--gray-800))" : "#ffffff",
    border: dark ? "rgb(var(--gray-800))" : "rgb(var(--gray-200))",
    text: dark ? "rgb(var(--gray-50))" : "rgb(var(--gray-900))",
    muted: dark ? "rgb(var(--gray-400))" : "rgb(var(--gray-500))"
  };
  const PROVIDER = {
    fireworks: {
      label: "Fireworks open models",
      color: "rgb(var(--primary))"
    },
    anthropic: {
      label: "Anthropic",
      color: "#d97757"
    },
    openai: {
      label: "OpenAI",
      color: "#10a37f"
    }
  };
  const mono = "var(--font-mono, ui-monospace, monospace)";
  const StepLabel = ({n, children, htmlFor, aside}) => <label htmlFor={htmlFor} style={{
    display: "flex",
    alignItems: "center",
    gap: 10,
    fontSize: 14,
    fontWeight: 600,
    color: C.text,
    marginBottom: 10
  }}>
      <span style={{
    width: 22,
    height: 22,
    borderRadius: 999,
    background: C.primary,
    color: "#fff",
    fontSize: 12,
    fontWeight: 700,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>{n}</span>
      {children}
      {aside && <span style={{
    fontWeight: 500,
    color: C.muted,
    fontSize: 13
  }}>{aside}</span>}
    </label>;
  const Segmented = ({options, value, onChange}) => <div style={{
    display: "inline-flex",
    padding: 3,
    borderRadius: 10,
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
      {options.map(([k, l]) => <button key={k} type="button" onClick={() => onChange(k)} style={{
    padding: "6px 14px",
    borderRadius: 8,
    fontSize: 13,
    fontWeight: 600,
    border: "none",
    cursor: "pointer",
    background: value === k ? C.primary : "transparent",
    color: value === k ? "#fff" : C.muted,
    boxShadow: value === k ? "0 1px 2px rgb(0 0 0 / 0.15)" : "none",
    transition: "all 150ms"
  }}>
          {l}
        </button>)}
    </div>;
  const Toggle = ({checked, onChange, children}) => <label style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 10,
    fontSize: 13,
    color: C.text,
    cursor: "pointer"
  }}>
      <input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} style={{
    position: "absolute",
    opacity: 0,
    width: 1,
    height: 1
  }} />
      <span style={{
    width: 34,
    height: 20,
    borderRadius: 999,
    background: checked ? C.primary : dark ? "rgb(var(--gray-700))" : "rgb(var(--gray-300))",
    position: "relative",
    transition: "background 150ms",
    flexShrink: 0
  }}>
        <span style={{
    position: "absolute",
    top: 2,
    left: checked ? 16 : 2,
    width: 16,
    height: 16,
    borderRadius: 999,
    background: "#fff",
    boxShadow: "0 1px 2px rgb(0 0 0 / 0.3)",
    transition: "left 150ms"
  }} />
      </span>
      {children}
    </label>;
  const CopyButton = ({text, id, onDark}) => <button type="button" onClick={() => copy(text, id)} aria-label="Copy" style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "4px 10px",
    borderRadius: 6,
    fontSize: 12,
    fontWeight: 500,
    cursor: "pointer",
    flexShrink: 0,
    color: onDark ? "#e4e4e7" : C.text,
    background: onDark ? "rgb(255 255 255 / 0.06)" : C.raised,
    border: `1px solid ${onDark ? "rgb(255 255 255 / 0.1)" : C.border}`
  }}>
      {copied === id ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M5 13l4 4L19 7" stroke="#34d399" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" /></svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="2" /><path d="M5 15V5a2 2 0 012-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" /></svg>}
      {copied === id ? "Copied" : "Copy"}
    </button>;
  const group = provider => <div key={provider} style={{
    display: "flex",
    flexDirection: "column",
    gap: 8
  }}>
      <span style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    fontSize: 12,
    fontWeight: 600,
    color: C.muted
  }}>
        <span style={{
    width: 8,
    height: 8,
    borderRadius: 999,
    background: PROVIDER[provider].color
  }} />
        {PROVIDER[provider].label}
      </span>
      <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6
  }}>
        {MODELS.filter(m => m.provider === provider).map(m => {
    const isPrimary = m.id === primary;
    const active = isPrimary || others.includes(m.id);
    const disabled = !active && members.length >= MAX_MEMBERS;
    const color = PROVIDER[provider].color;
    return <button key={m.id} type="button" disabled={isPrimary || disabled} onClick={() => toggleOther(m.id)} aria-pressed={active} title={isPrimary ? "Primary model" : m.label} style={{
      padding: "5px 11px",
      borderRadius: 999,
      fontSize: 12,
      fontFamily: mono,
      cursor: disabled ? "not-allowed" : isPrimary ? "default" : "pointer",
      transition: "all 150ms",
      opacity: disabled ? 0.4 : 1,
      border: `1px solid ${active ? color : C.border}`,
      background: active ? color : C.raised,
      color: active ? "#fff" : C.text,
      fontWeight: active ? 600 : 400
    }}>
              {m.id}{isPrimary ? " (primary)" : ""}
            </button>;
  })}
      </div>
    </div>;
  return <div data-testid="fr-builder" className="not-prose" style={{
    margin: "16px 0 24px",
    borderRadius: 16,
    border: `1px solid ${C.border}`,
    background: C.bg,
    overflow: "hidden",
    color: C.text,
    boxShadow: dark ? "none" : "0 1px 3px rgb(0 0 0 / 0.04), 0 8px 24px -12px rgb(var(--primary) / 0.18)"
  }}>
      <div style={{
    padding: "18px 22px",
    borderBottom: `1px solid ${C.border}`,
    background: `linear-gradient(135deg, ${C.tint}, transparent 70%)`,
    display: "flex",
    alignItems: "center",
    gap: 12
  }}>
        <span style={{
    width: 34,
    height: 34,
    borderRadius: 10,
    background: C.primary,
    display: "inline-flex",
    alignItems: "center",
    justifyContent: "center",
    flexShrink: 0
  }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M16 3h5v5M4 20L21 3M21 16v5h-5M15 15l6 6M4 4l5 5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>
        </span>
        <div>
          <div style={{
    fontSize: 15,
    fontWeight: 700,
    color: C.text
  }}>Build a FireRouter request</div>
          <div style={{
    fontSize: 13,
    color: C.muted
  }}>Choose models, an API, and a language. Copy a command that runs as is.</div>
        </div>
      </div>

      <div style={{
    padding: 22,
    display: "flex",
    flexDirection: "column",
    gap: 24
  }}>
        <div>
          <StepLabel n="1" htmlFor="fr-primary">Primary model</StepLabel>
          <select id="fr-primary" value={primary} onChange={e => {
    setPrimary(e.target.value);
    setOthers(others.filter(m => m !== e.target.value));
  }} style={{
    width: "100%",
    boxSizing: "border-box",
    padding: "10px 12px",
    borderRadius: 10,
    fontSize: 14,
    color: C.text,
    background: C.raised,
    border: `1.5px solid ${C.border}`,
    outline: "none",
    colorScheme: dark ? "dark" : "light"
  }}>
            {MODELS.map(m => <option key={m.id} value={m.id}>{m.id} · {m.label}</option>)}
          </select>
        </div>

        <div>
          <StepLabel n="2" aside={`${members.length} of ${MAX_MEMBERS}`}>Models to route to</StepLabel>
          <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 14
  }}>
            {["fireworks", "anthropic", "openai"].map(group)}
          </div>
        </div>

        <div style={{
    borderRadius: 12,
    padding: "14px 16px",
    background: C.surface,
    border: `1px solid ${C.border}`
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 12
  }}>
            <code data-testid="fr-id" style={{
    flex: 1,
    minWidth: 0,
    overflowX: "auto",
    whiteSpace: "nowrap",
    fontSize: 16,
    fontFamily: mono,
    background: "transparent",
    padding: 0,
    border: "none"
  }}>
              <span style={{
    color: C.muted
  }}>firerouter</span>
              {members.map(m => <span key={m}><span style={{
    color: C.muted
  }}>/</span><span style={{
    color: PROVIDER[BY_ID[m].provider].color,
    fontWeight: 600
  }}>{m}</span></span>)}
            </code>
            <CopyButton text={routerId} id="id" />
          </div>
          <div style={{
    fontSize: 13,
    color: C.muted,
    marginTop: 8,
    lineHeight: 1.5
  }}>
            {onlyOneClosed ? "One closed model: FireRouter also routes to the Fireworks-managed open-model mix." : onlyOneOpen ? "One open model: every turn uses this model, with no routing." : "FireRouter picks one of these models for each turn. The primary serves turns when routing is unclear."}
          </div>
          <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 6,
    marginTop: 10
  }}>
            {[["Fireworks API key", PROVIDER.fireworks.color, true], ["Anthropic", PROVIDER.anthropic.color, needsAnthropic], ["OpenAI", PROVIDER.openai.color, needsOpenAI]].filter(([, , on]) => on).map(([l, c]) => <span key={l} style={{
    display: "inline-flex",
    alignItems: "center",
    gap: 6,
    padding: "3px 10px",
    borderRadius: 999,
    fontSize: 12,
    fontWeight: 500,
    background: C.raised,
    border: `1px solid ${C.border}`
  }}>
                  <span style={{
    width: 7,
    height: 7,
    borderRadius: 999,
    background: c
  }} />{l}
                </span>)}
          </div>
        </div>

        <div style={{
    display: "flex",
    flexWrap: "wrap",
    gap: 20
  }}>
          <div>
            <StepLabel n="3">API</StepLabel>
            <Segmented options={[["chat", "Chat Completions"], ["responses", "Responses"], ["messages", "Messages"]]} value={api} onChange={setApi} />
          </div>
          <div>
            <StepLabel n="4">Language</StepLabel>
            <Segmented options={[["curl", "cURL"], ["python", "Python"], ["typescript", "TypeScript"]]} value={lang} onChange={setLang} />
          </div>
        </div>

        <div style={{
    display: "flex",
    flexDirection: "column",
    gap: 10
  }}>
          <Toggle checked={providerKeys} onChange={setProviderKeys}>My account has Provider Keys connected (omit provider headers)</Toggle>
          <Toggle checked={forcePrimary} onChange={setForcePrimary}>Force the primary model to test access (x-routing-preference: 1)</Toggle>
        </div>

        <div style={{
    borderRadius: 12,
    overflow: "hidden",
    background: "#0f0d14",
    border: "1px solid #27232e"
  }}>
          <div style={{
    display: "flex",
    alignItems: "center",
    justifyContent: "space-between",
    padding: "8px 12px",
    background: "#17141d",
    borderBottom: "1px solid #27232e"
  }}>
            <div style={{
    display: "flex",
    alignItems: "center",
    gap: 10
  }}>
              <span style={{
    display: "inline-flex",
    gap: 5
  }}>
                {["#ff5f57", "#febc2e", "#28c840"].map(c => <span key={c} style={{
    width: 9,
    height: 9,
    borderRadius: 999,
    background: c,
    opacity: 0.85
  }} />)}
              </span>
              <span style={{
    fontSize: 12,
    color: "#a1a1aa",
    fontFamily: mono
  }}>
                {lang === "curl" ? "cURL" : `${api === "messages" ? "Anthropic" : "OpenAI"} SDK · ${lang === "python" ? "Python" : "TypeScript"}`}
              </span>
            </div>
            <CopyButton text={code} id="code" onDark />
          </div>
          <pre style={{
    margin: 0,
    padding: "14px 16px",
    overflowX: "auto",
    fontSize: 12.5,
    lineHeight: 1.65,
    color: "#e4e4e7",
    background: "transparent",
    fontFamily: mono
  }}>
            <code data-testid="fr-code">{code}</code>
          </pre>
        </div>
      </div>
    </div>;
};

Use the same model ID, such as `auto` or `firerouter/opus`, from a coding harness, an LLM gateway, or your own code. Every path needs a Fireworks API key. Each closed provider in the route also needs a credential. See [Provide Anthropic and OpenAI credentials](/nexus/firerouter#provide-anthropic-and-openai-credentials).

| You are using                                                       | Setup                       |
| ------------------------------------------------------------------- | --------------------------- |
| Claude Code, Codex, OpenCode, Cursor IDE, or another coding harness | [FireConnect](#fireconnect) |
| LiteLLM, Portkey, or another LLM gateway                            | [LLM gateway](#llm-gateway) |
| Your own app, agent, or script                                      | [API and SDK](#api-and-sdk) |

## Choose a model ID

| Model ID                                                | What it routes across                                                                                                                                    | Credentials                                                      |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `auto`                                                  | Fireworks open models only                                                                                                                               | Fireworks API key                                                |
| `firerouter`                                            | Claude Opus or GPT Sol, [picked from your credentials and client](/nexus/firerouter#how-bare-firerouter-picks-its-closed-model), plus the open-model mix | Fireworks key, plus an Anthropic or OpenAI credential            |
| `firerouter/opus`, `firerouter/sol`, `firerouter/astra` | The named closed family, plus the open-model mix                                                                                                         | Fireworks key, plus the matching provider credential             |
| `firerouter/opus/glm-5p3`                               | Only the models you list                                                                                                                                 | Fireworks key, plus a credential for each closed provider listed |

Start with `auto` if you only have a Fireworks key. Without a closed-model credential, any `firerouter` ID serves every turn with open models. See [Build a router ID](/nexus/firerouter#build-a-router-id) and [Supported models](/nexus/firerouter#supported-models).

## FireConnect

Use FireRouter in any coding harness. [FireConnect](/nexus/fireconnect) writes the settings for you and restores them with `off`. Manual setup gives you the same settings to add yourself, with nothing to install.

### Set up your harness

Pick your harness, a model or router ID, and a setup method. Start with `auto` to route across Fireworks open models with only a Fireworks key, or choose **Build your own** to compose a `firerouter` route with Claude, GPT, and open models. The commands and config files update as you choose, including the provider-key headers your route needs.

<FireRouterHarnessSetup />

<Accordion title="FireConnect quick start for Claude Code">
  <Steps>
    <Step title="Install and sign in">
      ```bash wrap theme={null}
      curl -fsSL https://fireconnect.fireworks.ai/install.sh | bash
      fireconnect login
      ```
    </Step>

    <Step title="Connect with a router">
      ```bash wrap theme={null}
      fireconnect claude --model firerouter
      ```
    </Step>

    <Step title="Restart and pick FireRouter">
      Start a new session and open `/model`. Routes with Claude models use your Claude login.
    </Step>

    <Step title="Watch routing live">
      ```bash wrap theme={null}
      fireconnect claude live
      ```

      A meter beside Claude Code shows the model selected for each turn. See [Watch routing live](/nexus/firerouter#watch-routing-live).
    </Step>
  </Steps>
</Accordion>

### Which harnesses can send provider keys

| Harness                                                      | FireConnect with Claude routes                      | FireConnect with GPT routes | Manual setup                                             |
| ------------------------------------------------------------ | --------------------------------------------------- | --------------------------- | -------------------------------------------------------- |
| Claude Code                                                  | Your Claude login, or `--anthropic-api-key`         | OpenAI Provider Key         | Sends `x-anthropic-api-key` and `x-openai-api-key`       |
| Codex CLI, Codex app, ChatGPT desktop, OpenCode, Pi, VS Code | `--anthropic-api-key`, or an Anthropic Provider Key | OpenAI Provider Key         | Sends `x-anthropic-api-key` and `x-openai-api-key`       |
| Copilot CLI, DeepSeek Harness                                | Not supported yet                                   | OpenAI Provider Key         | Sends `x-anthropic-api-key` and `x-openai-api-key`       |
| Cursor IDE, Copilot App                                      | Not supported                                       | OpenAI Provider Key         | Provider Keys only; these apps cannot send extra headers |

FireConnect has no local OpenAI key option yet, so GPT routes such as `firerouter/astra` and `firerouter/sol` need an OpenAI [Provider Key](/nexus/provider-keys) when you connect with FireConnect. To send your own OpenAI key, use manual setup. For every harness's files and restore behavior, see [Coding Harnesses](/nexus/harnesses) and [Harness Compatibility](/nexus/harness-compatibility).

## LLM gateway

You do not need FireConnect behind a gateway. The gateway sends the exact `firerouter/...` model ID and your Fireworks API key to Fireworks. Clients and harnesses behind the gateway call the router ID with only their gateway key.

### Set up your gateway

Choose LiteLLM or Portkey, pick a preset or build your own route, and choose where the provider keys live. The widget writes the gateway config and a test request.

<FireRouterGatewaySetup />

Clients never need provider keys when the gateway stores them or when [Provider Keys](/nexus/provider-keys) is connected. For other gateways and more detail on each credential option, see [LLM Gateways](/nexus/llm-gateways#provide-closed-model-credentials).

## API and SDK

FireRouter works on all three Fireworks inference APIs. Use the SDK you already have and point it at Fireworks.

| API                                  | Endpoint                                                 | SDKs                         |
| ------------------------------------ | -------------------------------------------------------- | ---------------------------- |
| Chat Completions (OpenAI-compatible) | `https://api.fireworks.ai/inference/v1/chat/completions` | OpenAI SDK, Fireworks SDK    |
| Responses (OpenAI-compatible)        | `https://api.fireworks.ai/inference/v1/responses`        | OpenAI SDK                   |
| Messages (Anthropic-compatible)      | `https://api.fireworks.ai/inference/v1/messages`         | Anthropic SDK, Fireworks SDK |

In every SDK, the API key is your **Fireworks** API key. Send closed-provider keys as extra headers:

| Header                | When                                                                         |
| --------------------- | ---------------------------------------------------------------------------- |
| `x-anthropic-api-key` | The route includes a Claude model and no Anthropic Provider Key is connected |
| `x-openai-api-key`    | The route includes a GPT model and no OpenAI Provider Key is connected       |

Leave out a header when the matching [Provider Key](/nexus/provider-keys) is connected. A header, when sent, takes precedence over the stored key for that request. [Amazon Bedrock](/nexus/provider-keys/bedrock) has no request header; it uses Provider Keys only.

Install the SDKs you use:

<CodeGroup>
  ```bash Python theme={null}
  pip install openai anthropic fireworks-ai
  ```

  ```bash TypeScript theme={null}
  npm install openai @anthropic-ai/sdk
  ```
</CodeGroup>

### Build your request

Pick your models, API, and language. The router ID and the command update as you choose, with the provider headers your route needs.

<FireRouterBuilder />

### Examples

The examples below use two router IDs:

* **`firerouter/astra`** in the Chat Completions and Responses examples. FireRouter routes between GPT Astra and the Fireworks open-model mix, so it needs an OpenAI key. `firerouter/sol` works the same way with GPT Sol.
* **`firerouter/opus`** in the Messages examples. FireRouter routes between Claude Opus and the Fireworks open-model mix, so it needs an Anthropic key.

Any router ID works on any API. To use a different route, change `model` and send the header for each closed provider in it. See [Supported models](/nexus/firerouter#supported-models) and [Build a router ID](/nexus/firerouter#build-a-router-id).

#### Chat Completions

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.fireworks.ai/inference/v1/chat/completions \
    -H "Authorization: Bearer $FIREWORKS_API_KEY" \
    -H "x-openai-api-key: $OPENAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "firerouter/astra",
      "messages": [{"role": "user", "content": "Say pong in one word."}]
    }'
  ```

  ```python OpenAI SDK (Python) theme={null}
  import os

  from openai import OpenAI

  client = OpenAI(
      api_key=os.environ["FIREWORKS_API_KEY"],
      base_url="https://api.fireworks.ai/inference/v1",
      # Omit when an OpenAI Provider Key is connected.
      default_headers={"x-openai-api-key": os.environ["OPENAI_API_KEY"]},
  )

  response = client.chat.completions.create(
      model="firerouter/astra",
      messages=[{"role": "user", "content": "Say pong in one word."}],
  )

  print(response.model, response.choices[0].message.content)
  ```

  ```typescript OpenAI SDK (TypeScript) theme={null}
  import OpenAI from "openai";

  const client = new OpenAI({
    apiKey: process.env.FIREWORKS_API_KEY,
    baseURL: "https://api.fireworks.ai/inference/v1",
    // Omit when an OpenAI Provider Key is connected.
    defaultHeaders: { "x-openai-api-key": process.env.OPENAI_API_KEY },
  });

  const response = await client.chat.completions.create({
    model: "firerouter/astra",
    messages: [{ role: "user", content: "Say pong in one word." }],
  });

  console.log(response.model, response.choices[0].message.content);
  ```

  ```python Fireworks SDK (Python) theme={null}
  import os

  from fireworks import Fireworks

  # Reads FIREWORKS_API_KEY from the environment.
  client = Fireworks(
      # Omit when an OpenAI Provider Key is connected.
      default_headers={"x-openai-api-key": os.environ["OPENAI_API_KEY"]},
  )

  response = client.chat.completions.create(
      model="firerouter/astra",
      messages=[{"role": "user", "content": "Say pong in one word."}],
  )

  print(response.model, response.choices[0].message.content)
  ```
</CodeGroup>

#### Responses

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.fireworks.ai/inference/v1/responses \
    -H "Authorization: Bearer $FIREWORKS_API_KEY" \
    -H "x-openai-api-key: $OPENAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "firerouter/astra",
      "input": "Say pong in one word."
    }'
  ```

  ```python OpenAI SDK (Python) theme={null}
  import os

  from openai import OpenAI

  client = OpenAI(
      api_key=os.environ["FIREWORKS_API_KEY"],
      base_url="https://api.fireworks.ai/inference/v1",
      # Omit when an OpenAI Provider Key is connected.
      default_headers={"x-openai-api-key": os.environ["OPENAI_API_KEY"]},
  )

  response = client.responses.create(
      model="firerouter/astra",
      input="Say pong in one word.",
  )

  print(response.model, response.output_text)
  ```

  ```typescript OpenAI SDK (TypeScript) theme={null}
  import OpenAI from "openai";

  const client = new OpenAI({
    apiKey: process.env.FIREWORKS_API_KEY,
    baseURL: "https://api.fireworks.ai/inference/v1",
    // Omit when an OpenAI Provider Key is connected.
    defaultHeaders: { "x-openai-api-key": process.env.OPENAI_API_KEY },
  });

  const response = await client.responses.create({
    model: "firerouter/astra",
    input: "Say pong in one word.",
  });

  console.log(response.model, response.output_text);
  ```
</CodeGroup>

#### Messages

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.fireworks.ai/inference/v1/messages \
    -H "Authorization: Bearer $FIREWORKS_API_KEY" \
    -H "x-anthropic-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "firerouter/opus",
      "max_tokens": 1024,
      "messages": [{"role": "user", "content": "Say pong in one word."}]
    }'
  ```

  ```python Anthropic SDK (Python) theme={null}
  import os

  import anthropic

  client = anthropic.Anthropic(
      api_key=os.environ["FIREWORKS_API_KEY"],
      base_url="https://api.fireworks.ai/inference",
      # Omit when an Anthropic Provider Key is connected.
      default_headers={"x-anthropic-api-key": os.environ["ANTHROPIC_API_KEY"]},
  )

  message = client.messages.create(
      model="firerouter/opus",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Say pong in one word."}],
  )

  text = next(block.text for block in message.content if block.type == "text")
  print(message.model, text)
  ```

  ```typescript Anthropic SDK (TypeScript) theme={null}
  import Anthropic from "@anthropic-ai/sdk";

  const client = new Anthropic({
    apiKey: process.env.FIREWORKS_API_KEY,
    baseURL: "https://api.fireworks.ai/inference",
    // Omit when an Anthropic Provider Key is connected.
    defaultHeaders: { "x-anthropic-api-key": process.env.ANTHROPIC_API_KEY },
  });

  const message = await client.messages.create({
    model: "firerouter/opus",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Say pong in one word." }],
  });

  const text = message.content.find((block) => block.type === "text");
  console.log(message.model, text?.type === "text" ? text.text : "");
  ```

  ```python Fireworks SDK (Python) theme={null}
  import os

  from fireworks import Fireworks

  # Reads FIREWORKS_API_KEY from the environment.
  client = Fireworks(
      # Omit when an Anthropic Provider Key is connected.
      default_headers={"x-anthropic-api-key": os.environ["ANTHROPIC_API_KEY"]},
  )

  message = client.messages.create(
      model="firerouter/opus",
      max_tokens=1024,
      messages=[{"role": "user", "content": "Say pong in one word."}],
  )

  text = next(block.text for block in message.content if block.type == "text")
  print(message.model, text)
  ```
</CodeGroup>

The Anthropic SDK base URL is `https://api.fireworks.ai/inference`, without `/v1`. The SDK adds `/v1/messages`. Some models return a `thinking` block before the text, so read the first `text` block rather than `content[0]`.

### Check which model served the request

Each example prints the response `model` field. It names the model that served the turn, such as `glm-5p3-flash`, `claude-opus-5-5`, or `gpt-6-astra`, not the router ID you sent. To confirm closed-model access, add `x-routing-preference: 1` so the route's primary model serves the turn. See [Verify routing](/nexus/firerouter#verify-routing).

For errors, limits, and more API details, see [APIs and SDKs](/nexus/apis-and-sdks).
