summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/models/registry.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
committerAdam Malczewski <[email protected]>2026-05-20 15:04:26 +0900
commitadc8bd185b54935e7a31aae04da3175b7989927a (patch)
tree031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/core/src/models/registry.ts
parent52af4ca33b1f83b8f863a6267d447944f55e729d (diff)
downloaddispatch-adc8bd185b54935e7a31aae04da3175b7989927a.tar.gz
dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.zip
feat: phase 3 — config, skills, model groups, task list, and sidebar UI
- Config system: TOML-based dispatch.toml with hot-reload via chokidar - Model/key resolution: tag-based model selection, key fallback chains - Skills system: directory loader with TOML frontmatter, agent mappings - Task list tool: add/update/list/get operations with WebSocket events - API routes: GET /config, /skills, /skills/:name, /models, /models/resolve - Frontend: sidebar with model status, task list, config viewer, skills browser, permission log - Sliding sidebar animation using CSS transitions (not Svelte transitions)
Diffstat (limited to 'packages/core/src/models/registry.ts')
-rw-r--r--packages/core/src/models/registry.ts134
1 files changed, 134 insertions, 0 deletions
diff --git a/packages/core/src/models/registry.ts b/packages/core/src/models/registry.ts
new file mode 100644
index 0000000..f78fe0e
--- /dev/null
+++ b/packages/core/src/models/registry.ts
@@ -0,0 +1,134 @@
+import type { KeyDefinition, KeyState, ModelDefinition } from "../types/index.js";
+
+export class ModelRegistry {
+ private models: ModelDefinition[];
+ private keyStates: Map<string, KeyState>;
+ private fallbackOrder: string[];
+
+ constructor(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]) {
+ this.models = [];
+ this.keyStates = new Map();
+ this.fallbackOrder = [];
+ this._initConfig(models, keys, fallbackOrder, new Map());
+ }
+
+ private _initConfig(
+ models: ModelDefinition[],
+ keys: KeyDefinition[],
+ fallbackOrder: string[],
+ existingStates: Map<string, KeyState>,
+ ): void {
+ this.models = [...models];
+ this.fallbackOrder = this._buildFallbackOrder(keys, fallbackOrder);
+
+ const newStates = new Map<string, KeyState>();
+ for (const key of keys) {
+ const existing = existingStates.get(key.id);
+ if (existing) {
+ // Preserve existing state but update definition
+ newStates.set(key.id, { ...existing, definition: key });
+ } else {
+ newStates.set(key.id, { definition: key, status: "active" });
+ }
+ }
+ 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
+ .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;
+ this.keyStates.set(keyId, {
+ ...state,
+ status: "exhausted",
+ lastError: error,
+ exhaustedAt: Date.now(),
+ });
+ }
+
+ markKeyActive(keyId: string): void {
+ const state = this.keyStates.get(keyId);
+ if (!state) return;
+ const updated: KeyState = {
+ definition: state.definition,
+ status: "active",
+ };
+ this.keyStates.set(keyId, updated);
+ }
+
+ hasAvailableKey(provider: string): boolean {
+ for (const state of this.keyStates.values()) {
+ if (state.definition.provider === provider && state.status === "active") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ allKeysExhausted(): boolean {
+ for (const state of this.keyStates.values()) {
+ if (state.status === "active") {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ updateConfig(models: ModelDefinition[], keys: KeyDefinition[], fallbackOrder: string[]): void {
+ this._initConfig(models, keys, fallbackOrder, this.keyStates);
+ }
+
+ // Internal: get ordered key states for a specific provider
+ getOrderedKeysForProvider(provider: string): KeyState[] {
+ return this.fallbackOrder
+ .map((id) => this.keyStates.get(id))
+ .filter(
+ (state): state is KeyState =>
+ state !== undefined && state.definition.provider === provider,
+ );
+ }
+}