summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-21 19:06:15 +0900
committerAdam Malczewski <[email protected]>2026-05-21 19:06:15 +0900
commit55633c90c0d96e62153a4b6655f3f833a3b46ad4 (patch)
treea75f97b48071eb1ba9e8366d8053a376dde69af5
parentd6b208342edf97bafa5b1dcc986b782f9879d141 (diff)
downloaddispatch-55633c90c0d96e62153a4b6655f3f833a3b46ad4.tar.gz
dispatch-55633c90c0d96e62153a4b6655f3f833a3b46ad4.zip
refactor: gut model/tag/fallback/agent-template system, fix Docker setup
- Remove ModelResolver, model definitions, model tags, fallback order, agent templates - Remove all [[models]], [agents], and fallback from dispatch.toml and config schema - ModelRegistry is now a pure key-state manager - dispatch.toml reduced to keys + permissions only - Docker: fix entrypoint for existing UID, skip bun install in frontend container - Docker: scoped build cache prune in bin/clean
-rwxr-xr-xbin/clean3
-rw-r--r--dispatch.toml46
-rw-r--r--docker-compose.yml4
-rw-r--r--docker/entrypoint.dev.sh6
-rw-r--r--packages/api/src/agent-manager.ts59
-rw-r--r--packages/api/src/routes/models.ts44
-rw-r--r--packages/core/src/config/schema.ts126
-rw-r--r--packages/core/src/index.ts2
-rw-r--r--packages/core/src/models/index.ts1
-rw-r--r--packages/core/src/models/registry.ts64
-rw-r--r--packages/core/src/models/resolver.ts88
-rw-r--r--packages/core/src/types/index.ts25
-rw-r--r--packages/frontend/src/App.svelte14
-rw-r--r--packages/frontend/src/lib/components/ModelStatus.svelte26
-rw-r--r--packages/frontend/src/lib/components/SidebarPanel.svelte8
-rw-r--r--packages/frontend/src/lib/types.ts6
16 files changed, 36 insertions, 486 deletions
diff --git a/bin/clean b/bin/clean
index 1a3d459..76bdbe1 100755
--- a/bin/clean
+++ b/bin/clean
@@ -4,5 +4,6 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
-# Stop containers, remove volumes, remove images
+# Stop containers, remove volumes, remove images, and clear build cache
sudo docker compose -f "$PROJECT_DIR/docker-compose.yml" down --volumes --rmi local "$@"
+sudo docker builder prune -f --filter "label=com.docker.compose.project=dispatch"
diff --git a/dispatch.toml b/dispatch.toml
index 6789e95..4332462 100644
--- a/dispatch.toml
+++ b/dispatch.toml
@@ -1,10 +1,7 @@
-# Dispatch — Model & Key Configuration
+# Dispatch — Key & Permission Configuration
# Credentials and API keys are stored in the SQLite database.
# Use the Model Status panel to import credentials, or run bin/import-credentials.ts.
-# ─── Fallback Order (highest priority first) ────────────────────
-fallback = ["claude-pro", "claude-max", "opencode-1", "opencode-2", "copilot"]
-
# ─── API Keys ───────────────────────────────────────────────────
[[keys]]
@@ -34,47 +31,6 @@ id = "copilot"
provider = "github-copilot"
base_url = "https://api.githubcopilot.com"
-# ─── Models ─────────────────────────────────────────────────────
-
-[[models]]
-id = "claude-sonnet-4-20250514"
-provider = "anthropic"
-tags = ["heavy", "coding", "review"]
-
-[[models]]
-id = "claude-opus-4-20250514"
-provider = "anthropic"
-tags = ["heavy", "coding", "review"]
-
-[[models]]
-id = "deepseek-v4-pro"
-provider = "opencode-go"
-tags = ["heavy", "coding"]
-
-[[models]]
-id = "deepseek-v4-flash"
-provider = "opencode-go"
-tags = ["light", "quick"]
-
-[[models]]
-id = "gpt-4o"
-provider = "github-copilot"
-tags = ["heavy", "coding"]
-
-[[models]]
-id = "claude-3.5-sonnet"
-provider = "github-copilot"
-tags = ["heavy", "coding", "review"]
-
-# ─── Agent Templates ─────────────────────────────────────────────
-
-[agents.default]
-name = "Dispatch"
-description = "Default coding assistant"
-system_prompt = "You are a helpful AI coding assistant."
-tools = ["read_file", "write_file", "list_files", "run_shell", "task_list"]
-model_tag = "heavy"
-
# ─── Permissions ─────────────────────────────────────────────────
[permissions]
diff --git a/docker-compose.yml b/docker-compose.yml
index 3a019e8..2ce08a8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,7 +27,11 @@ services:
- "5173:5173"
volumes:
- .:/app
+ depends_on:
+ api:
+ condition: service_started
environment:
HOST_UID: ${HOST_UID:-1000}
HOST_GID: ${HOST_GID:-1000}
HOST_USER: ${HOST_USER:-dispatch}
+ SKIP_INSTALL: "1"
diff --git a/docker/entrypoint.dev.sh b/docker/entrypoint.dev.sh
index 8378585..bbde09a 100644
--- a/docker/entrypoint.dev.sh
+++ b/docker/entrypoint.dev.sh
@@ -40,8 +40,10 @@ if [ -d /app/node_modules ]; then
chown -R "$HOST_UID:$HOST_GID" /app/node_modules
fi
-# Install/update dependencies as the target user
-su -s /bin/bash - "$USER_NAME" -c "export HOME=$USER_HOME && cd /app && bun install"
+# Install/update dependencies as the target user (skip with SKIP_INSTALL=1)
+if [ "${SKIP_INSTALL:-}" != "1" ]; then
+ su -s /bin/bash - "$USER_NAME" -c "export HOME=$USER_HOME && cd /app && bun install"
+fi
# Execute the main command as the target user
exec su -s /bin/bash - "$USER_NAME" -c "export HOME=$USER_HOME && cd /app && exec $*"
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 92396b6..6e78ad7 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -16,7 +16,6 @@ import {
loadSkills,
createSkillsWatcher,
ModelRegistry,
- ModelResolver,
TaskList,
createTaskListTool,
type ClaudeAccount,
@@ -53,7 +52,6 @@ export class AgentManager {
private config: DispatchConfig;
private skillsData: { skills: SkillDefinition[]; mappings: AgentSkillMapping[] };
private modelRegistry: ModelRegistry | null = null;
- private modelResolver: ModelResolver | null = null;
private taskList: TaskList;
private configWatcher: { close(): void } | null = null;
@@ -89,7 +87,6 @@ export class AgentManager {
setSkillsGetter(() => this.skillsData);
setModelsGetter(
() => this.modelRegistry,
- () => this.modelResolver,
);
setAccountsGetter(() => this.claudeAccounts);
@@ -135,17 +132,14 @@ export class AgentManager {
}
private _initModelRegistry(config: DispatchConfig): void {
- if (config.models && config.keys) {
+ if (config.keys) {
if (this.modelRegistry) {
- this.modelRegistry.updateConfig(config.models, config.keys, config.fallback ?? []);
+ this.modelRegistry.updateConfig(config.keys);
} else {
- this.modelRegistry = new ModelRegistry(config.models, config.keys, config.fallback ?? []);
- this.modelResolver = new ModelResolver(this.modelRegistry);
+ this.modelRegistry = new ModelRegistry(config.keys);
}
} else {
- // Models/keys removed from config — clear the registry
this.modelRegistry = null;
- this.modelResolver = null;
}
}
@@ -263,7 +257,7 @@ export class AgentManager {
}
}
} else {
- console.warn(`dispatch: key "${effectiveKeyId}" not found in model registry, falling back to tag-based resolution`);
+ console.warn(`dispatch: key "${effectiveKeyId}" not found in model registry`);
}
}
@@ -273,51 +267,6 @@ export class AgentManager {
this.activeModelId = null;
}
- if (!useOverride && this.modelRegistry && this.modelResolver) {
- // Try to get model_tag from default agent template, fall back to "heavy"
- const defaultAgent = this.config.agents?.["default"];
- const tag = defaultAgent?.model_tag ?? "heavy";
- const resolved = this.modelResolver.resolve(tag);
- if (resolved) {
- model = resolved.model.id;
- baseURL = resolved.key.base_url;
- // Check if resolved key is anthropic
- if (resolved.key.provider === "anthropic") {
- const credFile = resolved.key.credentials_file;
- const account = this.claudeAccounts.find((a) => a.id === resolved.key.id)
- ?? (credFile ? this.claudeAccounts.find((a) => a.source === credFile) : this.claudeAccounts[0]);
- if (account) {
- let creds = refreshAccountCredentials(account);
- if (!creds || creds.expiresAt <= Date.now() + 60_000) {
- creds = await refreshAccountCredentialsAsync(account);
- if (creds) account.credentials = creds;
- }
- if (creds) {
- claudeCredentials = { accessToken: creds.accessToken };
- apiKey = creds.accessToken;
- provider = "anthropic";
- } else {
- console.warn(`dispatch: no valid Claude credentials for key "${resolved.key.id}"`);
- }
- } else {
- console.warn(`dispatch: no Claude credentials found for key "${resolved.key.id}"`);
- }
- } else {
- const envKey = resolveApiKey(resolved.key.id);
- if (envKey) {
- apiKey = envKey;
- } else {
- console.warn(`dispatch: env var not set for key "${resolved.key.id}", falling back to defaults`);
- model = "deepseek-v4-flash";
- baseURL = "https://opencode.ai/zen/go/v1";
- apiKey = "";
- }
- }
- } else {
- console.warn(`dispatch: could not resolve model for tag "${tag}", falling back to env vars`);
- }
- }
-
this.agent = new Agent({
model,
apiKey,
diff --git a/packages/api/src/routes/models.ts b/packages/api/src/routes/models.ts
index ca58e38..86411f1 100644
--- a/packages/api/src/routes/models.ts
+++ b/packages/api/src/routes/models.ts
@@ -1,4 +1,4 @@
-import type { ModelRegistry, ModelResolver } from "@dispatch/core";
+import type { ModelRegistry } from "@dispatch/core";
import {
ANTHROPIC_MODELS_FALLBACK,
type ClaudeAccount,
@@ -20,15 +20,12 @@ import {
import { Hono } from "hono";
let getRegistry: () => ModelRegistry | null = () => null;
-let getResolver: () => ModelResolver | null = () => null;
let getAccounts: () => ClaudeAccount[] = () => [];
export function setModelsGetter(
registryGetter: () => ModelRegistry | null,
- resolverGetter: () => ModelResolver | null,
): void {
getRegistry = registryGetter;
- getResolver = resolverGetter;
}
export function setAccountsGetter(getter: () => ClaudeAccount[]): void {
@@ -45,11 +42,9 @@ export const modelsRoutes = new Hono();
modelsRoutes.get("/", (c) => {
const registry = getRegistry();
if (!registry) {
- return c.json({ models: [], tags: [], keys: [] });
+ return c.json({ keys: [] });
}
- const models = registry.getModels();
- const tags = registry.getAllTags();
const keyStates = registry.getKeys();
const keys = keyStates.map((ks) => ({
@@ -60,40 +55,7 @@ modelsRoutes.get("/", (c) => {
exhaustedAt: ks.exhaustedAt ?? null,
}));
- return c.json({ models, tags, keys });
-});
-
-modelsRoutes.get("/resolve", (c) => {
- const registry = getRegistry();
- const resolver = getResolver();
- if (!registry || !resolver) {
- return c.json({ resolved: null, reason: "no models configured" });
- }
-
- const tag = c.req.query("tag");
- if (!tag) {
- return c.json({ error: "tag query parameter is required" }, 400);
- }
-
- const matchingModels = registry.getModelsByTag(tag);
- if (matchingModels.length === 0) {
- return c.json({ resolved: null, reason: "no models match tag" });
- }
-
- const resolved = resolver.resolve(tag);
- if (!resolved) {
- return c.json({ resolved: null, reason: "all keys exhausted for matching providers" });
- }
-
- return c.json({
- resolved: {
- model: resolved.model,
- key: {
- id: resolved.key.id,
- provider: resolved.key.provider,
- },
- },
- });
+ return c.json({ keys });
});
// Fetch available models for a specific provider key.
diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts
index 83e2695..2cd8d55 100644
--- a/packages/core/src/config/schema.ts
+++ b/packages/core/src/config/schema.ts
@@ -1,9 +1,7 @@
import type {
- AgentTemplate,
ConfigError,
DispatchConfig,
KeyDefinition,
- ModelDefinition,
} from "../types/index.js";
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -58,69 +56,6 @@ function validatePermissions(
return result;
}
-function validateAgentTemplate(
- raw: unknown,
- path: string,
- errors: ConfigError[],
-): AgentTemplate | null {
- if (!isRecord(raw)) {
- errors.push({ path, message: "must be an object" });
- return null;
- }
-
- const requiredStrings = ["name", "description", "system_prompt", "model_tag"] as const;
- for (const field of requiredStrings) {
- if (typeof raw[field] !== "string") {
- errors.push({ path: `${path}.${field}`, message: "must be a string" });
- return null;
- }
- }
-
- if (!Array.isArray(raw["tools"]) || !raw["tools"].every((t) => typeof t === "string")) {
- errors.push({ path: `${path}.tools`, message: "must be an array of strings" });
- return null;
- }
-
- const perms = validatePermissions(raw["permissions"] ?? {}, `${path}.permissions`, errors);
-
- return {
- name: raw["name"] as string,
- description: raw["description"] as string,
- system_prompt: raw["system_prompt"] as string,
- tools: raw["tools"] as string[],
- model_tag: raw["model_tag"] as string,
- permissions: perms,
- };
-}
-
-function validateModel(raw: unknown, path: string, errors: ConfigError[]): ModelDefinition | null {
- if (!isRecord(raw)) {
- errors.push({ path, message: "must be an object" });
- return null;
- }
- if (typeof raw["id"] !== "string") {
- errors.push({ path: `${path}.id`, message: "must be a string" });
- return null;
- }
- if (typeof raw["provider"] !== "string") {
- errors.push({ path: `${path}.provider`, message: "must be a string" });
- return null;
- }
- if (
- !Array.isArray(raw["tags"]) ||
- raw["tags"].length === 0 ||
- !raw["tags"].every((t) => typeof t === "string")
- ) {
- errors.push({ path: `${path}.tags`, message: "must be a non-empty array of strings" });
- return null;
- }
- return {
- id: raw["id"] as string,
- provider: raw["provider"] as string,
- tags: raw["tags"] as string[],
- };
-}
-
function validateKey(raw: unknown, path: string, errors: ConfigError[]): KeyDefinition | null {
if (!isRecord(raw)) {
errors.push({ path, message: "must be an object" });
@@ -169,34 +104,6 @@ export function validateConfig(raw: unknown): { config: DispatchConfig; errors:
// permissions (required, but can be empty)
const permissions = validatePermissions(raw["permissions"] ?? {}, "permissions", errors);
- // agents (optional)
- let agents: Record<string, AgentTemplate> | undefined;
- if (raw["agents"] !== undefined) {
- if (!isRecord(raw["agents"])) {
- errors.push({ path: "agents", message: "must be an object" });
- } else {
- agents = {};
- for (const [key, value] of Object.entries(raw["agents"])) {
- const agent = validateAgentTemplate(value, `agents.${key}`, errors);
- if (agent) agents[key] = agent;
- }
- }
- }
-
- // models (optional)
- let models: ModelDefinition[] | undefined;
- if (raw["models"] !== undefined) {
- if (!Array.isArray(raw["models"])) {
- errors.push({ path: "models", message: "must be an array" });
- } else {
- models = [];
- for (let i = 0; i < raw["models"].length; i++) {
- const model = validateModel(raw["models"][i], `models[${i}]`, errors);
- if (model) models.push(model);
- }
- }
- }
-
// keys (optional)
let keys: KeyDefinition[] | undefined;
if (raw["keys"] !== undefined) {
@@ -211,42 +118,9 @@ export function validateConfig(raw: unknown): { config: DispatchConfig; errors:
}
}
- // fallback (optional)
- let fallback: string[] | undefined;
- if (raw["fallback"] !== undefined) {
- if (!Array.isArray(raw["fallback"]) || !raw["fallback"].every((f) => typeof f === "string")) {
- errors.push({ path: "fallback", message: "must be an array of strings" });
- } else {
- fallback = raw["fallback"] as string[];
- // Validate that referenced key IDs exist
- const keyIds = new Set((keys ?? []).map((k) => k.id));
- for (const id of fallback) {
- if (!keyIds.has(id)) {
- errors.push({ path: "fallback", message: `key id "${id}" not found in keys` });
- }
- }
- }
- }
-
- // Warn if agent model_tags don't match any model
- if (agents && models) {
- const allTags = new Set(models.flatMap((m) => m.tags));
- for (const [name, agent] of Object.entries(agents)) {
- if (!allTags.has(agent.model_tag)) {
- errors.push({
- path: `agents.${name}.model_tag`,
- message: `no model found with tag "${agent.model_tag}"`,
- });
- }
- }
- }
-
const config: DispatchConfig = {
permissions,
- ...(agents !== undefined && { agents }),
- ...(models !== undefined && { models }),
...(keys !== undefined && { keys }),
- ...(fallback !== undefined && { fallback }),
};
return { config, errors };
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 90cafbe..b1e2e14 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -25,7 +25,7 @@ export { loadConfig, configToRuleset, validateConfig, createConfigWatcher } from
export { parseSkillFile, loadSkills, resolveSkillsForAgent, getSkillByName, createSkillsWatcher } from "./skills/index.js";
// Models
-export { ModelRegistry, ModelResolver } from "./models/index.js";
+export { ModelRegistry } from "./models/index.js";
// Credentials
export * from "./credentials/index.js";
diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts
index e5efcc8..cf59749 100644
--- a/packages/core/src/models/index.ts
+++ b/packages/core/src/models/index.ts
@@ -1,2 +1 @@
export { ModelRegistry } from "./registry.js";
-export { ModelResolver } from "./resolver.js";
diff --git a/packages/core/src/models/registry.ts b/packages/core/src/models/registry.ts
index f78fe0e..9ea0b32 100644
--- a/packages/core/src/models/registry.ts
+++ b/packages/core/src/models/registry.ts
@@ -1,25 +1,20 @@
-import type { KeyDefinition, KeyState, ModelDefinition } from "../types/index.js";
+import type { KeyDefinition, KeyState } from "../types/index.js";
export class ModelRegistry {
- private models: ModelDefinition[];
private keyStates: Map<string, KeyState>;
- private fallbackOrder: string[];
+ private keyOrder: string[];
- constructor(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]) {
- this.models = [];
+ constructor(keys: KeyDefinition[]) {
this.keyStates = new Map();
- this.fallbackOrder = [];
- this._initConfig(models, keys, fallbackOrder, new Map());
+ this.keyOrder = [];
+ this._initConfig(keys, new Map());
}
private _initConfig(
- models: ModelDefinition[],
keys: KeyDefinition[],
- fallbackOrder: string[],
existingStates: Map<string, KeyState>,
): void {
- this.models = [...models];
- this.fallbackOrder = this._buildFallbackOrder(keys, fallbackOrder);
+ this.keyOrder = keys.map((k) => k.id);
const newStates = new Map<string, KeyState>();
for (const key of keys) {
@@ -34,51 +29,12 @@ export class ModelRegistry {
this.keyStates = newStates;
}
- private _buildFallbackOrder(keys: KeyDefinition[], fallbackOrder: string[]): string[] {
- const ordered: string[] = [];
- const keyIds = new Set(keys.map((k) => k.id));
-
- // Add keys from fallbackOrder first (if they exist)
- for (const id of fallbackOrder) {
- if (keyIds.has(id) && !ordered.includes(id)) {
- ordered.push(id);
- }
- }
-
- // Append remaining keys not in fallbackOrder
- for (const key of keys) {
- if (!ordered.includes(key.id)) {
- ordered.push(key.id);
- }
- }
-
- return ordered;
- }
-
- getModels(): ModelDefinition[] {
- return [...this.models];
- }
-
getKeys(): KeyState[] {
- return this.fallbackOrder
+ return this.keyOrder
.map((id) => this.keyStates.get(id))
.filter((state): state is KeyState => state !== undefined);
}
- getModelsByTag(tag: string): ModelDefinition[] {
- return this.models.filter((m) => m.tags.includes(tag));
- }
-
- getAllTags(): string[] {
- const tags = new Set<string>();
- for (const model of this.models) {
- for (const tag of model.tags) {
- tags.add(tag);
- }
- }
- return [...tags];
- }
-
markKeyExhausted(keyId: string, error?: string): void {
const state = this.keyStates.get(keyId);
if (!state) return;
@@ -118,13 +74,13 @@ export class ModelRegistry {
return true;
}
- updateConfig(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]): void {
- this._initConfig(models, keys, fallbackOrder, this.keyStates);
+ updateConfig(keys: KeyDefinition[]): void {
+ this._initConfig(keys, this.keyStates);
}
// Internal: get ordered key states for a specific provider
getOrderedKeysForProvider(provider: string): KeyState[] {
- return this.fallbackOrder
+ return this.keyOrder
.map((id) => this.keyStates.get(id))
.filter(
(state): state is KeyState =>
diff --git a/packages/core/src/models/resolver.ts b/packages/core/src/models/resolver.ts
deleted file mode 100644
index 3ada713..0000000
--- a/packages/core/src/models/resolver.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import type { ResolvedModel } from "../types/index.js";
-import type { ModelRegistry } from "./registry.js";
-
-export class ModelResolver {
- private registry: ModelRegistry;
-
- constructor(registry: ModelRegistry) {
- this.registry = registry;
- }
-
- resolve(tag: string): ResolvedModel | null {
- const models = this.registry.getModelsByTag(tag);
- const keys = this.registry.getKeys();
-
- for (const keyState of keys) {
- if (keyState.status !== "active") continue;
- const model = models.find((m) => m.provider === keyState.definition.provider);
- if (model) {
- return { model, key: keyState.definition };
- }
- }
-
- return null;
- }
-
- async waitForKey(
- tag: string,
- options?: {
- pollIntervalMs?: number;
- signal?: AbortSignal;
- onWaiting?: () => void;
- onResume?: () => void;
- },
- ): Promise<ResolvedModel | null> {
- const pollIntervalMs = options?.pollIntervalMs ?? 60000;
- const signal = options?.signal;
-
- // Try immediately first
- const immediate = this.resolve(tag);
- if (immediate) return immediate;
-
- // Check if aborted before entering wait state
- if (signal?.aborted) return null;
-
- options?.onWaiting?.();
-
- return new Promise<ResolvedModel | null>((resolve) => {
- let timer: ReturnType<typeof setTimeout> | null = null;
-
- const cleanup = () => {
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- };
-
- const onAbort = () => {
- cleanup();
- resolve(null);
- };
-
- if (signal) {
- signal.addEventListener("abort", onAbort, { once: true });
- }
-
- const poll = () => {
- if (signal?.aborted) {
- resolve(null);
- return;
- }
-
- const result = this.resolve(tag);
- if (result) {
- if (signal) {
- signal.removeEventListener("abort", onAbort);
- }
- options?.onResume?.();
- resolve(result);
- return;
- }
-
- timer = setTimeout(poll, pollIntervalMs);
- };
-
- timer = setTimeout(poll, pollIntervalMs);
- });
- }
-}
diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts
index 6977564..f5dafbb 100644
--- a/packages/core/src/types/index.ts
+++ b/packages/core/src/types/index.ts
@@ -77,28 +77,10 @@ export interface AgentConfig {
// ─── Config Types (dispatch.toml) ────────────────────────────────
export interface DispatchConfig {
- agents?: Record<string, AgentTemplate>;
- models?: ModelDefinition[];
keys?: KeyDefinition[];
- fallback?: string[];
permissions: Record<string, string | Record<string, string>>;
}
-export interface AgentTemplate {
- name: string;
- description: string;
- system_prompt: string;
- tools: string[];
- permissions: Record<string, string | Record<string, string>>;
- model_tag: string;
-}
-
-export interface ModelDefinition {
- id: string;
- provider: string;
- tags: string[];
-}
-
export interface KeyDefinition {
id: string;
provider: string;
@@ -108,13 +90,6 @@ export interface KeyDefinition {
credentials_file?: string;
}
-// ─── Model Resolution ────────────────────────────────────────────
-
-export interface ResolvedModel {
- model: ModelDefinition;
- key: KeyDefinition;
-}
-
export type KeyStatus = "active" | "exhausted";
export interface KeyState {
diff --git a/packages/frontend/src/App.svelte b/packages/frontend/src/App.svelte
index 51fbd00..4ea968c 100644
--- a/packages/frontend/src/App.svelte
+++ b/packages/frontend/src/App.svelte
@@ -9,14 +9,12 @@ import HotReloadIndicator from "./lib/components/HotReloadIndicator.svelte";
import { chatStore } from "./lib/chat.svelte.js";
import { wsClient } from "./lib/ws.svelte.js";
import { config } from "./lib/config.js";
-import type { KeyInfo, ModelInfo } from "./lib/types.js";
+import type { KeyInfo } from "./lib/types.js";
const STORAGE_KEY = "dispatch-theme";
-let modelsData = $state<{ models: ModelInfo[]; keys: KeyInfo[]; tags: string[] }>({
- models: [],
+let modelsData = $state<{ keys: KeyInfo[] }>({
keys: [],
- tags: [],
});
let sidebarOpen = $state(true);
@@ -27,9 +25,7 @@ async function fetchModels() {
if (!res.ok) return;
const data = await res.json();
modelsData = {
- models: data.models ?? [],
keys: data.keys ?? [],
- tags: data.tags ? (Array.isArray(data.tags) ? data.tags : Object.keys(data.tags)) : [],
};
} catch {
// ignore fetch errors
@@ -83,10 +79,8 @@ onMount(() => {
class="w-80 flex-1 min-h-0 overflow-y-auto bg-base-100 border-l border-base-300 px-2 py-2 flex flex-col gap-2 [&>*]:shrink-0 transition-transform duration-300 ease-out"
style="transform: translateX({sidebarOpen ? '0' : '100%'})"
>
- <SidebarPanel
- models={modelsData.models}
- keys={modelsData.keys}
- tags={modelsData.tags}
+ <SidebarPanel
+ keys={modelsData.keys}
tasks={chatStore.tasks}
permissionLog={chatStore.permissionLog}
apiBase={config.apiBase}
diff --git a/packages/frontend/src/lib/components/ModelStatus.svelte b/packages/frontend/src/lib/components/ModelStatus.svelte
index b2b6902..a627de8 100644
--- a/packages/frontend/src/lib/components/ModelStatus.svelte
+++ b/packages/frontend/src/lib/components/ModelStatus.svelte
@@ -7,12 +7,6 @@
exhaustedAt: number | null;
}
- interface ModelInfo {
- id: string;
- provider: string;
- tags: string[];
- }
-
interface CredentialStatus {
keyId: string;
provider: string;
@@ -23,15 +17,11 @@
}
const {
- models = [],
keys = [],
- tags = [],
currentModel,
apiBase = "",
}: {
- models?: ModelInfo[];
keys?: KeyInfo[];
- tags?: string[];
currentModel?: string;
apiBase?: string;
} = $props();
@@ -42,8 +32,6 @@
const allExhausted = $derived(totalKeys > 0 && activeKeys === 0);
const someExhausted = $derived(totalKeys > 0 && activeKeys < totalKeys && activeKeys > 0);
- const uniqueTags = $derived([...new Set(tags)]);
-
let credentialStatus = $state<Record<string, CredentialStatus>>({});
let importingKey = $state<string | null>(null);
let importError = $state<string | null>(null);
@@ -158,7 +146,7 @@
</script>
<div class="flex flex-col gap-3">
- {#if models.length === 0 && keys.length === 0}
+ {#if keys.length === 0}
<p class="text-xs text-base-content/50">
No models configured. Using environment defaults.
</p>
@@ -191,18 +179,6 @@
</div>
{/if}
- <!-- Tags -->
- {#if uniqueTags.length > 0}
- <div class="flex flex-col gap-1">
- <p class="text-xs text-base-content/50 uppercase tracking-wide">Tags</p>
- <div class="flex flex-wrap gap-1">
- {#each uniqueTags as tag (tag)}
- <span class="badge badge-outline badge-xs">{tag}</span>
- {/each}
- </div>
- </div>
- {/if}
-
<!-- Import error/success banners -->
{#if importError}
<div role="alert" class="text-xs text-error/80">{importError}</div>
diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte
index 79eb73b..e5536de 100644
--- a/packages/frontend/src/lib/components/SidebarPanel.svelte
+++ b/packages/frontend/src/lib/components/SidebarPanel.svelte
@@ -7,12 +7,10 @@
import PermissionLog from "./PermissionLog.svelte";
import KeyUsage from "./KeyUsage.svelte";
import ClaudeReset from "./ClaudeReset.svelte";
- import type { TaskItem, LogEntry, KeyInfo, ModelInfo } from "../types.js";
+ import type { TaskItem, LogEntry, KeyInfo } from "../types.js";
const {
- models = [],
keys = [],
- tags = [],
tasks = [],
permissionLog = [],
apiBase = "",
@@ -23,9 +21,7 @@
onModelChange,
onReasoningChange,
}: {
- models?: ModelInfo[];
keys?: KeyInfo[];
- tags?: string[];
tasks?: TaskItem[];
permissionLog?: LogEntry[];
apiBase?: string;
@@ -109,7 +105,7 @@
{:else if panel.selected === "Claude Reset"}
<ClaudeReset {apiBase} />
{:else if panel.selected === "Model Status"}
- <ModelStatus {models} {keys} {tags} {apiBase} />
+ <ModelStatus {keys} {apiBase} />
{:else if panel.selected === "Tasks"}
<TaskListPanel {tasks} />
{:else if panel.selected === "Config"}
diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts
index 559742d..28752a0 100644
--- a/packages/frontend/src/lib/types.ts
+++ b/packages/frontend/src/lib/types.ts
@@ -94,12 +94,6 @@ export interface KeyInfo {
exhaustedAt: number | null;
}
-export interface ModelInfo {
- id: string;
- provider: string;
- tags: string[];
-}
-
export interface LogEntry {
id: string;
permission: string;