[object Object]

← back to Exo

Integrations helpers (#1810)

39c39e819919d73118cd5df7911a0c0f8e0bc500 · 2026-03-30 14:28:41 +0100 · rltakashige

## Motivation

<!-- Why is this change needed? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here -->

## Changes

<!-- Describe what you changed in detail -->

## Why It Works

<!-- Explain why your approach solves the problem -->

## Test Plan

### Manual Testing
<!-- Hardware: (e.g., MacBook Pro M1 Max 32GB, Mac Mini M2 16GB,
connected via Thunderbolt 4) -->
<!-- What you did: -->
<!-- - -->

### Automated Testing
<!-- Describe changes to automated tests, or how existing tests cover
this change -->
<!-- - -->

Files touched

Diff

commit 39c39e819919d73118cd5df7911a0c0f8e0bc500
Author: rltakashige <rl.takashige@gmail.com>
Date:   Mon Mar 30 14:28:41 2026 +0100

    Integrations helpers (#1810)
    
    ## Motivation
    
    <!-- Why is this change needed? What problem does it solve? -->
    <!-- If it fixes an open issue, please link to the issue here -->
    
    ## Changes
    
    <!-- Describe what you changed in detail -->
    
    ## Why It Works
    
    <!-- Explain why your approach solves the problem -->
    
    ## Test Plan
    
    ### Manual Testing
    <!-- Hardware: (e.g., MacBook Pro M1 Max 32GB, Mac Mini M2 16GB,
    connected via Thunderbolt 4) -->
    <!-- What you did: -->
    <!-- - -->
    
    ### Automated Testing
    <!-- Describe changes to automated tests, or how existing tests cover
    this change -->
    <!-- - -->
---
 dashboard/src/lib/components/HeaderNav.svelte      |  68 ++-
 .../src/lib/components/IntegrationCard.svelte      |  52 ++
 dashboard/src/lib/components/index.ts              |   1 +
 dashboard/src/routes/integrations/+page.svelte     | 577 +++++++++++++++++++++
 .../mlx-community--DeepSeek-V3.1-4bit.toml         |   2 +
 .../mlx-community--DeepSeek-V3.1-8bit.toml         |   2 +
 .../mlx-community--DeepSeek-V3.2-4bit.toml         |   2 +
 .../mlx-community--DeepSeek-V3.2-8bit.toml         |   2 +
 .../mlx-community--GLM-4.5-Air-8bit.toml           |   2 +
 .../mlx-community--GLM-4.5-Air-bf16.toml           |   2 +
 .../mlx-community--GLM-4.7-4bit.toml               |   2 +
 .../mlx-community--GLM-4.7-6bit.toml               |   2 +
 .../mlx-community--GLM-4.7-8bit-gs32.toml          |   2 +
 .../mlx-community--GLM-4.7-Flash-4bit.toml         |   2 +
 .../mlx-community--GLM-4.7-Flash-5bit.toml         |   2 +
 .../mlx-community--GLM-4.7-Flash-6bit.toml         |   2 +
 .../mlx-community--GLM-4.7-Flash-8bit.toml         |   2 +
 .../mlx-community--GLM-5-8bit.toml                 |   2 +
 .../mlx-community--GLM-5-MXFP4-Q8.toml             |   2 +
 .../mlx-community--GLM-5-bf16.toml                 |   2 +
 .../mlx-community--Kimi-K2-Instruct-4bit.toml      |   2 +
 .../mlx-community--Kimi-K2-Thinking.toml           |   2 +
 .../mlx-community--Kimi-K2.5.toml                  |   2 +
 ...y--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.toml |   2 +
 ...y--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.toml |   2 +
 ...y--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.toml |   2 +
 ...nity--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.toml |   2 +
 ...nity--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.toml |   2 +
 ...nity--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.toml |   2 +
 .../mlx-community--Llama-3.2-1B-Instruct-4bit.toml |   2 +
 .../mlx-community--Llama-3.2-3B-Instruct-4bit.toml |   2 +
 .../mlx-community--Llama-3.2-3B-Instruct-8bit.toml |   2 +
 ...mlx-community--Llama-3.3-70B-Instruct-4bit.toml |   2 +
 ...mlx-community--Llama-3.3-70B-Instruct-8bit.toml |   2 +
 ...ommunity--Meta-Llama-3.1-70B-Instruct-4bit.toml |   2 +
 ...community--Meta-Llama-3.1-8B-Instruct-4bit.toml |   2 +
 ...community--Meta-Llama-3.1-8B-Instruct-8bit.toml |   2 +
 ...community--Meta-Llama-3.1-8B-Instruct-bf16.toml |   2 +
 .../mlx-community--MiniMax-M2.1-3bit.toml          |   2 +
 .../mlx-community--MiniMax-M2.1-8bit.toml          |   2 +
 .../mlx-community--MiniMax-M2.5-4bit.toml          |   2 +
 .../mlx-community--MiniMax-M2.5-6bit.toml          |   2 +
 .../mlx-community--MiniMax-M2.5-8bit.toml          |   2 +
 ...y--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.toml |   2 +
 ...y--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.toml |   2 +
 ...y--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.toml |   2 +
 ...y--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.toml |   2 +
 ...y--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.toml |   2 +
 ...--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.toml |   2 +
 ...nity--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.toml |   2 +
 ...ommunity--NVIDIA-Nemotron-Nano-9B-v2-4bits.toml |   2 +
 ...community--NVIDIA-Nemotron-Nano-9B-v2-6bit.toml |   2 +
 .../mlx-community--Qwen3-0.6B-4bit.toml            |   2 +
 .../mlx-community--Qwen3-0.6B-8bit.toml            |   2 +
 ...munity--Qwen3-235B-A22B-Instruct-2507-4bit.toml |   2 +
 ...munity--Qwen3-235B-A22B-Instruct-2507-8bit.toml |   2 +
 .../mlx-community--Qwen3-30B-A3B-4bit.toml         |   2 +
 .../mlx-community--Qwen3-30B-A3B-8bit.toml         |   2 +
 ...unity--Qwen3-Coder-480B-A35B-Instruct-4bit.toml |   2 +
 ...unity--Qwen3-Coder-480B-A35B-Instruct-8bit.toml |   2 +
 .../mlx-community--Qwen3-Coder-Next-4bit.toml      |   2 +
 .../mlx-community--Qwen3-Coder-Next-5bit.toml      |   2 +
 .../mlx-community--Qwen3-Coder-Next-6bit.toml      |   2 +
 .../mlx-community--Qwen3-Coder-Next-8bit.toml      |   2 +
 .../mlx-community--Qwen3-Coder-Next-bf16.toml      |   2 +
 ...ommunity--Qwen3-Next-80B-A3B-Instruct-4bit.toml |   2 +
 ...ommunity--Qwen3-Next-80B-A3B-Instruct-8bit.toml |   2 +
 ...ommunity--Qwen3-Next-80B-A3B-Thinking-4bit.toml |   2 +
 ...ommunity--Qwen3-Next-80B-A3B-Thinking-8bit.toml |   2 +
 .../mlx-community--Qwen3-VL-4B-Instruct-4bit.toml  |   2 +
 .../mlx-community--Qwen3.5-122B-A10B-4bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-122B-A10B-6bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-122B-A10B-8bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-122B-A10B-bf16.toml     |   4 +-
 .../mlx-community--Qwen3.5-27B-4bit.toml           |   4 +-
 .../mlx-community--Qwen3.5-27B-8bit.toml           |   4 +-
 .../mlx-community--Qwen3.5-2B-MLX-8bit.toml        |   4 +-
 .../mlx-community--Qwen3.5-35B-A3B-4bit.toml       |   4 +-
 .../mlx-community--Qwen3.5-35B-A3B-8bit.toml       |   4 +-
 .../mlx-community--Qwen3.5-397B-A17B-4bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-397B-A17B-6bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-397B-A17B-8bit.toml     |   4 +-
 .../mlx-community--Qwen3.5-9B-4bit.toml            |   4 +-
 .../mlx-community--Qwen3.5-9B-8bit.toml            |   4 +-
 .../mlx-community--Step-3.5-Flash-4bit.toml        |   2 +
 .../mlx-community--Step-3.5-Flash-6bit.toml        |   2 +
 .../mlx-community--Step-3.5-Flash-8Bit.toml        |   2 +
 .../mlx-community--gpt-oss-120b-MXFP4-Q8.toml      |   2 +
 .../mlx-community--gpt-oss-20b-MXFP4-Q8.toml       |   2 +
 ...mlx-community--llama-3.3-70b-instruct-fp16.toml |   2 +
 src/exo/api/adapters/responses.py                  | 278 ++++++++--
 src/exo/api/main.py                                |   1 +
 src/exo/api/types/openai_responses.py              | 222 +++++++-
 src/exo/shared/models/model_cards.py               |   4 +
 .../worker/engines/mlx/generator/batch_generate.py |   2 +
 src/exo/worker/engines/mlx/generator/generate.py   |   2 +
 src/exo/worker/engines/mlx/utils_mlx.py            |  74 ++-
 src/exo/worker/engines/mlx/vision.py               |  21 +-
 98 files changed, 1394 insertions(+), 108 deletions(-)

diff --git a/dashboard/src/lib/components/HeaderNav.svelte b/dashboard/src/lib/components/HeaderNav.svelte
index 16582a64..d7dab78d 100644
--- a/dashboard/src/lib/components/HeaderNav.svelte
+++ b/dashboard/src/lib/components/HeaderNav.svelte
@@ -1,21 +1,38 @@
 <script lang="ts">
   import { browser } from "$app/environment";
 
-  export let showHome = true;
-  export let onHome: (() => void) | null = null;
-  export let showSidebarToggle = false;
-  export let sidebarVisible = true;
-  export let onToggleSidebar: (() => void) | null = null;
-  export let showMobileMenuToggle = false;
-  export let mobileMenuOpen = false;
-  export let onToggleMobileMenu: (() => void) | null = null;
-  export let showMobileRightToggle = false;
-  export let mobileRightOpen = false;
-  export let onToggleMobileRight: (() => void) | null = null;
-  export let downloadProgress: {
-    count: number;
-    percentage: number;
-  } | null = null;
+  interface Props {
+    showHome?: boolean;
+    onHome?: (() => void) | null;
+    showSidebarToggle?: boolean;
+    sidebarVisible?: boolean;
+    onToggleSidebar?: (() => void) | null;
+    showMobileMenuToggle?: boolean;
+    mobileMenuOpen?: boolean;
+    onToggleMobileMenu?: (() => void) | null;
+    showMobileRightToggle?: boolean;
+    mobileRightOpen?: boolean;
+    onToggleMobileRight?: (() => void) | null;
+    downloadProgress?: {
+      count: number;
+      percentage: number;
+    } | null;
+  }
+
+  let {
+    showHome = true,
+    onHome = null,
+    showSidebarToggle = false,
+    sidebarVisible = true,
+    onToggleSidebar = null,
+    showMobileMenuToggle = false,
+    mobileMenuOpen = false,
+    onToggleMobileMenu = null,
+    showMobileRightToggle = false,
+    mobileRightOpen = false,
+    onToggleMobileRight = null,
+    downloadProgress = null,
+  }: Props = $props();
 
   function handleHome(): void {
     if (onHome) {
@@ -259,5 +276,26 @@
       {/if}
       <span class="hidden sm:inline">Downloads</span>
     </a>
+    <a
+      href="/#/integrations"
+      class="text-xs md:text-sm text-white/70 hover:text-exo-yellow transition-colors tracking-wider uppercase flex items-center gap-1.5 md:gap-2 cursor-pointer"
+      title="Integration configs for external tools"
+    >
+      <svg
+        class="w-4 h-4"
+        viewBox="0 0 24 24"
+        fill="none"
+        stroke="currentColor"
+        stroke-width="2"
+        stroke-linecap="round"
+        stroke-linejoin="round"
+      >
+        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
+        <path
+          d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
+        />
+      </svg>
+      <span class="hidden sm:inline">Integrations</span>
+    </a>
   </nav>
 </header>
diff --git a/dashboard/src/lib/components/IntegrationCard.svelte b/dashboard/src/lib/components/IntegrationCard.svelte
new file mode 100644
index 00000000..83978b4d
--- /dev/null
+++ b/dashboard/src/lib/components/IntegrationCard.svelte
@@ -0,0 +1,52 @@
+<script lang="ts">
+  interface Props {
+    title: string;
+    subtitle: string;
+    config: string;
+    description?: string;
+    language?: "json" | "bash";
+  }
+
+  let {
+    title,
+    subtitle,
+    config,
+    description = "",
+    language = "json",
+  }: Props = $props();
+
+  let copied = $state(false);
+
+  async function copyToClipboard() {
+    await navigator.clipboard.writeText(config);
+    copied = true;
+    setTimeout(() => (copied = false), 2000);
+  }
+</script>
+
+<div
+  class="border border-exo-light-gray/20 rounded-lg bg-exo-medium-gray/20 overflow-hidden"
+>
+  <div class="flex items-center justify-between px-5 py-4">
+    <div>
+      <h3 class="text-white text-sm font-semibold tracking-wide">{title}</h3>
+      <p class="text-exo-light-gray/60 text-xs mt-0.5 font-mono">{subtitle}</p>
+    </div>
+    <button
+      onclick={copyToClipboard}
+      class="px-3 py-1.5 text-xs rounded border transition-all duration-200 cursor-pointer
+        {copied
+        ? 'border-green-500/50 text-green-400 bg-green-500/10'
+        : 'border-exo-light-gray/30 text-exo-light-gray hover:border-exo-yellow/50 hover:text-exo-yellow'}"
+    >
+      {copied ? "Copied!" : "Copy"}
+    </button>
+  </div>
+  {#if description}
+    <p class="text-exo-light-gray/70 text-xs px-5 pb-3">{description}</p>
+  {/if}
+  <div class="bg-black/30 border-t border-exo-light-gray/10">
+    <pre
+      class="text-xs text-exo-light-gray/90 font-mono p-4 overflow-x-auto whitespace-pre">{config}</pre>
+  </div>
+</div>
diff --git a/dashboard/src/lib/components/index.ts b/dashboard/src/lib/components/index.ts
index 7efe0f87..07192376 100644
--- a/dashboard/src/lib/components/index.ts
+++ b/dashboard/src/lib/components/index.ts
@@ -13,3 +13,4 @@ export { default as ModelFilterPopover } from "./ModelFilterPopover.svelte";
 export { default as ModelPickerGroup } from "./ModelPickerGroup.svelte";
 export { default as ModelPickerModal } from "./ModelPickerModal.svelte";
 export { default as ChatModelSelector } from "./ChatModelSelector.svelte";
+export { default as IntegrationCard } from "./IntegrationCard.svelte";
diff --git a/dashboard/src/routes/integrations/+page.svelte b/dashboard/src/routes/integrations/+page.svelte
new file mode 100644
index 00000000..924cfdf7
--- /dev/null
+++ b/dashboard/src/routes/integrations/+page.svelte
@@ -0,0 +1,577 @@
+<script lang="ts">
+  import { browser } from "$app/environment";
+  import { fade } from "svelte/transition";
+  import HeaderNav from "$lib/components/HeaderNav.svelte";
+  import IntegrationCard from "$lib/components/IntegrationCard.svelte";
+  import { instances, refreshState } from "$lib/stores/app.svelte";
+  import { onMount } from "svelte";
+
+  const apiUrl = browser
+    ? window.location.origin.replace("localhost", "127.0.0.1")
+    : "http://127.0.0.1:52415";
+
+  const instancesData = $derived(instances());
+
+  let modelCapabilities = $state<Record<string, string[]>>({});
+  let modelContextLengths = $state<Record<string, number>>({});
+
+  const runningModels = $derived.by(() => {
+    const models: string[] = [];
+    for (const [, wrapper] of Object.entries(instancesData)) {
+      if (wrapper && typeof wrapper === "object") {
+        const values = Object.values(wrapper as Record<string, unknown>);
+        if (values.length > 0) {
+          const instance = values[0];
+          if (instance && typeof instance === "object") {
+            const inst = instance as {
+              shardAssignments?: { modelId?: string };
+            };
+            const modelId = inst.shardAssignments?.modelId;
+            if (modelId && !models.includes(modelId)) {
+              models.push(modelId);
+            }
+          }
+        }
+      }
+    }
+    return models;
+  });
+
+  function estimateParamSize(modelId: string): number {
+    const match = modelId.match(/(\d+(?:\.\d+)?)[Bb]/);
+    return match ? parseFloat(match[1]) : 0;
+  }
+
+  const modelsBySize = $derived(
+    [...runningModels].sort(
+      (a, b) => estimateParamSize(b) - estimateParamSize(a),
+    ),
+  );
+
+  const defaultTiers = $derived.by(() => {
+    const n = modelsBySize.length;
+    if (n === 0)
+      return {
+        opus: "your-model-id",
+        sonnet: "your-model-id",
+        haiku: "your-model-id",
+      };
+    if (n === 1)
+      return {
+        opus: modelsBySize[0],
+        sonnet: modelsBySize[0],
+        haiku: modelsBySize[0],
+      };
+    if (n === 2)
+      return {
+        opus: modelsBySize[0],
+        sonnet: modelsBySize[1],
+        haiku: modelsBySize[1],
+      };
+    return {
+      opus: modelsBySize[0],
+      sonnet: modelsBySize[Math.floor(n / 2)],
+      haiku: modelsBySize[n - 1],
+    };
+  });
+
+  let opusModel = $state("");
+  let sonnetModel = $state("");
+  let haikuModel = $state("");
+
+  $effect(() => {
+    opusModel = defaultTiers.opus;
+    sonnetModel = defaultTiers.sonnet;
+    haikuModel = defaultTiers.haiku;
+  });
+
+  let codexModel = $state("");
+  let codexMcpPath = $state("/Users/username");
+  let openClawModel = $state("");
+  $effect(() => {
+    const def = modelsBySize.length > 0 ? modelsBySize[0] : "your-model-id";
+    codexModel = def;
+    openClawModel = def;
+  });
+
+  const claudeShellCommand = $derived(
+    [
+      `ANTHROPIC_BASE_URL=${apiUrl} \\`,
+      `ANTHROPIC_API_KEY=x \\`,
+      `ANTHROPIC_DEFAULT_OPUS_MODEL=${opusModel} \\`,
+      `ANTHROPIC_DEFAULT_SONNET_MODEL=${sonnetModel} \\`,
+      `ANTHROPIC_DEFAULT_HAIKU_MODEL=${haikuModel} \\`,
+      `API_TIMEOUT_MS=3000000 \\`,
+      `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \\`,
+      `claude`,
+    ].join("\n"),
+  );
+
+  const claudeSettingsJson = $derived(
+    JSON.stringify(
+      {
+        env: {
+          ANTHROPIC_BASE_URL: apiUrl,
+          ANTHROPIC_API_KEY: "x",
+          ANTHROPIC_DEFAULT_OPUS_MODEL: opusModel,
+          ANTHROPIC_DEFAULT_SONNET_MODEL: sonnetModel,
+          ANTHROPIC_DEFAULT_HAIKU_MODEL: haikuModel,
+          API_TIMEOUT_MS: "3000000",
+          CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
+        },
+      },
+      null,
+      2,
+    ),
+  );
+
+  const openCodeConfig = $derived.by(() => {
+    const models: Record<string, Record<string, unknown>> = {};
+    for (const modelId of runningModels) {
+      const caps = modelCapabilities[modelId] || [];
+      const ctxLen = modelContextLengths[modelId] || 0;
+      const entry: Record<string, unknown> = { name: modelId };
+      if (ctxLen > 0) {
+        entry.limit = { context: ctxLen, output: Math.min(ctxLen, 16384) };
+      }
+      if (caps.includes("vision")) {
+        entry.modalities = { input: ["text", "image"], output: ["text"] };
+      }
+      models[modelId] = entry;
+    }
+    if (Object.keys(models).length === 0) {
+      models["your-model-id"] = { name: "your-model-name" };
+    }
+    const firstModel =
+      runningModels.length > 0 ? runningModels[0] : "your-model-id";
+    return JSON.stringify(
+      {
+        $schema: "https://opencode.ai/config.json",
+        provider: {
+          exo: {
+            npm: "@ai-sdk/openai-compatible",
+            name: "exo",
+            options: {
+              baseURL: `${apiUrl}/v1`,
+              apiKey: "x",
+            },
+            models,
+          },
+        },
+        model: `exo/${firstModel}`,
+      },
+      null,
+      2,
+    );
+  });
+
+  const codexShellCommand = $derived(`EXO_API_KEY=x npx @openai/codex`);
+
+  const codexConfig = $derived(
+    [
+      `model = "${codexModel}"`,
+      `model_provider = "exo"`,
+      ``,
+      `[model_providers.exo]`,
+      `name = "exo"`,
+      `base_url = "${apiUrl}/v1"`,
+      `env_key = "EXO_API_KEY"`,
+      ``,
+      `[mcp_servers.filesystem]`,
+      `command = "npx"`,
+      `args = ["-y", "@modelcontextprotocol/server-filesystem", "${codexMcpPath}"]`,
+    ].join("\n"),
+  );
+
+  const openClawConfig = $derived(
+    JSON.stringify(
+      {
+        gateway: { mode: "local" },
+        models: {
+          providers: {
+            exo: {
+              baseUrl: `${apiUrl}/v1`,
+              apiKey: "x",
+              api: "openai-completions",
+              models: [
+                {
+                  id: openClawModel,
+                  name: "exo local",
+                  input: (modelCapabilities[openClawModel] || []).includes(
+                    "vision",
+                  )
+                    ? ["text", "image"]
+                    : ["text"],
+                },
+              ],
+            },
+          },
+        },
+        agents: {
+          defaults: {
+            model: `exo/${openClawModel}`,
+          },
+        },
+      },
+      null,
+      2,
+    ),
+  );
+
+  const ollamaCommand = $derived(
+    `OLLAMA_HOST=${apiUrl}/ollama ollama run ${modelsBySize.length > 0 ? modelsBySize[0] : "your-model-id"}`,
+  );
+
+  const openWebUiCommand = $derived(
+    [
+      `docker run -d -p 3000:8080 \\`,
+      `  -e OLLAMA_BASE_URL=${apiUrl.replace("localhost", "host.docker.internal")}/ollama \\`,
+      `  -v open-webui:/app/backend/data \\`,
+      `  --name open-webui \\`,
+      `  ghcr.io/open-webui/open-webui:main`,
+    ].join("\n"),
+  );
+
+  const n8nDockerCommand = $derived(
+    [
+      `docker run -d -p 5678:5678 \\`,
+      `  -v n8n_data:/home/node/.n8n \\`,
+      `  --name n8n \\`,
+      `  docker.n8n.io/n8nio/n8n`,
+    ].join("\n"),
+  );
+
+  const n8nCredentialSteps = $derived(
+    [
+      `1. Go to Credentials → Add Credential → search "OpenAI API"`,
+      `2. Set API Key to: x`,
+      `3. Set Base URL to: ${apiUrl.replace("127.0.0.1", "host.docker.internal").replace("localhost", "host.docker.internal")}/v1`,
+      `4. Save the credential`,
+    ].join("\n"),
+  );
+
+  const n8nWorkflowSteps = $derived(
+    [
+      `1. Create a new workflow → "Start from Scratch"`,
+      `2. Add an "AI Agent" or "Basic LLM Chain" node`,
+      `3. Inside it, add an "OpenAI Chat Model" sub-node`,
+      `4. Select the OpenAI credential you just created`,
+      `5. Set Model to "From list" and pick your model (e.g. ${modelsBySize.length > 0 ? modelsBySize[0] : "your-model-id"})`,
+      `6. Optionally toggle "Use Responses API", add Built-in Tools, or click "Add Option" for sampling settings`,
+      `7. Connect a "Chat Trigger" node for interactive chat`,
+      `8. On the Chat Trigger, enable "Allow File Uploads" for vision`,
+    ].join("\n"),
+  );
+
+  const firefoxConfig = $derived(
+    [
+      `1. Open about:config in Firefox`,
+      `2. Set browser.ml.chat.enabled to true`,
+      `3. Set browser.ml.chat.hideLocalhost to false`,
+      `4. Set browser.ml.chat.provider to: ${apiUrl}/`,
+    ].join("\n"),
+  );
+
+  const tabs = [
+    "Claude Code",
+    "OpenCode",
+    "Codex",
+    "OpenClaw",
+    "Open WebUI",
+    "n8n",
+    "Firefox",
+  ] as const;
+  type Tab = (typeof tabs)[number];
+  const stored = browser ? localStorage.getItem("exo-integrations-tab") : null;
+  let activeTab = $state<Tab>(
+    stored && tabs.includes(stored as Tab) ? (stored as Tab) : "Claude Code",
+  );
+  $effect(() => {
+    if (browser) localStorage.setItem("exo-integrations-tab", activeTab);
+  });
+
+  const selectClass =
+    "bg-black/30 border border-exo-light-gray/20 rounded px-2 py-1.5 text-white font-mono text-xs focus:border-exo-yellow/50 focus:outline-none appearance-none cursor-pointer";
+
+  onMount(async () => {
+    refreshState();
+    try {
+      const resp = await fetch("/v1/models");
+      const data = (await resp.json()) as {
+        data: { id: string; capabilities: string[]; context_length: number }[];
+      };
+      const caps: Record<string, string[]> = {};
+      const ctxs: Record<string, number> = {};
+      for (const model of data.data) {
+        caps[model.id] = model.capabilities || [];
+        if (model.context_length > 0) ctxs[model.id] = model.context_length;
+      }
+      modelCapabilities = caps;
+      modelContextLengths = ctxs;
+    } catch {
+      /* ignore */
+    }
+  });
+</script>
+
+<div class="min-h-screen bg-exo-dark-gray flex flex-col">
+  <HeaderNav showHome={true} />
+
+  <main
+    class="flex-1 max-w-3xl mx-auto w-full px-4 md:px-6 py-8"
+    in:fade={{ duration: 200 }}
+  >
+    <div class="mb-8">
+      <h1
+        class="text-white text-xl md:text-2xl font-semibold tracking-wide mb-2"
+      >
+        Integrations
+      </h1>
+      <p class="text-exo-light-gray/60 text-sm">
+        Connect external tools to your exo cluster.
+      </p>
+    </div>
+
+    <!-- Status -->
+    <div class="mb-8">
+      <span class="text-exo-light-gray/70 text-xs uppercase tracking-wider"
+        >API Endpoint</span
+      >
+      <span class="text-white font-mono text-sm ml-2">{apiUrl}</span>
+      {#if runningModels.length > 0}
+        <div class="text-exo-light-gray/50 text-xs mt-2">
+          Running model{runningModels.length > 1 ? "s" : ""}:
+          <ul class="mt-1 space-y-0.5 list-none">
+            {#each runningModels as model}
+              <li class="text-exo-yellow font-mono">{model}</li>
+            {/each}
+          </ul>
+        </div>
+      {:else}
+        <p class="text-exo-light-gray/40 text-xs mt-2 italic">
+          No models currently running
+        </p>
+      {/if}
+    </div>
+
+    <!-- API Endpoints -->
+    <div class="mb-8">
+      <div
+        class="flex flex-col sm:flex-row gap-3 text-xs font-mono text-exo-light-gray/70"
+      >
+        <div
+          class="flex-1 bg-black/20 border border-exo-light-gray/10 rounded px-3 py-2"
+        >
+          <span class="text-exo-light-gray/40 text-[10px] uppercase block mb-1"
+            >OpenAI-compatible</span
+          >
+          <span class="text-white/80">{apiUrl}/v1</span>
+        </div>
+        <div
+          class="flex-1 bg-black/20 border border-exo-light-gray/10 rounded px-3 py-2"
+        >
+          <span class="text-exo-light-gray/40 text-[10px] uppercase block mb-1"
+            >Claude-compatible</span
+          >
+          <span class="text-white/80">{apiUrl}</span>
+        </div>
+        <div
+          class="flex-1 bg-black/20 border border-exo-light-gray/10 rounded px-3 py-2"
+        >
+          <span class="text-exo-light-gray/40 text-[10px] uppercase block mb-1"
+            >Ollama-compatible</span
+          >
+          <span class="text-white/80">{apiUrl}/ollama</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- Tabs -->
+    <div
+      class="flex flex-wrap gap-2 mb-6 border-b border-exo-light-gray/10 pb-3"
+    >
+      {#each tabs as tab}
+        <button
+          onclick={() => (activeTab = tab)}
+          class="px-3 py-1.5 text-xs rounded-md transition-all cursor-pointer
+            {activeTab === tab
+            ? 'bg-exo-yellow/15 text-exo-yellow border border-exo-yellow/30'
+            : 'text-exo-light-gray/60 hover:text-white/80 border border-transparent hover:border-exo-light-gray/20'}"
+        >
+          {tab}
+        </button>
+      {/each}
+    </div>
+
+    <!-- Tab Content -->
+    <div class="space-y-4">
+      {#if activeTab === "Claude Code"}
+        {#if runningModels.length > 1}
+          <div class="grid grid-cols-3 gap-3 text-xs">
+            {#each [{ label: "Opus", bind: () => opusModel, set: (v: string) => (opusModel = v) }, { label: "Sonnet", bind: () => sonnetModel, set: (v: string) => (sonnetModel = v) }, { label: "Haiku", bind: () => haikuModel, set: (v: string) => (haikuModel = v) }] as tier}
+              <div>
+                <span
+                  class="text-exo-light-gray/50 text-[10px] uppercase tracking-wider block mb-1"
+                  >{tier.label}</span
+                >
+                <select
+                  value={tier.bind()}
+                  onchange={(e) =>
+                    tier.set((e.target as HTMLSelectElement).value)}
+                  class="w-full {selectClass}"
+                >
+                  {#each runningModels as model}
+                    <option value={model}>{model.split("/").pop()}</option>
+                  {/each}
+                </select>
+              </div>
+            {/each}
+          </div>
+        {/if}
+        <IntegrationCard
+          title="Shell Command"
+          subtitle="Run in terminal"
+          description="Launch Claude Code with exo as the backend. Paste this into your terminal."
+          config={claudeShellCommand}
+          language="bash"
+        />
+        <IntegrationCard
+          title="Settings File"
+          subtitle="~/.claude/settings.json"
+          description="Or add this to your Claude Code settings for persistent configuration."
+          config={claudeSettingsJson}
+        />
+      {:else if activeTab === "OpenCode"}
+        <IntegrationCard
+          title="Config File"
+          subtitle="opencode.json"
+          description="Add this to your project root or ~/.config/opencode/opencode.json for global config. Vision models automatically get image input modality."
+          config={openCodeConfig}
+        />
+      {:else if activeTab === "Codex"}
+        <div class="flex gap-3 text-xs">
+          {#if runningModels.length > 1}
+            <div>
+              <span
+                class="text-exo-light-gray/50 text-[10px] uppercase tracking-wider block mb-1"
+                >Model</span
+              >
+              <select bind:value={codexModel} class={selectClass}>
+                {#each runningModels as model}
+                  <option value={model}>{model.split("/").pop()}</option>
+                {/each}
+              </select>
+            </div>
+          {/if}
+          <div class="flex-1">
+            <span
+              class="text-exo-light-gray/50 text-[10px] uppercase tracking-wider block mb-1"
+              >MCP Filesystem Path</span
+            >
+            <input
+              type="text"
+              bind:value={codexMcpPath}
+              class="w-full bg-black/30 border border-exo-light-gray/20 rounded px-2 py-1.5 text-white font-mono text-xs focus:border-exo-yellow/50 focus:outline-none"
+            />
+          </div>
+        </div>
+        <IntegrationCard
+          title="Config File"
+          subtitle="~/.codex/config.toml"
+          description="Add this to your Codex CLI config so the model and provider persist."
+          config={codexConfig}
+        />
+        <IntegrationCard
+          title="Shell Command"
+          subtitle="Run in terminal"
+          description="Launch Codex with exo as the backend."
+          config={codexShellCommand}
+          language="bash"
+        />
+      {:else if activeTab === "OpenClaw"}
+        {#if runningModels.length > 1}
+          <div class="text-xs">
+            <span
+              class="text-exo-light-gray/50 text-[10px] uppercase tracking-wider block mb-1"
+              >Model</span
+            >
+            <select bind:value={openClawModel} class={selectClass}>
+              {#each runningModels as model}
+                <option value={model}>{model.split("/").pop()}</option>
+              {/each}
+            </select>
+          </div>
+        {/if}
+        <IntegrationCard
+          title="Config File"
+          subtitle="~/.openclaw/openclaw.json"
+          description="Add this to your OpenClaw config. If you haven't installed OpenClaw yet, run: npm install -g openclaw@latest"
+          config={openClawConfig}
+        />
+        <IntegrationCard
+          title="Setup Commands"
+          subtitle="Run in terminal"
+          description="After saving the config, run these commands to fix metadata and start the gateway."
+          config={`openclaw doctor --fix${(modelCapabilities[openClawModel] || []).includes("vision") ? `\nopenclaw models set-image exo/${openClawModel}` : ""}\nopenclaw gateway &\nopenclaw dashboard`}
+          language="bash"
+        />
+      {:else if activeTab === "Open WebUI"}
+        <IntegrationCard
+          title="1. Start Open WebUI"
+          subtitle="Run in terminal"
+          description="Run this to start Open WebUI."
+          config={openWebUiCommand}
+          language="bash"
+        />
+        <IntegrationCard
+          title="2. Open & Select Model"
+          subtitle="http://localhost:3000"
+          description={`Open http://localhost:3000 in your browser. Select the running model from the dropdown at the top: ${runningModels.length > 0 ? runningModels.join(", ") : "no models running"}`}
+          config={"open http://localhost:3000"}
+          language="bash"
+        />
+        <IntegrationCard
+          title="Ollama CLI"
+          subtitle="Run in terminal"
+          description="Or use the Ollama CLI directly."
+          config={ollamaCommand}
+          language="bash"
+        />
+      {:else if activeTab === "n8n"}
+        <IntegrationCard
+          title="1. Start n8n"
+          subtitle="Run in terminal"
+          description="Start n8n with Docker. If you already have n8n running, skip this step."
+          config={n8nDockerCommand}
+          language="bash"
+        />
+        <IntegrationCard
+          title="2. Open n8n"
+          subtitle="http://localhost:5678"
+          description="Open n8n in your browser. If this is your first time, complete the setup and select 'Start from Scratch' when prompted."
+          config={"open http://localhost:5678"}
+          language="bash"
+        />
+        <IntegrationCard
+          title="3. Add OpenAI Credential"
+          subtitle="n8n UI → Credentials"
+          description="Create an OpenAI credential pointing at your exo cluster."
+          config={n8nCredentialSteps}
+        />
+        <IntegrationCard
+          title="4. Build a Workflow"
+          subtitle="n8n UI → Workflows"
+          description="Create a workflow that uses your exo-powered model."
+          config={n8nWorkflowSteps}
+        />
+      {:else if activeTab === "Firefox"}
+        <IntegrationCard
+          title="Firefox AI Chatbot"
+          subtitle="about:config"
+          description="Use the exo dashboard as Firefox's built-in AI chatbot. Requires Firefox 130+."
+          config={firefoxConfig}
+        />
+      {/if}
+    </div>
+  </main>
+</div>
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
index 8c415095..01166482 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "DeepSeek V3.1"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 405874409472
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
index a1889395..62313d3a 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "DeepSeek V3.1"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 765577920512
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
index 0ca9bb22..5288181e 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "DeepSeek V3.2"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 378086226621
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
index a6d9d628..87f81738 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "DeepSeek V3.2"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 755957120916
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
index efa2c1f6..644fe425 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "GLM 4.5 Air"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 122406567936
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
index 42727eed..a7b5c37a 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
@@ -9,5 +9,7 @@ quantization = "bf16"
 base_model = "GLM 4.5 Air"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 229780750336
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
index 22fba6a9..c8b32a38 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "GLM 4.7"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 198556925568
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
index b83c34e5..7eb301e5 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
@@ -9,5 +9,7 @@ quantization = "6bit"
 base_model = "GLM 4.7"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 286737579648
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
index 3f6b2391..91365de8 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "GLM 4.7"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 396963397248
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
index ca183ded..d0be6d82 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "GLM 4.7 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 19327352832
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
index 3b0c06c9..9c7136db 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
@@ -9,5 +9,7 @@ quantization = "5bit"
 base_model = "GLM 4.7 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 22548578304
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
index 8669298f..cf2ed455 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
@@ -9,5 +9,7 @@ quantization = "6bit"
 base_model = "GLM 4.7 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 26843545600
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
index 3ffcacb2..879b322c 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "GLM 4.7 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 34359738368
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
index 5affe179..37b0fb2a 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "GLM-5"
 capabilities = ["text", "thinking"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 790517400864
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
index 8e942ed3..2874e4e6 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
 base_model = "GLM-5"
 capabilities = ["text", "thinking"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 405478939008
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml b/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
index 2803710e..1086d86a 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
@@ -9,5 +9,7 @@ quantization = "bf16"
 base_model = "GLM-5"
 capabilities = ["text", "thinking"]
 
+context_length = 202752
+
 [storage_size]
 in_bytes = 1487822475264
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Kimi-K2-Instruct-4bit.toml
index da82c4d8..878aa2aa 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Kimi K2"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 620622774272
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml b/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
index d088b615..37f4befc 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
@@ -9,5 +9,7 @@ quantization = ""
 base_model = "Kimi K2"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 706522120192
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml b/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
index a2eea6c3..efe7d4bb 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
@@ -9,6 +9,8 @@ quantization = ""
 base_model = "Kimi K2.5"
 capabilities = ["text", "thinking", "thinking_toggle", "vision"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 662498705408
 
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.toml
index 2aacb04e..d98cabdd 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Llama-3.1-Nemotron-70B-Instruct"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 39688355840
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.toml
index f7c34f8e..4f4297ab 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.toml
@@ -8,5 +8,7 @@ quantization = "8bit"
 base_model = "NVIDIA Llama-3.1-Nemotron-70B-Instruct"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 74964549632
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.toml
index f513934b..4ef63c17 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.toml
@@ -8,5 +8,7 @@ quantization = "bf16"
 base_model = "NVIDIA Llama-3.1-Nemotron-70B-Instruct"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 141107412992
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.toml
index 12707721..687c5664 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Llama-3.1-Nemotron-Nano-4B-v1.1"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 2538706944
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.toml
index b4baebc6..dae6fd93 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.toml
@@ -8,5 +8,7 @@ quantization = "8bit"
 base_model = "NVIDIA Llama-3.1-Nemotron-Nano-4B-v1.1"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 4794980352
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.toml b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.toml
index 2258b856..0e261646 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.toml
@@ -8,5 +8,7 @@ quantization = "bf16"
 base_model = "NVIDIA Llama-3.1-Nemotron-Nano-4B-v1.1"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 9025492992
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.2-1B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.2-1B-Instruct-4bit.toml
index 888b9f97..fa30edc3 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.2-1B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.2-1B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Llama 3.2 1B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 729808896
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-4bit.toml
index 60486e64..6255db7b 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Llama 3.2 3B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 1863319552
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-8bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-8bit.toml
index d252a2e5..e2de35ec 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Llama 3.2 3B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 3501195264
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-4bit.toml
index 03f9c12b..ef81e828 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Llama 3.3 70B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 40652242944
diff --git a/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-8bit.toml b/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-8bit.toml
index ef0254a8..fb83f3d0 100644
--- a/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Llama 3.3 70B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 76799803392
diff --git a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-70B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-70B-Instruct-4bit.toml
index d4c57ae1..89313087 100644
--- a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-70B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-70B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Llama 3.1 70B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 40652242944
diff --git a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-4bit.toml
index 816564a0..a9ab123b 100644
--- a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Llama 3.1 8B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 4637851648
diff --git a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-8bit.toml b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-8bit.toml
index ff82803a..84cdbaf1 100644
--- a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Llama 3.1 8B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 8954839040
diff --git a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-bf16.toml b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-bf16.toml
index db19a097..f2121d64 100644
--- a/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-bf16.toml
@@ -9,5 +9,7 @@ quantization = "bf16"
 base_model = "Llama 3.1 8B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 16882073600
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
index ec1e4f50..0b22f011 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
@@ -9,5 +9,7 @@ quantization = "3bit"
 base_model = "MiniMax M2.1"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 196608
+
 [storage_size]
 in_bytes = 100086644736
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
index c3bb8177..32e5bc3f 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "MiniMax M2.1"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 196608
+
 [storage_size]
 in_bytes = 242986745856
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
index 963b3b9b..c6418512 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "MiniMax M2.5"
 capabilities = ["text", "thinking"]
 
+context_length = 196608
+
 [storage_size]
 in_bytes = 128666664960
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
index af329569..9c366e6e 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
@@ -9,5 +9,7 @@ quantization = "6bit"
 base_model = "MiniMax M2.5"
 capabilities = ["text", "thinking"]
 
+context_length = 196608
+
 [storage_size]
 in_bytes = 185826705408
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
index 2a39701d..c6946fa4 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "MiniMax M2.5"
 capabilities = ["text", "thinking"]
 
+context_length = 196608
+
 [storage_size]
 in_bytes = 242986745856
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.toml
index f40798ea..7b79a205 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 17775342336
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.toml
index 15761184..8a4c7384 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.toml
@@ -8,5 +8,7 @@ quantization = "5bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 21721476864
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.toml
index 238d4fe3..96b7a7fb 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.toml
@@ -8,5 +8,7 @@ quantization = "6bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 25667611392
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.toml
index 7317de54..1c95fa90 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.toml
@@ -8,5 +8,7 @@ quantization = "8bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 33559880448
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.toml
index fd8886af..5364e5bc 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.toml
@@ -8,5 +8,7 @@ quantization = "bf16"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 63155889408
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.toml
index ec402a22..1c7137da 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 16788808704
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.toml
index d9b86095..13aae100 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 19323906944
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-4bits.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-4bits.toml
index 8084787e..649fecd9 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-4bits.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-4bits.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "NVIDIA Nemotron-Nano-9B-v2"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 5002791936
diff --git a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-6bit.toml b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-6bit.toml
index 4f09bde3..e5c6422b 100644
--- a/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-6bit.toml
@@ -8,5 +8,7 @@ quantization = "6bit"
 base_model = "NVIDIA Nemotron-Nano-9B-v2"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 7224298496
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-0.6B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-0.6B-4bit.toml
index 5b4bd368..f99033fc 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-0.6B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-0.6B-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 0.6B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 32768
+
 [storage_size]
 in_bytes = 342884352
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-0.6B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-0.6B-8bit.toml
index 0dad33c0..8d7e7643 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-0.6B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-0.6B-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 0.6B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 32768
+
 [storage_size]
 in_bytes = 698351616
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-4bit.toml
index 4a4fb10f..6e2709f5 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 235B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 141733920768
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-8bit.toml
index ed70dca4..0e41f4ed 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 235B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 268435456000
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-4bit.toml
index c0f7b577..4e736c14 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 30B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 32768
+
 [storage_size]
 in_bytes = 17612931072
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-8bit.toml
index 4018ec2d..15308a84 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 30B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 32768
+
 [storage_size]
 in_bytes = 33279705088
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-4bit.toml
index e4838f9c..95010af8 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 Coder 480B"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 289910292480
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-8bit.toml
index 44558b78..963fb205 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 Coder 480B"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 579820584960
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-4bit.toml
index a81e0059..e28c1d95 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 Coder Next"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 45644286500
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-5bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-5bit.toml
index 535b62eb..58c63f78 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-5bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-5bit.toml
@@ -9,5 +9,7 @@ quantization = "5bit"
 base_model = "Qwen3 Coder Next"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 57657697020
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-6bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-6bit.toml
index 4ad7289d..17f921e5 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-6bit.toml
@@ -9,5 +9,7 @@ quantization = "6bit"
 base_model = "Qwen3 Coder Next"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 68899327465
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-8bit.toml
index 51f13a63..934894ac 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 Coder Next"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 89357758772
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-bf16.toml b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-bf16.toml
index 76d77693..fd135bb0 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-bf16.toml
@@ -9,5 +9,7 @@ quantization = "bf16"
 base_model = "Qwen3 Coder Next"
 capabilities = ["text", "code"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 157548627945
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-4bit.toml
index d9eb5845..7a331b46 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 Next 80B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 46976204800
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-8bit.toml
index 09352d3f..a54f01de 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 Next 80B"
 capabilities = ["text"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 88814387200
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
index 12f341fb..d126f727 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Qwen3 Next 80B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 47080074240
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
index 7a52dbf7..b008b130 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Qwen3 Next 80B"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 88814387200
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-VL-4B-Instruct-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-VL-4B-Instruct-4bit.toml
index b1cd18f6..6d34735f 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-VL-4B-Instruct-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-VL-4B-Instruct-4bit.toml
@@ -8,5 +8,7 @@ quantization = "4bit"
 base_model = "Qwen3-VL 4B"
 capabilities = ["text", "thinking", "vision"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 3340000000
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
index 452b0a44..100ab997 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "4bit"
 base_model = "Qwen3.5 122B A10B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 69593314272
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
index 7c01c831..99177f48 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "6bit"
 base_model = "Qwen3.5 122B A10B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 100120675296
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
index 12c6a460..cff7cf62 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 122B A10B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 130648036320
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
index a4b22916..2299c8e7 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "bf16"
 base_model = "Qwen3.5 122B A10B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 245125640160
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
index ee42738f..8c8a4f11 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "4bit"
 base_model = "Qwen3.5 27B"
-capabilities = ["text", "thinking"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 16054266848
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
index 42b1184c..3c7dc6eb 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 27B"
-capabilities = ["text", "thinking"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 29500943328
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
index b1821192..a62d4495 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 2B"
-capabilities = ["text", "thinking"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 2662787264
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
index 1c0a6fe9..dfb5e244 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "4bit"
 base_model = "Qwen3.5 35B A3B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 20391405152
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
index 64b7a61e..24881e04 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 35B A3B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 37721130592
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
index 10af0e43..7823e87b 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "4bit"
 base_model = "Qwen3.5 397B A17B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 223860768352
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
index b8427aa1..8df64cad 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "6bit"
 base_model = "Qwen3.5 397B A17B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 322946674272
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
index 73fdbcd3..4aa63400 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 397B A17B"
-capabilities = ["text", "thinking", "thinking_toggle"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 422032580192
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
index 67a4970f..c8bb95c7 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "4bit"
 base_model = "Qwen3.5 9B"
-capabilities = ["text", "thinking"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 5950062560
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
index 3dcf5b0d..6b1dfede 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
@@ -7,7 +7,9 @@ tasks = ["TextGeneration"]
 family = "qwen"
 quantization = "8bit"
 base_model = "Qwen3.5 9B"
-capabilities = ["text", "thinking"]
+capabilities = ["text", "thinking", "thinking_toggle", "vision"]
+
+context_length = 262144
 
 [storage_size]
 in_bytes = 10426433504
diff --git a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-4bit.toml b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-4bit.toml
index 9dd73125..d2971641 100644
--- a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-4bit.toml
@@ -9,5 +9,7 @@ quantization = "4bit"
 base_model = "Step 3.5 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 114572190076
diff --git a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-6bit.toml b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-6bit.toml
index f910f25f..4adc3bb4 100644
--- a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-6bit.toml
@@ -9,5 +9,7 @@ quantization = "6bit"
 base_model = "Step 3.5 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 159039627774
diff --git a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-8Bit.toml b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-8Bit.toml
index eb4d635c..1306637c 100644
--- a/resources/inference_model_cards/mlx-community--Step-3.5-Flash-8Bit.toml
+++ b/resources/inference_model_cards/mlx-community--Step-3.5-Flash-8Bit.toml
@@ -9,5 +9,7 @@ quantization = "8bit"
 base_model = "Step 3.5 Flash"
 capabilities = ["text", "thinking", "thinking_toggle"]
 
+context_length = 262144
+
 [storage_size]
 in_bytes = 209082699847
diff --git a/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
index 900c18f5..02e8b755 100644
--- a/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
 base_model = "GPT-OSS 120B"
 capabilities = ["text", "thinking"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 70652212224
diff --git a/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
index 98748fd1..a363bfab 100644
--- a/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
 base_model = "GPT-OSS 20B"
 capabilities = ["text", "thinking"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 12025908224
diff --git a/resources/inference_model_cards/mlx-community--llama-3.3-70b-instruct-fp16.toml b/resources/inference_model_cards/mlx-community--llama-3.3-70b-instruct-fp16.toml
index a46dbb88..194a88d7 100644
--- a/resources/inference_model_cards/mlx-community--llama-3.3-70b-instruct-fp16.toml
+++ b/resources/inference_model_cards/mlx-community--llama-3.3-70b-instruct-fp16.toml
@@ -9,5 +9,7 @@ quantization = "fp16"
 base_model = "Llama 3.3 70B"
 capabilities = ["text"]
 
+context_length = 131072
+
 [storage_size]
 in_bytes = 144383672320
diff --git a/src/exo/api/adapters/responses.py b/src/exo/api/adapters/responses.py
index 37b72558..2e8baf14 100644
--- a/src/exo/api/adapters/responses.py
+++ b/src/exo/api/adapters/responses.py
@@ -1,5 +1,6 @@
 """OpenAI Responses API adapter for converting requests/responses."""
 
+import json
 from collections.abc import AsyncGenerator
 from itertools import count
 from typing import Any
@@ -10,7 +11,26 @@ from exo.api.adapters.chat_completions import (
 )
 from exo.api.types import Usage
 from exo.api.types.openai_responses import (
+    ApplyPatchCallInputItem,
+    ApplyPatchCallOutputInputItem,
+    CodeInterpreterCallInputItem,
+    CompactionInputItem,
+    ComputerCallInputItem,
+    ComputerCallOutputInputItem,
+    CustomToolCallInputItem,
+    CustomToolCallOutputInputItem,
+    FileSearchCallInputItem,
     FunctionCallInputItem,
+    FunctionCallOutputInputItem,
+    ImageGenerationCallInputItem,
+    ItemReferenceInputItem,
+    LocalShellCallInputItem,
+    LocalShellCallOutputInputItem,
+    McpApprovalRequestInputItem,
+    McpApprovalResponseInputItem,
+    McpCallInputItem,
+    McpListToolsInputItem,
+    ReasoningInputItem,
     ResponseCompletedEvent,
     ResponseContentPart,
     ResponseContentPartAddedEvent,
@@ -39,7 +59,13 @@ from exo.api.types.openai_responses import (
     ResponseTextDeltaEvent,
     ResponseTextDoneEvent,
     ResponseUsage,
+    ShellCallInputItem,
+    ShellCallOutputInputItem,
+    ToolSearchCallInputItem,
+    ToolSearchOutputInputItem,
+    WebSearchCallInputItem,
 )
+from exo.shared.logging import logger
 from exo.shared.types.chunks import (
     ErrorChunk,
     PrefillProgressChunk,
@@ -87,64 +113,216 @@ async def responses_request_to_text_generation(
             )
 
         for item in request.input:
-            if isinstance(item, ResponseInputMessage):
-                content = _extract_content(item.content)
-                if isinstance(item.content, list):
-                    for part in item.content:
-                        if isinstance(part, ResponseInputImagePart) and part.image_url:
-                            url = part.image_url
-                            if url.startswith(("http://", "https://")):
-                                images.append(await fetch_image_url(url))
-                            else:
-                                images.append(extract_base64_from_data_url(url))
-                            has_images = True
-                if item.role in ("user", "assistant", "developer"):
-                    input_messages.append(InputMessage(role=item.role, content=content))
-                if item.role == "system":
-                    chat_template_messages.append(
-                        {"role": "system", "content": content}
-                    )
-                elif has_images:
-                    multimodal: list[dict[str, Any]] = []
+            match item:
+                case ResponseInputMessage():
+                    content = _extract_content(item.content)
                     if isinstance(item.content, list):
                         for part in item.content:
-                            if isinstance(part, ResponseInputImagePart):
-                                multimodal.append({"type": "image"})
-                            elif hasattr(part, "text"):
-                                multimodal.append({"type": "text", "text": part.text})
+                            if (
+                                isinstance(part, ResponseInputImagePart)
+                                and part.image_url
+                            ):
+                                url = part.image_url
+                                if url.startswith(("http://", "https://")):
+                                    images.append(await fetch_image_url(url))
+                                else:
+                                    images.append(extract_base64_from_data_url(url))
+                                has_images = True
+                    if item.role in ("user", "assistant", "developer"):
+                        input_messages.append(
+                            InputMessage(role=item.role, content=content)
+                        )
+                    if item.role == "system":
+                        chat_template_messages.append(
+                            {"role": "system", "content": content}
+                        )
+                    elif has_images:
+                        multimodal: list[dict[str, Any]] = []
+                        if isinstance(item.content, list):
+                            for part in item.content:
+                                if isinstance(part, ResponseInputImagePart):
+                                    multimodal.append({"type": "image"})
+                                elif hasattr(part, "text"):
+                                    multimodal.append(
+                                        {"type": "text", "text": part.text}
+                                    )
+                        chat_template_messages.append(
+                            {"role": item.role, "content": multimodal}
+                        )
+                        has_images = False
+                    else:
+                        chat_template_messages.append(
+                            {"role": item.role, "content": content}
+                        )
+                case (
+                    FunctionCallInputItem()
+                    | McpCallInputItem()
+                    | CustomToolCallInputItem()
+                ):
                     chat_template_messages.append(
-                        {"role": item.role, "content": multimodal}
+                        {
+                            "role": "assistant",
+                            "content": "",
+                            "tool_calls": [
+                                {
+                                    "id": item.call_id,
+                                    "type": "function",
+                                    "function": {
+                                        "name": item.name,
+                                        "arguments": item.arguments,
+                                    },
+                                }
+                            ],
+                        }
                     )
-                    has_images = False
-                else:
+                case (
+                    LocalShellCallInputItem()
+                    | ShellCallInputItem()
+                    | ComputerCallInputItem()
+                ):
                     chat_template_messages.append(
-                        {"role": item.role, "content": content}
+                        {
+                            "role": "assistant",
+                            "content": "",
+                            "tool_calls": [
+                                {
+                                    "id": item.call_id,
+                                    "type": "function",
+                                    "function": {
+                                        "name": item.type,
+                                        "arguments": json.dumps(item.action),
+                                    },
+                                }
+                            ],
+                        }
                     )
-            elif isinstance(item, FunctionCallInputItem):
-                chat_template_messages.append(
-                    {
-                        "role": "assistant",
-                        "content": "",
-                        "tool_calls": [
+                case ApplyPatchCallInputItem():
+                    chat_template_messages.append(
+                        {
+                            "role": "assistant",
+                            "content": "",
+                            "tool_calls": [
+                                {
+                                    "id": item.call_id,
+                                    "type": "function",
+                                    "function": {
+                                        "name": "apply_patch",
+                                        "arguments": json.dumps({"patch": item.patch}),
+                                    },
+                                }
+                            ],
+                        }
+                    )
+                case (
+                    WebSearchCallInputItem()
+                    | FileSearchCallInputItem()
+                    | CodeInterpreterCallInputItem()
+                    | ImageGenerationCallInputItem()
+                    | ToolSearchCallInputItem()
+                ):
+                    args: dict[str, Any] = {}
+                    if isinstance(item, WebSearchCallInputItem):
+                        args = {"query": item.query}
+                    elif isinstance(item, FileSearchCallInputItem):
+                        args = {"queries": item.queries}
+                    elif isinstance(item, CodeInterpreterCallInputItem):
+                        args = {"code": item.code}
+                    elif isinstance(item, ImageGenerationCallInputItem):
+                        args = {"prompt": item.prompt}
+                    else:
+                        args = {"query": item.query}
+                    chat_template_messages.append(
+                        {
+                            "role": "assistant",
+                            "content": "",
+                            "tool_calls": [
+                                {
+                                    "id": item.call_id,
+                                    "type": "function",
+                                    "function": {
+                                        "name": item.type,
+                                        "arguments": json.dumps(args),
+                                    },
+                                }
+                            ],
+                        }
+                    )
+                case (
+                    FunctionCallOutputInputItem()
+                    | LocalShellCallOutputInputItem()
+                    | ShellCallOutputInputItem()
+                    | ApplyPatchCallOutputInputItem()
+                    | ComputerCallOutputInputItem()
+                    | CustomToolCallOutputInputItem()
+                    | ToolSearchOutputInputItem()
+                ):
+                    output = (
+                        item.output
+                        if isinstance(item.output, str)
+                        else json.dumps(item.output)
+                    )
+                    chat_template_messages.append(
+                        {
+                            "role": "tool",
+                            "tool_call_id": item.call_id,
+                            "content": output,
+                        }
+                    )
+                case ReasoningInputItem():
+                    reasoning_text = ""
+                    if item.content:
+                        reasoning_text = "".join(
+                            entry.get("text", "") for entry in item.content
+                        )
+                    elif item.summary:
+                        reasoning_text = "".join(
+                            entry.get("text", "") for entry in item.summary
+                        )
+                    if reasoning_text:
+                        chat_template_messages.append(
+                            {"role": "assistant", "content": reasoning_text}
+                        )
+                case CompactionInputItem():
+                    if item.summary:
+                        chat_template_messages.append(
+                            {"role": "system", "content": item.summary}
+                        )
+                case McpListToolsInputItem():
+                    tools_desc = ", ".join(t.get("name", "") for t in item.tools)
+                    if tools_desc:
+                        chat_template_messages.append(
                             {
-                                "id": item.call_id,
-                                "type": "function",
-                                "function": {
-                                    "name": item.name,
-                                    "arguments": item.arguments,
-                                },
+                                "role": "system",
+                                "content": f"Available MCP tools ({item.server_label}): {tools_desc}",
                             }
-                        ],
-                    }
-                )
-            else:
-                chat_template_messages.append(
-                    {
-                        "role": "tool",
-                        "tool_call_id": item.call_id,
-                        "content": item.output,
-                    }
-                )
+                        )
+                case McpApprovalRequestInputItem():
+                    chat_template_messages.append(
+                        {
+                            "role": "assistant",
+                            "content": "",
+                            "tool_calls": [
+                                {
+                                    "id": item.call_id,
+                                    "type": "function",
+                                    "function": {
+                                        "name": item.name,
+                                        "arguments": item.arguments,
+                                    },
+                                }
+                            ],
+                        }
+                    )
+                case McpApprovalResponseInputItem():
+                    chat_template_messages.append(
+                        {
+                            "role": "tool",
+                            "tool_call_id": item.approval_request_id,
+                            "content": f"{'Approved' if item.approve else 'Denied'}{': ' + item.reason if item.reason else ''}",
+                        }
+                    )
+                case ItemReferenceInputItem():
+                    logger.info("Cannot handle ItemReferenceInputItem, skipping")
 
         input_value = (
             input_messages
diff --git a/src/exo/api/main.py b/src/exo/api/main.py
index 3021d689..5635a409 100644
--- a/src/exo/api/main.py
+++ b/src/exo/api/main.py
@@ -1660,6 +1660,7 @@ class API:
                     quantization=card.quantization,
                     base_model=card.base_model,
                     capabilities=card.capabilities,
+                    context_length=card.context_length,
                 )
                 for card in cards
             ]
diff --git a/src/exo/api/types/openai_responses.py b/src/exo/api/types/openai_responses.py
index b06593ac..0b747648 100644
--- a/src/exo/api/types/openai_responses.py
+++ b/src/exo/api/types/openai_responses.py
@@ -75,8 +75,228 @@ class FunctionCallOutputInputItem(BaseModel, frozen=True):
     status: ResponseStatus | None = None
 
 
+class ReasoningInputItem(BaseModel, frozen=True):
+    type: Literal["reasoning"] = "reasoning"
+    id: str | None = None
+    summary: list[dict[str, Any]] | None = None
+    encrypted_content: str | None = None
+    content: list[dict[str, Any]] | None = None
+    status: ResponseStatus | None = None
+
+
+class ComputerCallInputItem(BaseModel, frozen=True):
+    type: Literal["computer_call"] = "computer_call"
+    id: str | None = None
+    call_id: str = ""
+    action: dict[str, Any] = {}
+    status: ResponseStatus | None = None
+
+
+class ComputerCallOutputInputItem(BaseModel, frozen=True):
+    type: Literal["computer_call_output"] = "computer_call_output"
+    id: str | None = None
+    call_id: str = ""
+    output: dict[str, Any] | str = ""
+    status: ResponseStatus | None = None
+
+
+class WebSearchCallInputItem(BaseModel, frozen=True):
+    type: Literal["web_search_call"] = "web_search_call"
+    id: str | None = None
+    call_id: str = ""
+    query: str = ""
+    status: ResponseStatus | None = None
+
+
+class FileSearchCallInputItem(BaseModel, frozen=True):
+    type: Literal["file_search_call"] = "file_search_call"
+    id: str | None = None
+    call_id: str = ""
+    queries: list[str] = []
+    results: list[dict[str, Any]] | None = None
+    status: ResponseStatus | None = None
+
+
+class CodeInterpreterCallInputItem(BaseModel, frozen=True):
+    type: Literal["code_interpreter_call"] = "code_interpreter_call"
+    id: str | None = None
+    call_id: str = ""
+    code: str = ""
+    results: list[dict[str, Any]] | None = None
+    status: ResponseStatus | None = None
+
+
+class ImageGenerationCallInputItem(BaseModel, frozen=True):
+    type: Literal["image_generation_call"] = "image_generation_call"
+    id: str | None = None
+    call_id: str = ""
+    prompt: str = ""
+    result: dict[str, Any] | None = None
+    status: ResponseStatus | None = None
+
+
+class LocalShellCallInputItem(BaseModel, frozen=True):
+    type: Literal["local_shell_call"] = "local_shell_call"
+    id: str | None = None
+    call_id: str = ""
+    action: dict[str, Any] = {}
+    status: ResponseStatus | None = None
+
+
+class LocalShellCallOutputInputItem(BaseModel, frozen=True):
+    type: Literal["local_shell_call_output"] = "local_shell_call_output"
+    id: str | None = None
+    call_id: str = ""
+    output: str = ""
+    status: ResponseStatus | None = None
+
+
+class ShellCallInputItem(BaseModel, frozen=True):
+    type: Literal["shell_call"] = "shell_call"
+    id: str | None = None
+    call_id: str = ""
+    action: dict[str, Any] = {}
+    status: ResponseStatus | None = None
+
+
+class ShellCallOutputInputItem(BaseModel, frozen=True):
+    type: Literal["shell_call_output"] = "shell_call_output"
+    id: str | None = None
+    call_id: str = ""
+    output: str = ""
+    status: ResponseStatus | None = None
+
+
+class ApplyPatchCallInputItem(BaseModel, frozen=True):
+    type: Literal["apply_patch_call"] = "apply_patch_call"
+    id: str | None = None
+    call_id: str = ""
+    patch: str = ""
+    status: ResponseStatus | None = None
+
+
+class ApplyPatchCallOutputInputItem(BaseModel, frozen=True):
+    type: Literal["apply_patch_call_output"] = "apply_patch_call_output"
+    id: str | None = None
+    call_id: str = ""
+    output: str = ""
+    status: ResponseStatus | None = None
+
+
+class ToolSearchCallInputItem(BaseModel, frozen=True):
+    type: Literal["tool_search_call"] = "tool_search_call"
+    id: str | None = None
+    call_id: str = ""
+    query: str = ""
+    status: ResponseStatus | None = None
+
+
+class ToolSearchOutputInputItem(BaseModel, frozen=True):
+    type: Literal["tool_search_output"] = "tool_search_output"
+    id: str | None = None
+    call_id: str = ""
+    output: str = ""
+    status: ResponseStatus | None = None
+
+
+class McpCallInputItem(BaseModel, frozen=True):
+    type: Literal["mcp_call"] = "mcp_call"
+    id: str | None = None
+    call_id: str = ""
+    name: str = ""
+    arguments: str = ""
+    server_label: str = ""
+    approval_request_id: str | None = None
+    error: str | None = None
+    output: str | None = None
+    status: ResponseStatus | None = None
+
+
+class McpListToolsInputItem(BaseModel, frozen=True):
+    type: Literal["mcp_list_tools"] = "mcp_list_tools"
+    id: str | None = None
+    server_label: str = ""
+    tools: list[dict[str, Any]] = []
+    error: str | None = None
+    status: ResponseStatus | None = None
+
+
+class McpApprovalRequestInputItem(BaseModel, frozen=True):
+    type: Literal["mcp_approval_request"] = "mcp_approval_request"
+    id: str | None = None
+    call_id: str = ""
+    name: str = ""
+    arguments: str = ""
+    server_label: str = ""
+    status: ResponseStatus | None = None
+
+
+class McpApprovalResponseInputItem(BaseModel, frozen=True):
+    type: Literal["mcp_approval_response"] = "mcp_approval_response"
+    id: str | None = None
+    approval_request_id: str = ""
+    approve: bool = True
+    reason: str = ""
+    status: ResponseStatus | None = None
+
+
+class CustomToolCallInputItem(BaseModel, frozen=True):
+    type: Literal["custom_tool_call"] = "custom_tool_call"
+    id: str | None = None
+    call_id: str = ""
+    name: str = ""
+    arguments: str = ""
+    status: ResponseStatus | None = None
+
+
+class CustomToolCallOutputInputItem(BaseModel, frozen=True):
+    type: Literal["custom_tool_call_output"] = "custom_tool_call_output"
+    id: str | None = None
+    call_id: str = ""
+    output: str = ""
+    status: ResponseStatus | None = None
+
+
+class CompactionInputItem(BaseModel, frozen=True):
+    type: Literal["compaction"] = "compaction"
+    id: str | None = None
+    summary: str = ""
+    encrypted_content: str | None = None
+    status: ResponseStatus | None = None
+
+
+class ItemReferenceInputItem(BaseModel, frozen=True):
+    type: Literal["item_reference"] = "item_reference"
+    id: str | None = None
+
+
 ResponseInputItem = (
-    ResponseInputMessage | FunctionCallInputItem | FunctionCallOutputInputItem
+    ResponseInputMessage
+    | FunctionCallInputItem
+    | FunctionCallOutputInputItem
+    | ReasoningInputItem
+    | ComputerCallInputItem
+    | ComputerCallOutputInputItem
+    | WebSearchCallInputItem
+    | FileSearchCallInputItem
+    | CodeInterpreterCallInputItem
+    | ImageGenerationCallInputItem
+    | LocalShellCallInputItem
+    | LocalShellCallOutputInputItem
+    | ShellCallInputItem
+    | ShellCallOutputInputItem
+    | ApplyPatchCallInputItem
+    | ApplyPatchCallOutputInputItem
+    | ToolSearchCallInputItem
+    | ToolSearchOutputInputItem
+    | McpCallInputItem
+    | McpListToolsInputItem
+    | McpApprovalRequestInputItem
+    | McpApprovalResponseInputItem
+    | CustomToolCallInputItem
+    | CustomToolCallOutputInputItem
+    | CompactionInputItem
+    | ItemReferenceInputItem
 )
 
 
diff --git a/src/exo/shared/models/model_cards.py b/src/exo/shared/models/model_cards.py
index 0883d3e1..50da8125 100644
--- a/src/exo/shared/models/model_cards.py
+++ b/src/exo/shared/models/model_cards.py
@@ -134,6 +134,7 @@ class ModelCard(CamelCaseModel):
     quantization: str = ""
     base_model: str = ""
     capabilities: list[str] = []
+    context_length: int = 0
     uses_cfg: bool = False
     trust_remote_code: bool = True
     is_custom: bool = False
@@ -202,6 +203,7 @@ class ModelCard(CamelCaseModel):
             hidden_size=config_data.hidden_size or 0,
             supports_tensor=config_data.supports_tensor,
             num_key_value_heads=config_data.num_key_value_heads,
+            context_length=config_data.max_position_embeddings,
             tasks=[ModelTask.TextGeneration],
             trust_remote_code=False,
             is_custom=True,
@@ -240,6 +242,7 @@ class ConfigData(BaseModel):
             "decoder_layers",
         )
     )
+    max_position_embeddings: int = 0
     vision: VisionCardConfig | None = None
 
     @property
@@ -269,6 +272,7 @@ class ConfigData(BaseModel):
                 "architectures",
                 "hidden_size",
                 "num_key_value_heads",
+                "max_position_embeddings",
                 "num_hidden_layers",
                 "num_layers",
                 "n_layer",
diff --git a/src/exo/worker/engines/mlx/generator/batch_generate.py b/src/exo/worker/engines/mlx/generator/batch_generate.py
index 303609a4..a6d9646b 100644
--- a/src/exo/worker/engines/mlx/generator/batch_generate.py
+++ b/src/exo/worker/engines/mlx/generator/batch_generate.py
@@ -132,6 +132,8 @@ class ExoBatchGenerator:
                     vision_processor=self.vision_processor,
                     tokenizer=self.tokenizer,
                     model=self.model,
+                    model_id=task_params.model,
+                    task_params=task_params,
                 )
             except Exception:
                 logger.opt(exception=True).warning(
diff --git a/src/exo/worker/engines/mlx/generator/generate.py b/src/exo/worker/engines/mlx/generator/generate.py
index d49ff58c..043a822f 100644
--- a/src/exo/worker/engines/mlx/generator/generate.py
+++ b/src/exo/worker/engines/mlx/generator/generate.py
@@ -508,6 +508,8 @@ def mlx_generate(
                 vision_processor=vision_processor,
                 tokenizer=tokenizer,
                 model=model,
+                model_id=task.model,
+                task_params=task,
             )
         except Exception:
             logger.opt(exception=True).warning(
diff --git a/src/exo/worker/engines/mlx/utils_mlx.py b/src/exo/worker/engines/mlx/utils_mlx.py
index 4ee643b9..e84a1934 100644
--- a/src/exo/worker/engines/mlx/utils_mlx.py
+++ b/src/exo/worker/engines/mlx/utils_mlx.py
@@ -503,11 +503,39 @@ def _needs_dsml_encoding(task_params: TextGenerationTaskParams) -> bool:
     return "deepseek-v3.2" in task_params.model.lower()
 
 
-def apply_chat_template(
+def consolidate_system_messages(
+    messages: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
+    """
+    System messages almost exclusively must go at the start of a message
+    and there must only be a single one.
+
+    Also, Codex sends "developer" messages which are just system prompts.
+    """
+    system_parts: list[str] = []
+    non_system: list[dict[str, Any]] = []
+    for msg in messages:
+        if msg.get("role") in ("system", "developer"):
+            content = cast(str, msg.get("content", ""))
+            if content:
+                system_parts.append(content)
+        else:
+            non_system.append(msg)
+    formatted_messages = non_system
+    if system_parts:
+        formatted_messages.insert(
+            0, {"role": "system", "content": "\n".join(system_parts)}
+        )
+    return formatted_messages
+
+
+def render_chat_template(
     tokenizer: TokenizerWrapper,
+    messages: list[dict[str, Any]],
     task_params: TextGenerationTaskParams,
 ) -> str:
-    """Convert TextGenerationTaskParams to a chat template prompt.
+    """
+    Convert TextGenerationTaskParams to a chat template prompt.
 
     Converts the internal format (input + instructions) to a messages list
     that can be processed by the tokenizer's chat template.
@@ -515,23 +543,7 @@ def apply_chat_template(
     When chat_template_messages is available (from Chat Completions API),
     uses those directly to preserve tool_calls, thinking, and other fields.
     """
-    formatted_messages: list[dict[str, Any]] = []
-    if task_params.chat_template_messages is not None:
-        # Use pre-formatted messages that preserve tool_calls, thinking, etc.
-        formatted_messages = list(task_params.chat_template_messages)
-    else:
-        # Add system message (instructions) if present
-        if task_params.instructions:
-            formatted_messages.append(
-                {"role": "system", "content": task_params.instructions}
-            )
-
-        # Convert input to messages
-        for msg in task_params.input:
-            if not msg.content:
-                logger.warning("Received message with empty content, skipping")
-                continue
-            formatted_messages.append({"role": msg.role, "content": msg.content})
+    formatted_messages = consolidate_system_messages(messages)
 
     # For assistant prefilling, append content after templating to avoid a closing turn token.
     partial_assistant_content: str | None = None
@@ -592,6 +604,30 @@ def apply_chat_template(
     if partial_assistant_content:
         prompt += partial_assistant_content
 
+    return prompt
+
+
+def apply_chat_template(
+    tokenizer: TokenizerWrapper,
+    task_params: TextGenerationTaskParams,
+) -> str:
+    messages: list[dict[str, Any]] = []
+    if task_params.chat_template_messages is not None:
+        # Use pre-formatted messages that preserve tool_calls, thinking, etc.
+        messages = list(task_params.chat_template_messages)
+    else:
+        # Add system message (instructions) if present
+        if task_params.instructions:
+            messages.append({"role": "system", "content": task_params.instructions})
+
+        # Convert input to messages
+        for msg in task_params.input:
+            if not msg.content:
+                logger.warning("Received message with empty content, skipping")
+                continue
+            messages.append({"role": msg.role, "content": msg.content})
+
+    prompt = render_chat_template(tokenizer, messages, task_params)
     logger.info(prompt)
 
     return prompt
diff --git a/src/exo/worker/engines/mlx/vision.py b/src/exo/worker/engines/mlx/vision.py
index f0839b21..49cb024d 100644
--- a/src/exo/worker/engines/mlx/vision.py
+++ b/src/exo/worker/engines/mlx/vision.py
@@ -25,8 +25,12 @@ from exo.download.download_utils import build_model_path
 from exo.shared.models.model_cards import VisionCardConfig
 from exo.shared.types.common import ModelId
 from exo.shared.types.mlx import Model
+from exo.shared.types.text_generation import TextGenerationTaskParams
 from exo.worker.engines.mlx.cache import encode_prompt
-from exo.worker.engines.mlx.utils_mlx import fix_unmatched_think_end_tokens
+from exo.worker.engines.mlx.utils_mlx import (
+    fix_unmatched_think_end_tokens,
+    render_chat_template,
+)
 from exo.worker.runner.bootstrap import logger
 
 
@@ -86,15 +90,9 @@ def build_vision_prompt(
     chat_template_messages: list[dict[str, Any]],
     n_tokens_per_image: list[int],
     image_token: str,
+    task_params: TextGenerationTaskParams,
 ) -> str:
-    logger.info(
-        f"Vision prompt messages: {[{k: (v[:50] if isinstance(v, str) else v) for k, v in m.items()} for m in chat_template_messages]}"  # type: ignore
-    )
-    prompt: str = tokenizer.apply_chat_template(
-        chat_template_messages,
-        tokenize=False,
-        add_generation_prompt=True,
-    )
+    prompt = render_chat_template(tokenizer, chat_template_messages, task_params)
 
     image_idx = 0
     result: list[str] = []
@@ -498,6 +496,7 @@ class VisionProcessor:
         chat_template_messages: list[dict[str, Any]],
         tokenizer: TokenizerWrapper,
         model: Model,
+        task_params: TextGenerationTaskParams,
     ) -> VisionResult:
         logger.info(f"Vision pipeline: {len(images)} image(s)")
 
@@ -529,6 +528,7 @@ class VisionProcessor:
             formatted_messages,
             n_tokens_per_image,
             image_token,
+            task_params,
         )
 
         logger.info(
@@ -572,6 +572,8 @@ def prepare_vision(
     vision_processor: VisionProcessor,
     tokenizer: TokenizerWrapper,
     model: Model,
+    model_id: ModelId,
+    task_params: TextGenerationTaskParams,
 ) -> VisionResult | None:
     if not images:
         return None
@@ -586,4 +588,5 @@ def prepare_vision(
         chat_template_messages=chat_template_messages,
         tokenizer=tokenizer,
         model=model,
+        task_params=task_params,
     )

← e5cb7b80 Add SSE-keepalive to not time out on long prefill on clients  ·  back to Exo  ·  Only update KV prefix cache on a good cache hit (#1817) c6815bfd →