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 --> <!-- - -->
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "DeepSeek V3.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 405874409472
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "DeepSeek V3.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 765577920512
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "DeepSeek V3.2"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 378086226621
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "DeepSeek V3.2"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 755957120916
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 198556925568
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "6bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 286737579648
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "GLM 4.7"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 396963397248
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 790517400864
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 405478939008
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "bf16"
|
||||
base_model = "GLM-5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 202752
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 1487822475264
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Kimi K2"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 620622774272
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = ""
|
||||
base_model = "Kimi K2"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 706522120192
|
||||
|
||||
@@ -9,6 +9,8 @@ quantization = ""
|
||||
base_model = "Kimi K2.5"
|
||||
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 662498705408
|
||||
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Llama 3.2 1B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 729808896
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Llama 3.2 3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 1863319552
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Llama 3.2 3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 3501195264
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Llama 3.3 70B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 40652242944
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Llama 3.3 70B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 76799803392
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Llama 3.1 70B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 40652242944
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Llama 3.1 8B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 4637851648
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Llama 3.1 8B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 8954839040
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "bf16"
|
||||
base_model = "Llama 3.1 8B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 16882073600
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "3bit"
|
||||
base_model = "MiniMax M2.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 100086644736
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "MiniMax M2.1"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 242986745856
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 128666664960
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "6bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 185826705408
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "MiniMax M2.5"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 196608
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 242986745856
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "4bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 17775342336
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "5bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 21721476864
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "6bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 25667611392
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "8bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 33559880448
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "bf16"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 63155889408
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "4bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 16788808704
|
||||
|
||||
+2
@@ -8,5 +8,7 @@ quantization = "4bit"
|
||||
base_model = "NVIDIA Nemotron-3-Nano-30B-A3B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 19323906944
|
||||
|
||||
@@ -8,5 +8,7 @@ quantization = "4bit"
|
||||
base_model = "NVIDIA Nemotron-Nano-9B-v2"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 5002791936
|
||||
|
||||
@@ -8,5 +8,7 @@ quantization = "6bit"
|
||||
base_model = "NVIDIA Nemotron-Nano-9B-v2"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 7224298496
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 0.6B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 32768
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 342884352
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 0.6B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 32768
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 698351616
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 235B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 141733920768
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 235B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 268435456000
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 30B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 32768
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 17612931072
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 30B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 32768
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 33279705088
|
||||
|
||||
+2
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 Coder 480B"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 289910292480
|
||||
|
||||
+2
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 Coder 480B"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 579820584960
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 45644286500
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "5bit"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 57657697020
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "6bit"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 68899327465
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 89357758772
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "bf16"
|
||||
base_model = "Qwen3 Coder Next"
|
||||
capabilities = ["text", "code"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 157548627945
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 46976204800
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 88814387200
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 47080074240
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "8bit"
|
||||
base_model = "Qwen3 Next 80B"
|
||||
capabilities = ["text", "thinking", "thinking_toggle"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 88814387200
|
||||
|
||||
@@ -8,5 +8,7 @@ quantization = "4bit"
|
||||
base_model = "Qwen3-VL 4B"
|
||||
capabilities = ["text", "thinking", "vision"]
|
||||
|
||||
context_length = 262144
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 3340000000
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
|
||||
base_model = "GPT-OSS 120B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 70652212224
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "MXFP4-Q8"
|
||||
base_model = "GPT-OSS 20B"
|
||||
capabilities = ["text", "thinking"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 12025908224
|
||||
|
||||
@@ -9,5 +9,7 @@ quantization = "fp16"
|
||||
base_model = "Llama 3.3 70B"
|
||||
capabilities = ["text"]
|
||||
|
||||
context_length = 131072
|
||||
|
||||
[storage_size]
|
||||
in_bytes = 144383672320
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user