{"version":3,"file":"model-command.mjs","names":[],"sources":["../../../src/commands/model-command.ts"],"sourcesContent":["/**\n * /llm command — view or switch the LLM model at runtime.\n *\n * Usage:\n *   /llm              — show current model + available shortcuts\n *   /llm opus         — switch to Claude Opus 4.6\n *   /llm sonnet       — switch to Claude Sonnet 4.6\n *   /llm gemini       — switch to Gemini 3 Pro\n *   /llm gpt          — switch to GPT-5.2\n *   /llm <full-name>  — switch to any model\n *\n * Supports both direct Anthropic/OpenRouter keys and Bankr LLM Gateway.\n * When provider is \"bankr\", model IDs use Bankr format (e.g. bankr/claude-opus-4.6).\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nconst CONFIG_PATH = join(process.env.HOME ?? '/root', '.openclaw', 'openclaw.json');\n\n/**\n * Detect which LLM provider is active.\n * Priority: OPENCLAWNCH_LLM_PROVIDER env > infer from current model string > 'anthropic'\n */\nfunction getProvider(): 'anthropic' | 'openrouter' | 'bankr' | 'openai' {\n  const explicit = process.env.OPENCLAWNCH_LLM_PROVIDER;\n  if (explicit === 'bankr' || explicit === 'openrouter' || explicit === 'openai') return explicit;\n  if (explicit === 'anthropic') return 'anthropic';\n\n  // Infer from current model\n  const current = getCurrentModel();\n  if (current.startsWith('bankr/')) return 'bankr';\n  if (current.startsWith('openrouter/')) return 'openrouter';\n  if (current.startsWith('openai/')) return 'openai';\n  return 'anthropic';\n}\n\n// ─── Model Shortcuts ─────────────────────────────────────────────────\n// Maps friendly names to provider-specific model IDs.\n// Format: { shortcut: { anthropic, openrouter, bankr } }\ninterface ModelIds {\n  anthropic: string;\n  openrouter: string;\n  bankr: string;\n  openai: string;\n}\n\nconst MODEL_MAP: Record<string, ModelIds> = {\n  // Claude\n  'opus':           { anthropic: 'anthropic/claude-opus-4-6',              openrouter: 'openrouter/anthropic/claude-opus-4-6',              bankr: 'bankr/claude-opus-4.6',     openai: 'anthropic/claude-opus-4-6' },\n  'opus4.6':        { anthropic: 'anthropic/claude-opus-4-6',              openrouter: 'openrouter/anthropic/claude-opus-4-6',              bankr: 'bankr/claude-opus-4.6',     openai: 'anthropic/claude-opus-4-6' },\n  'opus4.5':        { anthropic: 'anthropic/claude-opus-4-5',              openrouter: 'openrouter/anthropic/claude-opus-4-5',              bankr: 'bankr/claude-opus-4.5',     openai: 'anthropic/claude-opus-4-5' },\n  'sonnet':         { anthropic: 'anthropic/claude-sonnet-4-20250514',     openrouter: 'openrouter/anthropic/claude-sonnet-4-20250514',     bankr: 'bankr/claude-sonnet-4.6',   openai: 'anthropic/claude-sonnet-4-20250514' },\n  'sonnet4.6':      { anthropic: 'anthropic/claude-sonnet-4-20250514',     openrouter: 'openrouter/anthropic/claude-sonnet-4-20250514',     bankr: 'bankr/claude-sonnet-4.6',   openai: 'anthropic/claude-sonnet-4-20250514' },\n  'sonnet4.5':      { anthropic: 'anthropic/claude-sonnet-4-5-20250514',   openrouter: 'openrouter/anthropic/claude-sonnet-4-5-20250514',   bankr: 'bankr/claude-sonnet-4.5',   openai: 'anthropic/claude-sonnet-4-5-20250514' },\n  'haiku':          { anthropic: 'anthropic/claude-haiku-3-20250514',      openrouter: 'openrouter/anthropic/claude-haiku-3-20250514',      bankr: 'bankr/claude-haiku-4.5',    openai: 'anthropic/claude-haiku-3-20250514' },\n\n  // Gemini\n  'gemini':         { anthropic: 'bankr/gemini-3-pro',    openrouter: 'openrouter/google/gemini-3-pro',    bankr: 'bankr/gemini-3-pro',    openai: 'openai/gpt-5.2' },\n  'gemini-pro':     { anthropic: 'bankr/gemini-3-pro',    openrouter: 'openrouter/google/gemini-3-pro',    bankr: 'bankr/gemini-3-pro',    openai: 'openai/gpt-5.2' },\n  'gemini-flash':   { anthropic: 'bankr/gemini-3-flash',  openrouter: 'openrouter/google/gemini-3-flash',  bankr: 'bankr/gemini-3-flash',  openai: 'openai/gpt-5-mini' },\n  'gemini3':        { anthropic: 'bankr/gemini-3-pro',    openrouter: 'openrouter/google/gemini-3-pro',    bankr: 'bankr/gemini-3-pro',    openai: 'openai/gpt-5.2' },\n  'gemini2.5':      { anthropic: 'bankr/gemini-2.5-pro',  openrouter: 'openrouter/google/gemini-2.5-pro',  bankr: 'bankr/gemini-2.5-pro',  openai: 'openai/gpt-5.2' },\n  'gemini2.5-flash':{ anthropic: 'bankr/gemini-2.5-flash',openrouter: 'openrouter/google/gemini-2.5-flash',bankr: 'bankr/gemini-2.5-flash',openai: 'openai/gpt-5-mini' },\n\n  // GPT\n  'gpt':            { anthropic: 'bankr/gpt-5.2',         openrouter: 'openrouter/openai/gpt-5.2',         bankr: 'bankr/gpt-5.2',        openai: 'openai/gpt-5.2' },\n  'gpt5':           { anthropic: 'bankr/gpt-5.2',         openrouter: 'openrouter/openai/gpt-5.2',         bankr: 'bankr/gpt-5.2',        openai: 'openai/gpt-5.2' },\n  'gpt5.2':         { anthropic: 'bankr/gpt-5.2',         openrouter: 'openrouter/openai/gpt-5.2',         bankr: 'bankr/gpt-5.2',        openai: 'openai/gpt-5.2' },\n  'codex':          { anthropic: 'bankr/gpt-5.2-codex',   openrouter: 'openrouter/openai/gpt-5.2-codex',   bankr: 'bankr/gpt-5.2-codex',  openai: 'openai/gpt-5.2-codex' },\n  'gpt-mini':       { anthropic: 'bankr/gpt-5-mini',      openrouter: 'openrouter/openai/gpt-5-mini',      bankr: 'bankr/gpt-5-mini',     openai: 'openai/gpt-5-mini' },\n  'gpt-nano':       { anthropic: 'bankr/gpt-5-nano',      openrouter: 'openrouter/openai/gpt-5-nano',      bankr: 'bankr/gpt-5-nano',     openai: 'openai/gpt-5-nano' },\n\n  // Other\n  'kimi':           { anthropic: 'bankr/kimi-k2.5',       openrouter: 'openrouter/moonshotai/kimi-k2.5',   bankr: 'bankr/kimi-k2.5',      openai: 'openai/gpt-5.2' },\n  'qwen':           { anthropic: 'bankr/qwen3-coder',     openrouter: 'openrouter/qwen/qwen3-coder',       bankr: 'bankr/qwen3-coder',    openai: 'openai/gpt-5.2' },\n};\n\nfunction getCurrentModel(): string {\n  try {\n    const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));\n    return cfg?.agents?.defaults?.model?.primary ?? 'unknown';\n  } catch {\n    return 'unknown (config not readable)';\n  }\n}\n\nfunction setModel(modelId: string): void {\n  let cfg: any;\n  try {\n    cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));\n  } catch {\n    // Config doesn't exist or is malformed — create minimal structure\n    cfg = {};\n  }\n  cfg.agents = cfg.agents ?? {};\n  cfg.agents.defaults = cfg.agents.defaults ?? {};\n  cfg.agents.defaults.model = cfg.agents.defaults.model ?? {};\n  cfg.agents.defaults.model.primary = modelId;\n  try {\n    const { mkdirSync } = require('node:fs');\n    const { dirname } = require('node:path');\n    mkdirSync(dirname(CONFIG_PATH), { recursive: true });\n    writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf8');\n  } catch (err) {\n    throw new Error(`Cannot write config: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\n// H7: Validate model IDs to prevent injection via config file writes\nconst SAFE_MODEL_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._\\-\\/]{0,120}$/;\n\nfunction validateModelId(input: string): string {\n  if (!SAFE_MODEL_ID_RE.test(input)) {\n    throw new Error(`Invalid model ID: \"${input.slice(0, 30)}\". Only alphanumeric, dots, hyphens, underscores, and slashes are allowed.`);\n  }\n  return input;\n}\n\n/** Resolve a shortcut or raw model name to the correct provider-prefixed ID. */\nfunction resolveModel(input: string): string {\n  const provider = getProvider();\n  const mapping = MODEL_MAP[input];\n  if (mapping) {\n    return validateModelId(mapping[provider] ?? mapping.bankr);\n  }\n  // Not a shortcut — validate and use as-is, but prefix if needed\n  validateModelId(input);\n  if (provider === 'bankr' && !input.includes('/')) {\n    return `bankr/${input}`;\n  }\n  if (provider === 'openrouter' && !input.startsWith('openrouter/')) {\n    if (input.startsWith('anthropic/') || input.startsWith('openai/') || input.startsWith('google/')) {\n      return `openrouter/${input}`;\n    }\n  }\n  return input;\n}\n\nconst PROVIDER_LABEL: Record<string, string> = {\n  anthropic: 'Anthropic (direct)',\n  openrouter: 'OpenRouter',\n  bankr: 'Bankr LLM Gateway',\n  openai: 'OpenAI (direct)',\n};\n\nexport const modelCommand = {\n  name: 'llm',\n  description: 'View or switch the LLM model (e.g. /llm sonnet)',\n  acceptsArgs: true,\n  requireAuth: true,\n  handler: async (ctx: any) => {\n    const args = (ctx?.args ?? ctx?.text ?? '').trim().toLowerCase();\n    const modelArg = args.replace(/^\\/llm\\s*/, '').trim();\n\n    const provider = getProvider();\n    const providerName = PROVIDER_LABEL[provider] ?? provider;\n\n    if (!modelArg) {\n      const current = getCurrentModel();\n      const isBankr = provider === 'bankr';\n\n      let text = `Current model: ${current}\\nProvider: ${providerName}\\n\\n`;\n      text += '**Claude**\\n';\n      text += '  /llm_opus — Opus 4.6 (most capable)\\n';\n      text += '  /llm_sonnet — Sonnet 4.6 (fast + capable)\\n';\n      text += '  /llm_haiku — Haiku 4.5 (fastest Claude)\\n';\n\n      if (isBankr) {\n        text += '\\n**Gemini**\\n';\n        text += '  /llm_gemini — Gemini 3 Pro\\n';\n        text += '  /llm_gemini_flash — Gemini 3 Flash\\n';\n        text += '\\n**GPT**\\n';\n        text += '  /llm_gpt — GPT-5.2\\n';\n        text += '  /llm_codex — GPT-5.2 Codex\\n';\n        text += '  /llm_gpt_mini — GPT-5 Mini\\n';\n        text += '  /llm_gpt_nano — GPT-5 Nano\\n';\n        text += '\\n**Other**\\n';\n        text += '  /llm_kimi — Kimi K2.5\\n';\n        text += '  /llm_qwen — Qwen3 Coder\\n';\n        text += '\\nCheck credits: /llmcredits\\nUsage breakdown: /llmcost';\n      }\n\n      text += '\\n\\nOr use a full model ID: `/llm <model-id>`';\n      return { text };\n    }\n\n    // Normalize underscores to hyphens for tappable slash commands\n    // e.g. /llm gemini_flash → gemini-flash\n    const normalized = modelArg.replace(/_/g, '-');\n\n    try {\n      const finalModel = resolveModel(normalized);\n      const previousModel = getCurrentModel();\n      setModel(finalModel);\n      return {\n        text: `Model switched.\\n\\n  Before: ${previousModel}\\n  Now: ${finalModel}\\n\\nTakes effect on your next message.`,\n      };\n    } catch (err) {\n      return {\n        text: `Failed to switch model: ${err instanceof Error ? err.message : String(err)}`,\n      };\n    }\n  },\n};\n\n// ─── Tappable /llm_<model> shortcut commands ────────────────────────────\n// Each delegates to the main handler with the model name as args.\n\nconst LLM_SHORTCUTS: Array<{ name: string; model: string; label: string }> = [\n  { name: 'llm_opus', model: 'opus', label: 'Claude Opus 4.6' },\n  { name: 'llm_sonnet', model: 'sonnet', label: 'Claude Sonnet 4.6' },\n  { name: 'llm_haiku', model: 'haiku', label: 'Claude Haiku 4.5' },\n  { name: 'llm_gemini', model: 'gemini', label: 'Gemini 3 Pro' },\n  { name: 'llm_gemini_flash', model: 'gemini-flash', label: 'Gemini 3 Flash' },\n  { name: 'llm_gpt', model: 'gpt', label: 'GPT-5.2' },\n  { name: 'llm_codex', model: 'codex', label: 'GPT-5.2 Codex' },\n  { name: 'llm_gpt_mini', model: 'gpt-mini', label: 'GPT-5 Mini' },\n  { name: 'llm_gpt_nano', model: 'gpt-nano', label: 'GPT-5 Nano' },\n  { name: 'llm_kimi', model: 'kimi', label: 'Kimi K2.5' },\n  { name: 'llm_qwen', model: 'qwen', label: 'Qwen3 Coder' },\n];\n\nexport const llmShortcutCommands = LLM_SHORTCUTS.map(({ name, model, label }) => ({\n  name,\n  description: `Switch to ${label}`,\n  acceptsArgs: false,\n  requireAuth: true,\n  handler: async () => modelCommand.handler({ args: model }),\n}));\n"],"mappings":"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"}