@wrongstack/webui-server 0.298.1 → 0.298.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -15665,7 +15665,8 @@ function createProviderOperations(deps2) {
15665
15665
  providerId,
15666
15666
  config
15667
15667
  );
15668
- const siblingId = SIBLING_CATALOG[providerId];
15668
+ const siblingCatalogKey = config?.family ?? providerId;
15669
+ const siblingId = SIBLING_CATALOG[siblingCatalogKey];
15669
15670
  const sibling = siblingId && siblingId !== providerId ? await deps2.modelsRegistry.getProvider(siblingId).catch(() => void 0) : void 0;
15670
15671
  let models = resolveProviderModelList(
15671
15672
  config?.models,
@@ -15820,6 +15821,50 @@ function createProviderOperations(deps2) {
15820
15821
  sendOperationResult(ws, false, errMessage(err));
15821
15822
  }
15822
15823
  }
15824
+ async function handleCustomModelSet(ws, providerId, modelId, definition) {
15825
+ try {
15826
+ const providers = await loadConfigProviders();
15827
+ const cfg = Object.hasOwn(providers, providerId) ? providers[providerId] : void 0;
15828
+ if (!cfg) {
15829
+ sendOperationResult(ws, false, `Unknown provider "${providerId}"`);
15830
+ return;
15831
+ }
15832
+ if (!cfg.customModels) cfg.customModels = {};
15833
+ cfg.customModels[modelId] = definition;
15834
+ if (!cfg.models) cfg.models = [];
15835
+ if (!cfg.models.includes(modelId)) cfg.models.push(modelId);
15836
+ await saveConfigProviders(providers);
15837
+ sendOperationResult(ws, true, `Saved model "${modelId}" for ${providerId}`);
15838
+ broadcastSaved(providers);
15839
+ } catch (err) {
15840
+ sendOperationResult(ws, false, errMessage(err));
15841
+ }
15842
+ }
15843
+ async function handleCustomModelRemove(ws, providerId, modelId) {
15844
+ try {
15845
+ const providers = await loadConfigProviders();
15846
+ const cfg = Object.hasOwn(providers, providerId) ? providers[providerId] : void 0;
15847
+ if (!cfg) {
15848
+ sendOperationResult(ws, false, `Unknown provider "${providerId}"`);
15849
+ return;
15850
+ }
15851
+ if (cfg.customModels && Object.hasOwn(cfg.customModels, modelId)) {
15852
+ delete cfg.customModels[modelId];
15853
+ if (Object.keys(cfg.customModels).length === 0) delete cfg.customModels;
15854
+ if (cfg.models) {
15855
+ cfg.models = cfg.models.filter((m) => m !== modelId);
15856
+ if (cfg.models.length === 0) delete cfg.models;
15857
+ }
15858
+ await saveConfigProviders(providers);
15859
+ sendOperationResult(ws, true, `Removed model "${modelId}" from ${providerId}`);
15860
+ broadcastSaved(providers);
15861
+ } else {
15862
+ sendOperationResult(ws, false, `Model "${modelId}" not found in ${providerId}`);
15863
+ }
15864
+ } catch (err) {
15865
+ sendOperationResult(ws, false, errMessage(err));
15866
+ }
15867
+ }
15823
15868
  async function handleProviderUndoClear(ws, providerId, previousModels) {
15824
15869
  try {
15825
15870
  const providers = await loadConfigProviders();
@@ -15895,7 +15940,7 @@ function createProviderOperations(deps2) {
15895
15940
  const p = existing ? { ...existing } : { type: providerId };
15896
15941
  p.family = outcome.family;
15897
15942
  if (!p.baseUrl) p.baseUrl = outcome.baseUrl;
15898
- p.models = [...outcome.models];
15943
+ if (outcome.models.length > 0) p.models = [...outcome.models];
15899
15944
  const keys = normalizeKeys(p).filter((k) => k.label !== outcome.apiKey.label);
15900
15945
  keys.push(outcome.apiKey);
15901
15946
  writeKeysBack(p, keys);
@@ -16003,6 +16048,8 @@ function createProviderOperations(deps2) {
16003
16048
  handleProviderAdd,
16004
16049
  handleProviderRemove,
16005
16050
  handleProviderClearModels,
16051
+ handleCustomModelSet,
16052
+ handleCustomModelRemove,
16006
16053
  handleProviderUndoClear,
16007
16054
  handleProviderUpdate,
16008
16055
  handleProviderProbe,
@@ -16339,6 +16386,8 @@ var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
16339
16386
  "prefs.update",
16340
16387
  "provider.add",
16341
16388
  "provider.clear_models",
16389
+ "provider.custom_models.remove",
16390
+ "provider.custom_models.set",
16342
16391
  "provider.models",
16343
16392
  "provider.models.search",
16344
16393
  "provider.probe",
@@ -18202,10 +18251,14 @@ async function handleProjectRoute(ws, msg, handlers) {
18202
18251
  }
18203
18252
 
18204
18253
  // src/server/provider-routes.ts
18254
+ import { modelsDevModelSchema } from "@wrongstack/core/models";
18205
18255
  function asPayloadRecord(msg) {
18206
18256
  const payload = msg.payload;
18207
18257
  return typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : null;
18208
18258
  }
18259
+ function isRecord4(value) {
18260
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18261
+ }
18209
18262
  function requiredString(payload, key) {
18210
18263
  const value = payload[key];
18211
18264
  return typeof value === "string" && value.trim().length > 0 ? value : null;
@@ -18228,7 +18281,7 @@ var CUSTOM_MODEL_BOOLEAN_CAPS = /* @__PURE__ */ new Set([
18228
18281
  "streaming",
18229
18282
  "jsonMode"
18230
18283
  ]);
18231
- var INLINE_DEFINITION_KEYS = /* @__PURE__ */ new Set(["name", "maxOutput", "capabilities", "provider"]);
18284
+ var INLINE_DEFINITION_KEYS = /* @__PURE__ */ new Set(["name", "maxOutput", "capabilities", "provider", "modelsDev"]);
18232
18285
  function optionalCustomModels(payload) {
18233
18286
  const value = payload["customModels"];
18234
18287
  if (value === void 0) return void 0;
@@ -18267,11 +18320,21 @@ function optionalCustomModels(payload) {
18267
18320
  }
18268
18321
  capabilities = built;
18269
18322
  }
18323
+ const rawMd = definition["modelsDev"];
18324
+ let validatedModelsDev;
18325
+ if (rawMd !== void 0) {
18326
+ if (!isRecord4(rawMd)) return null;
18327
+ const parsed = modelsDevModelSchema.safeParse({ ...rawMd, id: modelId });
18328
+ if (!parsed.success) return null;
18329
+ const { id: _parsedId, ...validMd } = parsed.data;
18330
+ validatedModelsDev = validMd;
18331
+ }
18270
18332
  out[modelId] = {
18271
18333
  ...typeof definition["name"] === "string" ? { name: definition["name"] } : {},
18272
18334
  ...typeof definition["provider"] === "string" ? { provider: definition["provider"] } : {},
18273
18335
  ...typeof definition["maxOutput"] === "number" ? { maxOutput: definition["maxOutput"] } : {},
18274
- ...capabilities ? { capabilities } : {}
18336
+ ...capabilities ? { capabilities } : {},
18337
+ ...validatedModelsDev ? { modelsDev: validatedModelsDev } : {}
18275
18338
  };
18276
18339
  }
18277
18340
  return out;
@@ -18375,6 +18438,30 @@ async function handleProviderRoute(ws, msg, routes) {
18375
18438
  await routes.providerHandlers.handleProviderClearModels(ws, providerId);
18376
18439
  return true;
18377
18440
  }
18441
+ case "provider.custom_models.set": {
18442
+ const payload = asPayloadRecord(msg);
18443
+ const providerId = payload ? requiredString(payload, "providerId") : null;
18444
+ const modelId = payload ? requiredString(payload, "modelId") : null;
18445
+ if (!payload || !providerId || !SAFE_CONFIG_KEY.test(providerId) || !modelId || !SAFE_CONFIG_KEY.test(modelId)) {
18446
+ return invalidPayload(ws, msg.type);
18447
+ }
18448
+ const customModelRaw = payload["customModel"];
18449
+ if (!isRecord4(customModelRaw)) return invalidPayload(ws, msg.type);
18450
+ const cm = optionalCustomModels({ customModels: { [modelId]: customModelRaw } });
18451
+ if (!cm || !cm[modelId]) return invalidPayload(ws, msg.type);
18452
+ await routes.providerHandlers.handleCustomModelSet(ws, providerId, modelId, cm[modelId]);
18453
+ return true;
18454
+ }
18455
+ case "provider.custom_models.remove": {
18456
+ const payload = asPayloadRecord(msg);
18457
+ const providerId = payload ? requiredString(payload, "providerId") : null;
18458
+ const modelId = payload ? requiredString(payload, "modelId") : null;
18459
+ if (!payload || !providerId || !SAFE_CONFIG_KEY.test(providerId) || !modelId || !SAFE_CONFIG_KEY.test(modelId)) {
18460
+ return invalidPayload(ws, msg.type);
18461
+ }
18462
+ await routes.providerHandlers.handleCustomModelRemove(ws, providerId, modelId);
18463
+ return true;
18464
+ }
18378
18465
  case "provider.undo_clear": {
18379
18466
  const payload = asPayloadRecord(msg);
18380
18467
  const providerId = payload ? requiredString(payload, "providerId") : null;
@@ -1,3 +1,3 @@
1
1
  export declare const CLIENT_WORKSPACE_MESSAGE_TYPES: readonly ['files.list', 'files.read', 'files.tree', 'files.write', 'git.changes', 'git.diff', 'git.info', 'projects.add', 'projects.list', 'projects.select', 'working_dir.set', 'worktree.cleanup', 'worktree.diff', 'worktree.merge', 'worktree.remove', 'worktree.scan', 'shell.open', 'process.kill', 'process.killAll', 'process.list', 'terminal.close', 'terminal.create', 'terminal.input', 'terminal.resize'];
2
- export declare const CLIENT_CONFIGURATION_MESSAGE_TYPES: readonly ['codebase.index.server.shutdown', 'connections.health', 'connections.service_action', 'diag.get', 'key.add', 'key.delete', 'key.set_active', 'key.update', 'prefs.get', 'prefs.update', 'provider.add', 'provider.clear_models', 'provider.models', 'provider.models.search', 'provider.probe', 'provider.remove', 'provider.status.clear', 'provider.status.get', 'provider.status.retry', 'provider.undo_clear', 'provider.update', 'providers.list', 'providers.saved', 'tool.disable', 'tool.enable', 'tools.list', 'webui.shutdown'];
2
+ export declare const CLIENT_CONFIGURATION_MESSAGE_TYPES: readonly ['codebase.index.server.shutdown', 'connections.health', 'connections.service_action', 'diag.get', 'key.add', 'key.delete', 'key.set_active', 'key.update', 'prefs.get', 'prefs.update', 'provider.add', 'provider.clear_models', 'provider.custom_models.remove', 'provider.custom_models.set', 'provider.models', 'provider.models.search', 'provider.probe', 'provider.remove', 'provider.status.clear', 'provider.status.get', 'provider.status.retry', 'provider.undo_clear', 'provider.update', 'providers.list', 'providers.saved', 'tool.disable', 'tool.enable', 'tools.list', 'webui.shutdown'];
3
3
  //# sourceMappingURL=client-workspace.d.ts.map
@@ -289,6 +289,8 @@ var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
289
289
  "prefs.update",
290
290
  "provider.add",
291
291
  "provider.clear_models",
292
+ "provider.custom_models.remove",
293
+ "provider.custom_models.set",
292
294
  "provider.models",
293
295
  "provider.models.search",
294
296
  "provider.probe",
@@ -1,4 +1,4 @@
1
- export declare const CLIENT_MESSAGE_TYPES: readonly ["abort", "ping", "user_message", "tool.confirm_result", "completion.request", "model.switch", "model.refine", "autonomy.switch", "context.clear", "context.compact", "context.debug", "context.editor.open", "context.editor.validate", "context.editor.apply", "context.mode.create", "context.mode.delete", "context.mode.switch", "context.mode.update", "context.modes.list", "context.repair", "mode.switch", "modes.list", "session.checkpoints", "session.delete", "session.new", "session.rename", "session.resume", "session.rewind", "session.save", "sessions.list", "side_effects.list", "stats.get", "todo.update", "todos.clear", "todos.get", "todos.remove", "collab.join", "collab.leave", "collab.annotate", "collab.resolve", "collab.request_pause", "collab.resume", "collab.grant_control", "collab.inject_tool", "mailbox.action", "mailbox.agents", "mailbox.clear", "mailbox.compact", "mailbox.messages", "mailbox.purge", "mailbox.send", "files.list", "files.read", "files.tree", "files.write", "git.changes", "git.diff", "git.info", "projects.add", "projects.list", "projects.select", "working_dir.set", "worktree.cleanup", "worktree.diff", "worktree.merge", "worktree.remove", "worktree.scan", "shell.open", "process.kill", "process.killAll", "process.list", "terminal.close", "terminal.create", "terminal.input", "terminal.resize", "codebase.index.server.shutdown", "connections.health", "connections.service_action", "diag.get", "key.add", "key.delete", "key.set_active", "key.update", "prefs.get", "prefs.update", "provider.add", "provider.clear_models", "provider.models", "provider.models.search", "provider.probe", "provider.remove", "provider.status.clear", "provider.status.get", "provider.status.retry", "provider.undo_clear", "provider.update", "providers.list", "providers.saved", "tool.disable", "tool.enable", "tools.list", "webui.shutdown", "goal-state.get", "goal.addTask", "goal.assess", "goal.assignTask", "goal.clear", "goal.get", "goal.list", "goal.load", "goal.moveTask", "goal.pause", "goal.resume", "goal.retryTask", "goal.revert", "goal.runTask", "goal.save", "goal.selectPhase", "goal.start", "goal.state", "goal.status", "goal.stop", "goal.taskStatus", "goal.toggleAutonomous", "plan.get", "plan.item.update", "plan.template_use", "task.update", "tasks.get", "sdd.board.cancel_task", "sdd.board.cleanup_worktrees", "sdd.board.delete_task", "sdd.board.destroy", "sdd.board.get", "sdd.board.list", "sdd.board.pause", "sdd.board.reassign", "sdd.board.resume", "sdd.board.retry", "sdd.board.retry_all_failed", "sdd.board.rollback", "sdd.board.set_task_fallbacks", "sdd.board.set_task_model", "sdd.board.set_task_verification", "sdd.board.split_task", "sdd.board.stop", "sdd.run.from_graph", "sdd.run.from_spec", "sdd.run.start", "sdd.spec.approve", "sdd.spec.discard", "sdd.spec.get", "sdd.spec.message", "sdd.spec.start", "specs.get", "specs.list", "specs.taskStatus", "brain.ask", "brain.config.get", "brain.config.set", "brain.risk", "brain.status", "chronicle.facet", "chronicle.facets", "chronicle.graph", "chronicle.metrics", "chronicle.query", "chronicle.status", "config.doctor", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.update", "auth.oauth.cancel", "auth.oauth.code", "auth.oauth.start", "mcp.add", "mcp.disable", "mcp.discover", "mcp.enable", "mcp.list", "mcp.prompt.get", "mcp.prompts", "mcp.remove", "mcp.resource.read", "mcp.resources", "mcp.restart", "mcp.sleep", "mcp.update", "mcp.wake", "prompts.content", "prompts.create", "prompts.favorite", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.create", "skills.edit", "skills.export", "skills.install", "skills.list", "skills.uninstall", "skills.update"];
1
+ export declare const CLIENT_MESSAGE_TYPES: readonly ["abort", "ping", "user_message", "tool.confirm_result", "completion.request", "model.switch", "model.refine", "autonomy.switch", "context.clear", "context.compact", "context.debug", "context.editor.open", "context.editor.validate", "context.editor.apply", "context.mode.create", "context.mode.delete", "context.mode.switch", "context.mode.update", "context.modes.list", "context.repair", "mode.switch", "modes.list", "session.checkpoints", "session.delete", "session.new", "session.rename", "session.resume", "session.rewind", "session.save", "sessions.list", "side_effects.list", "stats.get", "todo.update", "todos.clear", "todos.get", "todos.remove", "collab.join", "collab.leave", "collab.annotate", "collab.resolve", "collab.request_pause", "collab.resume", "collab.grant_control", "collab.inject_tool", "mailbox.action", "mailbox.agents", "mailbox.clear", "mailbox.compact", "mailbox.messages", "mailbox.purge", "mailbox.send", "files.list", "files.read", "files.tree", "files.write", "git.changes", "git.diff", "git.info", "projects.add", "projects.list", "projects.select", "working_dir.set", "worktree.cleanup", "worktree.diff", "worktree.merge", "worktree.remove", "worktree.scan", "shell.open", "process.kill", "process.killAll", "process.list", "terminal.close", "terminal.create", "terminal.input", "terminal.resize", "codebase.index.server.shutdown", "connections.health", "connections.service_action", "diag.get", "key.add", "key.delete", "key.set_active", "key.update", "prefs.get", "prefs.update", "provider.add", "provider.clear_models", "provider.custom_models.remove", "provider.custom_models.set", "provider.models", "provider.models.search", "provider.probe", "provider.remove", "provider.status.clear", "provider.status.get", "provider.status.retry", "provider.undo_clear", "provider.update", "providers.list", "providers.saved", "tool.disable", "tool.enable", "tools.list", "webui.shutdown", "goal-state.get", "goal.addTask", "goal.assess", "goal.assignTask", "goal.clear", "goal.get", "goal.list", "goal.load", "goal.moveTask", "goal.pause", "goal.resume", "goal.retryTask", "goal.revert", "goal.runTask", "goal.save", "goal.selectPhase", "goal.start", "goal.state", "goal.status", "goal.stop", "goal.taskStatus", "goal.toggleAutonomous", "plan.get", "plan.item.update", "plan.template_use", "task.update", "tasks.get", "sdd.board.cancel_task", "sdd.board.cleanup_worktrees", "sdd.board.delete_task", "sdd.board.destroy", "sdd.board.get", "sdd.board.list", "sdd.board.pause", "sdd.board.reassign", "sdd.board.resume", "sdd.board.retry", "sdd.board.retry_all_failed", "sdd.board.rollback", "sdd.board.set_task_fallbacks", "sdd.board.set_task_model", "sdd.board.set_task_verification", "sdd.board.split_task", "sdd.board.stop", "sdd.run.from_graph", "sdd.run.from_spec", "sdd.run.start", "sdd.spec.approve", "sdd.spec.discard", "sdd.spec.get", "sdd.spec.message", "sdd.spec.start", "specs.get", "specs.list", "specs.taskStatus", "brain.ask", "brain.config.get", "brain.config.set", "brain.risk", "brain.status", "chronicle.facet", "chronicle.facets", "chronicle.graph", "chronicle.metrics", "chronicle.query", "chronicle.status", "config.doctor", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.update", "auth.oauth.cancel", "auth.oauth.code", "auth.oauth.start", "mcp.add", "mcp.disable", "mcp.discover", "mcp.enable", "mcp.list", "mcp.prompt.get", "mcp.prompts", "mcp.remove", "mcp.resource.read", "mcp.resources", "mcp.restart", "mcp.sleep", "mcp.update", "mcp.wake", "prompts.content", "prompts.create", "prompts.favorite", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.create", "skills.edit", "skills.export", "skills.install", "skills.list", "skills.uninstall", "skills.update"];
2
2
  export declare const SERVER_MESSAGE_TYPES: readonly ["error", "log", "pong", "side_effects", "agent.status_changed", "agent.timeline.message", "client.status_update", "chimera.report_available", "compaction.failed", "completion.result", "context.compacted", "context.debug", "context.editor.snapshot", "context.editor.validation", "context.editor.applied", "context.mode.changed", "context.modes.list", "context.repaired", "ctx.max_context", "ctx.pct", "delegate.completed", "delegate.started", "iteration.completed", "iteration.limit_reached", "iteration.started", "model.refine_result", "modes.list", "provider.active_blocked", "provider.error", "provider.fallback", "provider.response", "provider.retry", "provider.status_changed", "provider.stream_error", "provider.text_delta", "provider.thinking_delta", "run.result", "session.checkpoints", "session.damaged", "session.end", "session.rewound", "session.start", "session.stats", "sessions.list", "sessions.status_update", "stats.get", "token.cost_estimate_unavailable", "token.threshold", "tool.confirm_needed", "tool.disabled", "tool.enabled", "tool.executed", "tool.loop_detected", "tool.progress", "tool.started", "tools.list", "trust.persisted", "collab.annotation.added", "collab.annotation.resolved", "collab.event", "collab.injection.granted", "collab.participant.joined", "collab.participant.left", "collab.pause.granted", "collab.pause.released", "collab.state", "mailbox.action_result", "mailbox.agent_registered", "mailbox.agents", "mailbox.cleared", "mailbox.compacted", "mailbox.event", "mailbox.messages", "mailbox.sent", "mailbox.purged", "mailbox.received", "subagent.budget_extended", "subagent.event", "checkpoint.written", "codemap.file_event", "codemap.index_updated", "codemap.tool_executed", "codemap.tool_started", "file.saved", "files.list", "files.read", "files.tree", "files.written", "git.changes", "git.diff", "git.info", "process.list", "projects.added", "projects.list", "projects.selected", "terminal.exit", "terminal.output", "working_dir.changed", "worktree.cleanup_result", "worktree.diff_result", "worktree.event", "worktree.merge_result", "worktree.orphans", "worktree.state", "auth.oauth.status", "codebase.index.server.shutdown_result", "connections.health_error", "connections.health_result", "connections.service_action_result", "diag.get", "key.operation_result", "model.switch_result", "prefs.updated", "provider.catalog", "provider.models", "provider.models.search_result", "provider.probe", "provider.status.snapshot", "providers.saved", "budget.decision", "budget.threshold_reached", "coordinator.stats", "coordinator.status", "eternal.iteration", "fleet.concurrency_update", "goal-state.updated", "goal.assess.result", "goal.list", "goal.paused", "goal.resumed", "goal.saved", "goal.error", "goal.stopped", "goal.failed", "goal.completed", "goal.cleared", "goal.reverted", "goal.progress", "goal.state", "in_flight.ended", "in_flight.started", "plan.updated", "task.completed", "task.failed", "task.pending", "task.started", "tasks.updated", "todos.cleared", "todos.updated", "kanban.task.activity", "sdd.board.lifecycle_result", "sdd.board.list", "sdd.board.snapshot", "sdd.run.started", "sdd.spec.agent_text", "sdd.spec.error", "sdd.spec.snapshot", "specs.detail", "specs.list", "consensus.vote_cast", "consensus.vote_initiated", "consensus.vote_resolved", "cron.job_fired", "cron.snapshot", "techstack.job.cancelled", "techstack.job.failed", "techstack.job.progress", "techstack.job.started", "techstack.report.delivered", "techstack.report.ready", "techstack.snapshot.updated", "techstack.workspace.completed", "brain.answer", "brain.config", "brain.event", "brain.status", "chronicle.error", "chronicle.facet_result", "chronicle.facets_result", "chronicle.graph_result", "chronicle.metrics_result", "chronicle.query_result", "chronicle.status_result", "config.doctor.result", "design.list", "design.materialize", "design.set", "design.state", "design.swap", "design.tune", "design.use", "design.verify", "memory.event", "memory.list", "memory.sage.backfillRecoverable", "memory.sage.candidateResolve", "memory.sage.delete", "memory.sage.forFile", "memory.sage.get", "memory.sage.graph", "memory.sage.list", "memory.sage.listPage", "memory.sage.recover", "memory.sage.remember", "memory.sage.update", "mcp.content.error", "mcp.content.selected", "mcp.list", "mcp.operation_result", "mcp.prompts", "mcp.resources", "mcp.server.added", "mcp.server.connected", "mcp.server.disconnected", "mcp.server.discovered", "mcp.server.error", "mcp.server.reconnected", "mcp.server.removed", "mcp.server.sleeping", "mcp.server.updated", "mcp.server.waking", "prompts.content", "prompts.created", "prompts.favorite", "prompts.list", "prompts.recent", "prompts.search", "prompts.used", "skills.content", "skills.created", "skills.edited", "skills.exported", "skills.installed", "skills.list", "skills.uninstalled", "skills.updated"];
3
3
  export type ExactClientMessageType = (typeof CLIENT_MESSAGE_TYPES)[number];
4
4
  export type ExactServerMessageType = (typeof SERVER_MESSAGE_TYPES)[number];
@@ -54,6 +54,8 @@ export declare function createEmbeddedProviderOperations(ctx: EmbeddedProviderCo
54
54
  }) => Promise<void>;
55
55
  handleProviderRemove: (ws: WebSocket, providerId: string) => Promise<void>;
56
56
  handleProviderClearModels: (ws: WebSocket, providerId: string) => Promise<void>;
57
+ handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
58
+ handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
57
59
  handleProviderUndoClear: (ws: WebSocket, providerId: string, previousModels: string[]) => Promise<void>;
58
60
  handleProviderUpdate: (ws: WebSocket, payload: {
59
61
  id: string;
@@ -13762,7 +13762,8 @@ function createProviderOperations(deps2) {
13762
13762
  providerId,
13763
13763
  config
13764
13764
  );
13765
- const siblingId = SIBLING_CATALOG[providerId];
13765
+ const siblingCatalogKey = config?.family ?? providerId;
13766
+ const siblingId = SIBLING_CATALOG[siblingCatalogKey];
13766
13767
  const sibling = siblingId && siblingId !== providerId ? await deps2.modelsRegistry.getProvider(siblingId).catch(() => void 0) : void 0;
13767
13768
  let models = resolveProviderModelList(
13768
13769
  config?.models,
@@ -13917,6 +13918,50 @@ function createProviderOperations(deps2) {
13917
13918
  sendOperationResult(ws, false, errMessage(err));
13918
13919
  }
13919
13920
  }
13921
+ async function handleCustomModelSet(ws, providerId, modelId, definition) {
13922
+ try {
13923
+ const providers = await loadConfigProviders();
13924
+ const cfg = Object.hasOwn(providers, providerId) ? providers[providerId] : void 0;
13925
+ if (!cfg) {
13926
+ sendOperationResult(ws, false, `Unknown provider "${providerId}"`);
13927
+ return;
13928
+ }
13929
+ if (!cfg.customModels) cfg.customModels = {};
13930
+ cfg.customModels[modelId] = definition;
13931
+ if (!cfg.models) cfg.models = [];
13932
+ if (!cfg.models.includes(modelId)) cfg.models.push(modelId);
13933
+ await saveConfigProviders(providers);
13934
+ sendOperationResult(ws, true, `Saved model "${modelId}" for ${providerId}`);
13935
+ broadcastSaved(providers);
13936
+ } catch (err) {
13937
+ sendOperationResult(ws, false, errMessage(err));
13938
+ }
13939
+ }
13940
+ async function handleCustomModelRemove(ws, providerId, modelId) {
13941
+ try {
13942
+ const providers = await loadConfigProviders();
13943
+ const cfg = Object.hasOwn(providers, providerId) ? providers[providerId] : void 0;
13944
+ if (!cfg) {
13945
+ sendOperationResult(ws, false, `Unknown provider "${providerId}"`);
13946
+ return;
13947
+ }
13948
+ if (cfg.customModels && Object.hasOwn(cfg.customModels, modelId)) {
13949
+ delete cfg.customModels[modelId];
13950
+ if (Object.keys(cfg.customModels).length === 0) delete cfg.customModels;
13951
+ if (cfg.models) {
13952
+ cfg.models = cfg.models.filter((m) => m !== modelId);
13953
+ if (cfg.models.length === 0) delete cfg.models;
13954
+ }
13955
+ await saveConfigProviders(providers);
13956
+ sendOperationResult(ws, true, `Removed model "${modelId}" from ${providerId}`);
13957
+ broadcastSaved(providers);
13958
+ } else {
13959
+ sendOperationResult(ws, false, `Model "${modelId}" not found in ${providerId}`);
13960
+ }
13961
+ } catch (err) {
13962
+ sendOperationResult(ws, false, errMessage(err));
13963
+ }
13964
+ }
13920
13965
  async function handleProviderUndoClear(ws, providerId, previousModels) {
13921
13966
  try {
13922
13967
  const providers = await loadConfigProviders();
@@ -13992,7 +14037,7 @@ function createProviderOperations(deps2) {
13992
14037
  const p = existing ? { ...existing } : { type: providerId };
13993
14038
  p.family = outcome.family;
13994
14039
  if (!p.baseUrl) p.baseUrl = outcome.baseUrl;
13995
- p.models = [...outcome.models];
14040
+ if (outcome.models.length > 0) p.models = [...outcome.models];
13996
14041
  const keys = normalizeKeys(p).filter((k) => k.label !== outcome.apiKey.label);
13997
14042
  keys.push(outcome.apiKey);
13998
14043
  writeKeysBack(p, keys);
@@ -14100,6 +14145,8 @@ function createProviderOperations(deps2) {
14100
14145
  handleProviderAdd,
14101
14146
  handleProviderRemove,
14102
14147
  handleProviderClearModels,
14148
+ handleCustomModelSet,
14149
+ handleCustomModelRemove,
14103
14150
  handleProviderUndoClear,
14104
14151
  handleProviderUpdate,
14105
14152
  handleProviderProbe,
@@ -14373,6 +14420,8 @@ var CLIENT_CONFIGURATION_MESSAGE_TYPES = [
14373
14420
  "prefs.update",
14374
14421
  "provider.add",
14375
14422
  "provider.clear_models",
14423
+ "provider.custom_models.remove",
14424
+ "provider.custom_models.set",
14376
14425
  "provider.models",
14377
14426
  "provider.models.search",
14378
14427
  "provider.probe",
@@ -15898,10 +15947,14 @@ async function handleProjectRoute(ws, msg, handlers) {
15898
15947
  }
15899
15948
 
15900
15949
  // src/server/provider-routes.ts
15950
+ import { modelsDevModelSchema } from "@wrongstack/core/models";
15901
15951
  function asPayloadRecord(msg) {
15902
15952
  const payload = msg.payload;
15903
15953
  return typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : null;
15904
15954
  }
15955
+ function isRecord4(value) {
15956
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15957
+ }
15905
15958
  function requiredString(payload, key) {
15906
15959
  const value = payload[key];
15907
15960
  return typeof value === "string" && value.trim().length > 0 ? value : null;
@@ -15924,7 +15977,7 @@ var CUSTOM_MODEL_BOOLEAN_CAPS = /* @__PURE__ */ new Set([
15924
15977
  "streaming",
15925
15978
  "jsonMode"
15926
15979
  ]);
15927
- var INLINE_DEFINITION_KEYS = /* @__PURE__ */ new Set(["name", "maxOutput", "capabilities", "provider"]);
15980
+ var INLINE_DEFINITION_KEYS = /* @__PURE__ */ new Set(["name", "maxOutput", "capabilities", "provider", "modelsDev"]);
15928
15981
  function optionalCustomModels(payload) {
15929
15982
  const value = payload["customModels"];
15930
15983
  if (value === void 0) return void 0;
@@ -15963,11 +16016,21 @@ function optionalCustomModels(payload) {
15963
16016
  }
15964
16017
  capabilities = built;
15965
16018
  }
16019
+ const rawMd = definition["modelsDev"];
16020
+ let validatedModelsDev;
16021
+ if (rawMd !== void 0) {
16022
+ if (!isRecord4(rawMd)) return null;
16023
+ const parsed = modelsDevModelSchema.safeParse({ ...rawMd, id: modelId });
16024
+ if (!parsed.success) return null;
16025
+ const { id: _parsedId, ...validMd } = parsed.data;
16026
+ validatedModelsDev = validMd;
16027
+ }
15966
16028
  out[modelId] = {
15967
16029
  ...typeof definition["name"] === "string" ? { name: definition["name"] } : {},
15968
16030
  ...typeof definition["provider"] === "string" ? { provider: definition["provider"] } : {},
15969
16031
  ...typeof definition["maxOutput"] === "number" ? { maxOutput: definition["maxOutput"] } : {},
15970
- ...capabilities ? { capabilities } : {}
16032
+ ...capabilities ? { capabilities } : {},
16033
+ ...validatedModelsDev ? { modelsDev: validatedModelsDev } : {}
15971
16034
  };
15972
16035
  }
15973
16036
  return out;
@@ -16071,6 +16134,30 @@ async function handleProviderRoute(ws, msg, routes) {
16071
16134
  await routes.providerHandlers.handleProviderClearModels(ws, providerId);
16072
16135
  return true;
16073
16136
  }
16137
+ case "provider.custom_models.set": {
16138
+ const payload = asPayloadRecord(msg);
16139
+ const providerId = payload ? requiredString(payload, "providerId") : null;
16140
+ const modelId = payload ? requiredString(payload, "modelId") : null;
16141
+ if (!payload || !providerId || !SAFE_CONFIG_KEY.test(providerId) || !modelId || !SAFE_CONFIG_KEY.test(modelId)) {
16142
+ return invalidPayload(ws, msg.type);
16143
+ }
16144
+ const customModelRaw = payload["customModel"];
16145
+ if (!isRecord4(customModelRaw)) return invalidPayload(ws, msg.type);
16146
+ const cm = optionalCustomModels({ customModels: { [modelId]: customModelRaw } });
16147
+ if (!cm || !cm[modelId]) return invalidPayload(ws, msg.type);
16148
+ await routes.providerHandlers.handleCustomModelSet(ws, providerId, modelId, cm[modelId]);
16149
+ return true;
16150
+ }
16151
+ case "provider.custom_models.remove": {
16152
+ const payload = asPayloadRecord(msg);
16153
+ const providerId = payload ? requiredString(payload, "providerId") : null;
16154
+ const modelId = payload ? requiredString(payload, "modelId") : null;
16155
+ if (!payload || !providerId || !SAFE_CONFIG_KEY.test(providerId) || !modelId || !SAFE_CONFIG_KEY.test(modelId)) {
16156
+ return invalidPayload(ws, msg.type);
16157
+ }
16158
+ await routes.providerHandlers.handleCustomModelRemove(ws, providerId, modelId);
16159
+ return true;
16160
+ }
16074
16161
  case "provider.undo_clear": {
16075
16162
  const payload = asPayloadRecord(msg);
16076
16163
  const providerId = payload ? requiredString(payload, "providerId") : null;
@@ -98,6 +98,8 @@ export declare function createProviderOperations(deps: ProviderOperationsDeps):
98
98
  }) => Promise<void>;
99
99
  handleProviderRemove: (ws: WebSocket, providerId: string) => Promise<void>;
100
100
  handleProviderClearModels: (ws: WebSocket, providerId: string) => Promise<void>;
101
+ handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
102
+ handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
101
103
  handleProviderUndoClear: (ws: WebSocket, providerId: string, previousModels: string[]) => Promise<void>;
102
104
  handleProviderUpdate: (ws: WebSocket, payload: {
103
105
  id: string;
@@ -138,6 +140,8 @@ export declare function createProviderHandlers(deps: ProviderHandlerDeps): {
138
140
  }) => Promise<void>;
139
141
  handleProviderRemove: (ws: WebSocket, providerId: string) => Promise<void>;
140
142
  handleProviderClearModels: (ws: WebSocket, providerId: string) => Promise<void>;
143
+ handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
144
+ handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
141
145
  handleProviderUndoClear: (ws: WebSocket, providerId: string, previousModels: string[]) => Promise<void>;
142
146
  handleProviderUpdate: (ws: WebSocket, payload: {
143
147
  id: string;
@@ -17,6 +17,8 @@ export interface ProviderMutationHandlers {
17
17
  }) => Promise<void>;
18
18
  handleProviderRemove: (ws: WebSocket, providerId: string) => Promise<void>;
19
19
  handleProviderClearModels: (ws: WebSocket, providerId: string) => Promise<void>;
20
+ handleCustomModelSet: (ws: WebSocket, providerId: string, modelId: string, definition: NonNullable<ProviderConfig['customModels']>[string]) => Promise<void>;
21
+ handleCustomModelRemove: (ws: WebSocket, providerId: string, modelId: string) => Promise<void>;
20
22
  handleProviderUndoClear: (ws: WebSocket, providerId: string, previousModels: string[]) => Promise<void>;
21
23
  handleProviderUpdate: (ws: WebSocket, payload: {
22
24
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/webui-server",
3
- "version": "0.298.1",
3
+ "version": "0.298.2",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack WebUI HTTP/WebSocket server module — extracted from @wrongstack/webui in PR #243/244 to remove the CLI -> @wrongstack/webui/server cross-package edge (audit §3.1.1). Pure backend: HTTP routes, WebSocket handlers, MCP tool wrappers, HTML serving. The web frontend lives in @wrongstack/webui; this package is the standalone server it can run on.",
6
6
  "keywords": [
@@ -40,15 +40,15 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "ws": "^8.21.1",
43
- "@wrongstack/kanban": "0.298.1",
44
- "@wrongstack/core": "0.298.1",
45
- "@wrongstack/runtime": "0.298.1",
46
- "@wrongstack/mcp": "0.298.1",
47
- "@wrongstack/providers": "0.298.1",
48
- "@wrongstack/techstack": "0.298.1",
49
- "@wrongstack/sage": "0.298.1",
50
- "@wrongstack/sdd": "0.298.1",
51
- "@wrongstack/tools": "0.298.1"
43
+ "@wrongstack/core": "0.298.2",
44
+ "@wrongstack/sdd": "0.298.2",
45
+ "@wrongstack/runtime": "0.298.2",
46
+ "@wrongstack/kanban": "0.298.2",
47
+ "@wrongstack/sage": "0.298.2",
48
+ "@wrongstack/providers": "0.298.2",
49
+ "@wrongstack/techstack": "0.298.2",
50
+ "@wrongstack/tools": "0.298.2",
51
+ "@wrongstack/mcp": "0.298.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.1.2",