← 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
M dashboard/src/lib/components/HeaderNav.svelteA dashboard/src/lib/components/IntegrationCard.svelteM dashboard/src/lib/components/index.tsA dashboard/src/routes/integrations/+page.svelteM resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-4bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-6bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-5-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--GLM-5-bf16.tomlM resources/inference_model_cards/mlx-community--Kimi-K2-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.tomlM resources/inference_model_cards/mlx-community--Kimi-K2.5.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-4bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-8bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-70B-Instruct-HF-bf16.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-4bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-8bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.1-Nemotron-Nano-4B-v1.1-bf16.tomlM resources/inference_model_cards/mlx-community--Llama-3.2-1B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.2-3B-Instruct-8bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Llama-3.3-70B-Instruct-8bit.tomlM resources/inference_model_cards/mlx-community--Meta-Llama-3.1-70B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-8bit.tomlM resources/inference_model_cards/mlx-community--Meta-Llama-3.1-8B-Instruct-bf16.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-4Bit.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-5Bit.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-6Bit.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-8Bit.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-BF16.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-MLX-MXFP4.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-4bits.tomlM resources/inference_model_cards/mlx-community--NVIDIA-Nemotron-Nano-9B-v2-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-0.6B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-0.6B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-235B-A22B-Instruct-2507-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-30B-A3B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-480B-A35B-Instruct-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-5bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Coder-Next-bf16.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Instruct-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-VL-4B-Instruct-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.tomlM resources/inference_model_cards/mlx-community--Step-3.5-Flash-4bit.tomlM resources/inference_model_cards/mlx-community--Step-3.5-Flash-6bit.tomlM resources/inference_model_cards/mlx-community--Step-3.5-Flash-8Bit.tomlM resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--llama-3.3-70b-instruct-fp16.tomlM src/exo/api/adapters/responses.pyM src/exo/api/main.pyM src/exo/api/types/openai_responses.pyM src/exo/shared/models/model_cards.pyM src/exo/worker/engines/mlx/generator/batch_generate.pyM src/exo/worker/engines/mlx/generator/generate.pyM src/exo/worker/engines/mlx/utils_mlx.pyM src/exo/worker/engines/mlx/vision.py
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 →